Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Sources/CodecHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,41 @@ public class CodecHelper<K: CodingKey> {
return try container.decode(type, forKey: forKey)
}

// This is public because the class is used by generated SDK code
public init() {}
}

class SingleValueCodecHelper {
func encodeSingle(_ value: Encodable, container: inout SingleValueEncodingContainer) throws {
switch value {
case let int64Value as Int64:
let int64Converter = Int64CodableConverter()
let int64Value = try int64Converter.encode(input: int64Value)
try container.encode(int64Value)
case let uuidValue as UUID:
let uuidConverter = UUIDCodableConverter()
let uuidValue = try uuidConverter.encode(input: uuidValue)
try container.encode(uuidValue)
default:
try container.encode(value)
}
}

func decodeSingle<T: Decodable>(_ type: T.Type,
container: inout SingleValueDecodingContainer) throws -> T {
if type == Int64.self || type == Int64?.self {
let int64String = try? container.decode(String.self)
let int64Converter = Int64CodableConverter()
let int64Value = try int64Converter.decode(input: int64String)
return int64Value as! T
} else if type == UUID.self || type == UUID?.self {
let uuidString = try container.decode(String.self)
let uuidConverter = UUIDCodableConverter()
let uuidDecoded = try uuidConverter.decode(input: uuidString)

return uuidDecoded as! T
} else {
return try container.decode(type)
}
}
}
129 changes: 119 additions & 10 deletions Sources/Scalars/AnyValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,71 @@ import Foundation
/// Double, String, Bool,...) or a JSON object
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public struct AnyValue {
public private(set) var value: Data
public var value: Data {
do {
let jsonEncoder = JSONEncoder()
let data = try jsonEncoder.encode(anyCodableValue)
return data
} catch {
DataConnectLogger.logger.warning("Error encoding anyCodableValue \(error)")
return Data()
}
}

private var anyCodableValue: AnyCodableValue

public init(codableValue: Codable) throws {
do {
let jsonEncoder = JSONEncoder()
value = try jsonEncoder.encode(codableValue)
if let int64Val = codableValue as? Int64 {
anyCodableValue = .int64(int64Val)
} else {
// to recontruct JSON dictionary, one has to decode it from json data
let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(codableValue)
let jsonDecoder = JSONDecoder()
anyCodableValue = try jsonDecoder.decode(AnyCodableValue.self, from: jsonData)
}
}
}

public func decodeValue<T: Decodable>(_ type: T.Type) throws -> T? {
do {
let jsonDecoder = JSONDecoder()
let decodedResult = try jsonDecoder.decode(type, from: value)
return decodedResult
switch anyCodableValue {
case let .int64(int64):
if type == Int64.self {
return int64 as? T
} else {
throw DataConnectCodecError.decodingFailed()
}
default:
let jsonDecoder = JSONDecoder()
let decodedResult = try jsonDecoder.decode(type, from: value)
return decodedResult
}
}
}
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension AnyValue: Codable {
public init(from decoder: any Decoder) throws {
let singleValueContainer = try decoder.singleValueContainer()
value = try singleValueContainer.decode(Data.self)
public init(from decoder: any Swift.Decoder) throws {
var container = try decoder.singleValueContainer()
do {
if let b64Data = try? container.decode(Data.self) {
// backwards compatibility
let jsonDecoder = JSONDecoder()
anyCodableValue = try jsonDecoder.decode(AnyCodableValue.self, from: b64Data)
} else {
let codecHelper = SingleValueCodecHelper()
anyCodableValue = try codecHelper.decodeSingle(AnyCodableValue.self, container: &container)
}
}
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value)
let codecHelper = SingleValueCodecHelper()
try codecHelper.encodeSingle(anyCodableValue, container: &container)
}
}

Expand All @@ -65,3 +102,75 @@ extension AnyValue: Hashable {

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension AnyValue: Sendable {}

// MARK: -

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
enum AnyCodableValue: Codable, Equatable {
case string(String)
case int64(Int64)
case number(Double)
case bool(Bool)
case dictionary([String: AnyCodableValue])
case array([AnyCodableValue])
case null

static func == (lhs: AnyCodableValue, rhs: AnyCodableValue) -> Bool {
switch (lhs, rhs) {
case let (.string(l), .string(r)): return l == r
case let (.int64(l), .int64(r)): return l == r
case let (.number(l), .number(r)): return l == r
case let (.bool(l), .bool(r)): return l == r
case let (.dictionary(l), .dictionary(r)): return l == r
case let (.array(l), .array(r)): return l == r
case (.null, .null): return true
default: return false
}
}

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

if let stringVal = try? container.decode(String.self) {
if let int64Val = try? Int64CodableConverter().decode(input: stringVal) {
self = .int64(int64Val)
} else {
self = .string(stringVal)
}
} else if let doubleVal = try? container.decode(Double.self) {
self = .number(doubleVal)
} else if let boolVal = try? container.decode(Bool.self) {
self = .bool(boolVal)
} else if let dictVal = try? container.decode([String: AnyCodableValue].self) {
self = .dictionary(dictVal)
} else if let arrayVal = try? container.decode([AnyCodableValue].self) {
self = .array(arrayVal)
} else if container.decodeNil() {
self = .null
} else {
throw DataConnectCodecError
.decodingFailed(message: "Error decode AnyCodableValue from \(container)")
}
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case let .int64(value):
let encodedVal = try? Int64CodableConverter().encode(input: value)
try container.encode(encodedVal)
case let .string(value):
try container.encode(value)
case let .number(value):
try container.encode(value)
case let .bool(value):
try container.encode(value)
case let .dictionary(value):
try container.encode(value)
case let .array(value):
try container.encode(value)
case .null:
try container.encodeNil()
}
}
}
37 changes: 37 additions & 0 deletions Tests/Integration/AnyScalarTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,21 @@ final class AnyScalarTests: IntegrationTestBase {
XCTAssertEqual(testDouble, decodedResult)
}

func testAnyValueUUID() async throws {
let anyValueId = UUID()
_ = try await DataConnect.kitchenSinkConnector.createAnyValueTypeMutation.ref(
id: anyValueId,
props: AnyValue(codableValue: anyValueId)
).execute()

let result = try await DataConnect.kitchenSinkConnector.getAnyValueTypeQuery.ref(id: anyValueId)
.execute()
let anyValueResult = result.data.anyValueType?.props
let decodedResult = try anyValueResult?.decodeValue(UUID.self)

XCTAssertEqual(anyValueId, decodedResult)
}

func testAnyValueDoubleMin() async throws {
let testDouble = Double.leastNormalMagnitude
let anyTestData = try AnyValue(codableValue: testDouble)
Expand Down Expand Up @@ -223,4 +238,26 @@ final class AnyScalarTests: IntegrationTestBase {

XCTAssertEqual(structVal, decodedResult)
}

func testAnyValueArray() async throws {
let intArray = {
var ia = [Double]()
for _ in 1 ... 10 {
ia.append(Double.random(in: Double.leastNormalMagnitude ... Double.greatestFiniteMagnitude))
}
return ia
}()

let anyValueId = UUID()
_ = try await DataConnect.kitchenSinkConnector.createAnyValueTypeMutation
.execute(id: anyValueId, props: AnyValue(codableValue: intArray))

let result = try await DataConnect.kitchenSinkConnector.getAnyValueTypeQuery.execute(
id: anyValueId
)
let anyValueResult = result.data.anyValueType?.props
let decodedResult = try anyValueResult?.decodeValue([Double].self)

XCTAssertEqual(intArray, decodedResult)
}
}
75 changes: 75 additions & 0 deletions Tests/Unit/AnyValueCodableTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation
import XCTest

@testable import FirebaseDataConnect

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
final class AnyValueCodableTests: XCTestCase {
override func setUpWithError() throws {}

override func tearDownWithError() throws {}

func testAnyValueStringCodable() throws {
let stringVal = "Hello World \(Int.random(in: 1 ... 1000))"
let anyValue = try AnyValue(codableValue: stringVal)
let stringDecoded = try anyValue.decodeValue(String.self)
XCTAssert(stringVal == stringDecoded)
}

func testAnyValueDoubleRandomCodable() throws {
let doubleVal = Double
.random(in: Double.leastNormalMagnitude ... Double.greatestFiniteMagnitude)
let anyValue = try AnyValue(codableValue: doubleVal)
let doubleDecoded = try anyValue.decodeValue(Double.self)
XCTAssertEqual(doubleVal, doubleDecoded)
}

func testAnyValueDoubleMaxCodable() throws {
let doubleValMax = Double.greatestFiniteMagnitude
let anyValue = try AnyValue(codableValue: doubleValMax)
let doubleDecoded = try anyValue.decodeValue(Double.self)
XCTAssertEqual(doubleValMax, doubleDecoded)
}

func testAnyValueDoubleMinCodable() throws {
let doubleValMin = Double.leastNormalMagnitude
let anyValue = try AnyValue(codableValue: doubleValMin)
let doubleDecoded = try anyValue.decodeValue(Double.self)
XCTAssertEqual(doubleValMin, doubleDecoded)
}

func testAnyValueInt64RandomCodable() throws {
let int64 = Int64.random(in: Int64.min ... Int64.max)
let anyValue = try AnyValue(codableValue: int64)
let int64Decoded = try anyValue.decodeValue(Int64.self)
XCTAssertEqual(int64, int64Decoded)
}

func testAnyValueInt64MaxCodable() throws {
let int64 = Int64.max
let anyValue = try AnyValue(codableValue: int64)
let int64Decoded = try anyValue.decodeValue(Int64.self)
XCTAssertEqual(int64, int64Decoded)
}

func testAnyValueInt64MinCodable() throws {
let int64 = Int64.min
let anyValue = try AnyValue(codableValue: int64)
let int64Decoded = try anyValue.decodeValue(Int64.self)
XCTAssertEqual(int64, int64Decoded)
}
}
Loading