Skip to content

Commit 9ae8c10

Browse files
Convert to storing AnyValue as JSON objects (#53)
1 parent 08b488b commit 9ae8c10

File tree

4 files changed

+267
-10
lines changed

4 files changed

+267
-10
lines changed

Sources/CodecHelper.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,41 @@ public class CodecHelper<K: CodingKey> {
6161
return try container.decode(type, forKey: forKey)
6262
}
6363

64+
// This is public because the class is used by generated SDK code
6465
public init() {}
6566
}
67+
68+
class SingleValueCodecHelper {
69+
func encodeSingle(_ value: Encodable, container: inout SingleValueEncodingContainer) throws {
70+
switch value {
71+
case let int64Value as Int64:
72+
let int64Converter = Int64CodableConverter()
73+
let int64Value = try int64Converter.encode(input: int64Value)
74+
try container.encode(int64Value)
75+
case let uuidValue as UUID:
76+
let uuidConverter = UUIDCodableConverter()
77+
let uuidValue = try uuidConverter.encode(input: uuidValue)
78+
try container.encode(uuidValue)
79+
default:
80+
try container.encode(value)
81+
}
82+
}
83+
84+
func decodeSingle<T: Decodable>(_ type: T.Type,
85+
container: inout SingleValueDecodingContainer) throws -> T {
86+
if type == Int64.self || type == Int64?.self {
87+
let int64String = try? container.decode(String.self)
88+
let int64Converter = Int64CodableConverter()
89+
let int64Value = try int64Converter.decode(input: int64String)
90+
return int64Value as! T
91+
} else if type == UUID.self || type == UUID?.self {
92+
let uuidString = try container.decode(String.self)
93+
let uuidConverter = UUIDCodableConverter()
94+
let uuidDecoded = try uuidConverter.decode(input: uuidString)
95+
96+
return uuidDecoded as! T
97+
} else {
98+
return try container.decode(type)
99+
}
100+
}
101+
}

Sources/Scalars/AnyValue.swift

Lines changed: 119 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,71 @@ import Foundation
1818
/// Double, String, Bool,...) or a JSON object
1919
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
2020
public struct AnyValue {
21-
public private(set) var value: Data
21+
public var value: Data {
22+
do {
23+
let jsonEncoder = JSONEncoder()
24+
let data = try jsonEncoder.encode(anyCodableValue)
25+
return data
26+
} catch {
27+
DataConnectLogger.logger.warning("Error encoding anyCodableValue \(error)")
28+
return Data()
29+
}
30+
}
31+
32+
private var anyCodableValue: AnyCodableValue
2233

2334
public init(codableValue: Codable) throws {
2435
do {
25-
let jsonEncoder = JSONEncoder()
26-
value = try jsonEncoder.encode(codableValue)
36+
if let int64Val = codableValue as? Int64 {
37+
anyCodableValue = .int64(int64Val)
38+
} else {
39+
// to recontruct JSON dictionary, one has to decode it from json data
40+
let jsonEncoder = JSONEncoder()
41+
let jsonData = try jsonEncoder.encode(codableValue)
42+
let jsonDecoder = JSONDecoder()
43+
anyCodableValue = try jsonDecoder.decode(AnyCodableValue.self, from: jsonData)
44+
}
2745
}
2846
}
2947

3048
public func decodeValue<T: Decodable>(_ type: T.Type) throws -> T? {
3149
do {
32-
let jsonDecoder = JSONDecoder()
33-
let decodedResult = try jsonDecoder.decode(type, from: value)
34-
return decodedResult
50+
switch anyCodableValue {
51+
case let .int64(int64):
52+
if type == Int64.self {
53+
return int64 as? T
54+
} else {
55+
throw DataConnectCodecError.decodingFailed()
56+
}
57+
default:
58+
let jsonDecoder = JSONDecoder()
59+
let decodedResult = try jsonDecoder.decode(type, from: value)
60+
return decodedResult
61+
}
3562
}
3663
}
3764
}
3865

3966
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
4067
extension AnyValue: Codable {
41-
public init(from decoder: any Decoder) throws {
42-
let singleValueContainer = try decoder.singleValueContainer()
43-
value = try singleValueContainer.decode(Data.self)
68+
public init(from decoder: any Swift.Decoder) throws {
69+
var container = try decoder.singleValueContainer()
70+
do {
71+
if let b64Data = try? container.decode(Data.self) {
72+
// backwards compatibility
73+
let jsonDecoder = JSONDecoder()
74+
anyCodableValue = try jsonDecoder.decode(AnyCodableValue.self, from: b64Data)
75+
} else {
76+
let codecHelper = SingleValueCodecHelper()
77+
anyCodableValue = try codecHelper.decodeSingle(AnyCodableValue.self, container: &container)
78+
}
79+
}
4480
}
4581

4682
public func encode(to encoder: any Encoder) throws {
4783
var container = encoder.singleValueContainer()
48-
try container.encode(value)
84+
let codecHelper = SingleValueCodecHelper()
85+
try codecHelper.encodeSingle(anyCodableValue, container: &container)
4986
}
5087
}
5188

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

66103
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
67104
extension AnyValue: Sendable {}
105+
106+
// MARK: -
107+
108+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
109+
enum AnyCodableValue: Codable, Equatable {
110+
case string(String)
111+
case int64(Int64)
112+
case number(Double)
113+
case bool(Bool)
114+
case dictionary([String: AnyCodableValue])
115+
case array([AnyCodableValue])
116+
case null
117+
118+
static func == (lhs: AnyCodableValue, rhs: AnyCodableValue) -> Bool {
119+
switch (lhs, rhs) {
120+
case let (.string(l), .string(r)): return l == r
121+
case let (.int64(l), .int64(r)): return l == r
122+
case let (.number(l), .number(r)): return l == r
123+
case let (.bool(l), .bool(r)): return l == r
124+
case let (.dictionary(l), .dictionary(r)): return l == r
125+
case let (.array(l), .array(r)): return l == r
126+
case (.null, .null): return true
127+
default: return false
128+
}
129+
}
130+
131+
init(from decoder: Decoder) throws {
132+
let container = try decoder.singleValueContainer()
133+
134+
if let stringVal = try? container.decode(String.self) {
135+
if let int64Val = try? Int64CodableConverter().decode(input: stringVal) {
136+
self = .int64(int64Val)
137+
} else {
138+
self = .string(stringVal)
139+
}
140+
} else if let doubleVal = try? container.decode(Double.self) {
141+
self = .number(doubleVal)
142+
} else if let boolVal = try? container.decode(Bool.self) {
143+
self = .bool(boolVal)
144+
} else if let dictVal = try? container.decode([String: AnyCodableValue].self) {
145+
self = .dictionary(dictVal)
146+
} else if let arrayVal = try? container.decode([AnyCodableValue].self) {
147+
self = .array(arrayVal)
148+
} else if container.decodeNil() {
149+
self = .null
150+
} else {
151+
throw DataConnectCodecError
152+
.decodingFailed(message: "Error decode AnyCodableValue from \(container)")
153+
}
154+
}
155+
156+
func encode(to encoder: Encoder) throws {
157+
var container = encoder.singleValueContainer()
158+
switch self {
159+
case let .int64(value):
160+
let encodedVal = try? Int64CodableConverter().encode(input: value)
161+
try container.encode(encodedVal)
162+
case let .string(value):
163+
try container.encode(value)
164+
case let .number(value):
165+
try container.encode(value)
166+
case let .bool(value):
167+
try container.encode(value)
168+
case let .dictionary(value):
169+
try container.encode(value)
170+
case let .array(value):
171+
try container.encode(value)
172+
case .null:
173+
try container.encodeNil()
174+
}
175+
}
176+
}

Tests/Integration/AnyScalarTests.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,21 @@ final class AnyScalarTests: IntegrationTestBase {
153153
XCTAssertEqual(testDouble, decodedResult)
154154
}
155155

156+
func testAnyValueUUID() async throws {
157+
let anyValueId = UUID()
158+
_ = try await DataConnect.kitchenSinkConnector.createAnyValueTypeMutation.ref(
159+
id: anyValueId,
160+
props: AnyValue(codableValue: anyValueId)
161+
).execute()
162+
163+
let result = try await DataConnect.kitchenSinkConnector.getAnyValueTypeQuery.ref(id: anyValueId)
164+
.execute()
165+
let anyValueResult = result.data.anyValueType?.props
166+
let decodedResult = try anyValueResult?.decodeValue(UUID.self)
167+
168+
XCTAssertEqual(anyValueId, decodedResult)
169+
}
170+
156171
func testAnyValueDoubleMin() async throws {
157172
let testDouble = Double.leastNormalMagnitude
158173
let anyTestData = try AnyValue(codableValue: testDouble)
@@ -223,4 +238,26 @@ final class AnyScalarTests: IntegrationTestBase {
223238

224239
XCTAssertEqual(structVal, decodedResult)
225240
}
241+
242+
func testAnyValueArray() async throws {
243+
let intArray = {
244+
var ia = [Double]()
245+
for _ in 1 ... 10 {
246+
ia.append(Double.random(in: Double.leastNormalMagnitude ... Double.greatestFiniteMagnitude))
247+
}
248+
return ia
249+
}()
250+
251+
let anyValueId = UUID()
252+
_ = try await DataConnect.kitchenSinkConnector.createAnyValueTypeMutation
253+
.execute(id: anyValueId, props: AnyValue(codableValue: intArray))
254+
255+
let result = try await DataConnect.kitchenSinkConnector.getAnyValueTypeQuery.execute(
256+
id: anyValueId
257+
)
258+
let anyValueResult = result.data.anyValueType?.props
259+
let decodedResult = try anyValueResult?.decodeValue([Double].self)
260+
261+
XCTAssertEqual(intArray, decodedResult)
262+
}
226263
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
import XCTest
17+
18+
@testable import FirebaseDataConnect
19+
20+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
21+
final class AnyValueCodableTests: XCTestCase {
22+
override func setUpWithError() throws {}
23+
24+
override func tearDownWithError() throws {}
25+
26+
func testAnyValueStringCodable() throws {
27+
let stringVal = "Hello World \(Int.random(in: 1 ... 1000))"
28+
let anyValue = try AnyValue(codableValue: stringVal)
29+
let stringDecoded = try anyValue.decodeValue(String.self)
30+
XCTAssert(stringVal == stringDecoded)
31+
}
32+
33+
func testAnyValueDoubleRandomCodable() throws {
34+
let doubleVal = Double
35+
.random(in: Double.leastNormalMagnitude ... Double.greatestFiniteMagnitude)
36+
let anyValue = try AnyValue(codableValue: doubleVal)
37+
let doubleDecoded = try anyValue.decodeValue(Double.self)
38+
XCTAssertEqual(doubleVal, doubleDecoded)
39+
}
40+
41+
func testAnyValueDoubleMaxCodable() throws {
42+
let doubleValMax = Double.greatestFiniteMagnitude
43+
let anyValue = try AnyValue(codableValue: doubleValMax)
44+
let doubleDecoded = try anyValue.decodeValue(Double.self)
45+
XCTAssertEqual(doubleValMax, doubleDecoded)
46+
}
47+
48+
func testAnyValueDoubleMinCodable() throws {
49+
let doubleValMin = Double.leastNormalMagnitude
50+
let anyValue = try AnyValue(codableValue: doubleValMin)
51+
let doubleDecoded = try anyValue.decodeValue(Double.self)
52+
XCTAssertEqual(doubleValMin, doubleDecoded)
53+
}
54+
55+
func testAnyValueInt64RandomCodable() throws {
56+
let int64 = Int64.random(in: Int64.min ... Int64.max)
57+
let anyValue = try AnyValue(codableValue: int64)
58+
let int64Decoded = try anyValue.decodeValue(Int64.self)
59+
XCTAssertEqual(int64, int64Decoded)
60+
}
61+
62+
func testAnyValueInt64MaxCodable() throws {
63+
let int64 = Int64.max
64+
let anyValue = try AnyValue(codableValue: int64)
65+
let int64Decoded = try anyValue.decodeValue(Int64.self)
66+
XCTAssertEqual(int64, int64Decoded)
67+
}
68+
69+
func testAnyValueInt64MinCodable() throws {
70+
let int64 = Int64.min
71+
let anyValue = try AnyValue(codableValue: int64)
72+
let int64Decoded = try anyValue.decodeValue(Int64.self)
73+
XCTAssertEqual(int64, int64Decoded)
74+
}
75+
}

0 commit comments

Comments
 (0)