diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift index 9677a3f8b..9faa88c9b 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift @@ -58,6 +58,7 @@ class SelectionSetTemplateTests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: false, + generateDecodableTypes: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) @@ -3333,6 +3334,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_animalFragment.fragment, generateInitializers: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3411,6 +3413,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3499,6 +3502,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3587,6 +3591,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3675,6 +3680,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3768,6 +3774,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3867,6 +3874,7 @@ class SelectionSetTemplateTests: XCTestCase { let basicFragmentSubject = SelectionSetTemplate( definition: allAnimals_basicFragment.fragment, generateInitializers: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3959,6 +3967,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_animalFragment.fragment, generateInitializers: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -4051,6 +4060,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_animalFragment.fragment, generateInitializers: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -9684,6 +9694,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -9769,6 +9780,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_animalFragment.fragment, generateInitializers: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -11940,6 +11952,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentTemplate = SelectionSetTemplate( definition: detailsFragment.fragment, generateInitializers: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_ErrorHandling_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_ErrorHandling_Tests.swift index f7a549c91..7c88c5878 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_ErrorHandling_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_ErrorHandling_Tests.swift @@ -56,6 +56,7 @@ class SelectionSetTemplate_ErrorHandling_Tests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: true, + generateDecodableTypes: false, config: ApolloCodegen.ConfigurationContext(config: config), nonFatalErrorRecorder: errorRecorder, renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) @@ -79,6 +80,7 @@ class SelectionSetTemplate_ErrorHandling_Tests: XCTestCase { subject = SelectionSetTemplate( definition: fragment.irObject, generateInitializers: true, + generateDecodableTypes: false, config: ApolloCodegen.ConfigurationContext(config: config), nonFatalErrorRecorder: errorRecorder, renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_FieldMerging_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_FieldMerging_Tests.swift index d381d0c64..d6502c933 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_FieldMerging_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_FieldMerging_Tests.swift @@ -67,6 +67,7 @@ class SelectionSetTemplate_FieldMerging_Tests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: selectionSetInitializers, + generateDecodableTypes: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift index aeda3eb5b..1ce9d35e7 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift @@ -49,6 +49,7 @@ class SelectionSetTemplate_Initializers_Tests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: true, + generateDecodableTypes: false, config: ApolloCodegen.ConfigurationContext(config: config), nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) @@ -77,6 +78,7 @@ class SelectionSetTemplate_Initializers_Tests: XCTestCase { subject = SelectionSetTemplate( definition: fragment.irObject, generateInitializers: true, + generateDecodableTypes: false, config: ApolloCodegen.ConfigurationContext(config: config), nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_LocalCacheMutation_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_LocalCacheMutation_Tests.swift index 255cb4c4d..bf64df38e 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_LocalCacheMutation_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_LocalCacheMutation_Tests.swift @@ -50,6 +50,7 @@ class SelectionSetTemplate_LocalCacheMutationTests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: false, + generateDecodableTypes: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/TemplateString_DeprecationMessage_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/TemplateString_DeprecationMessage_Tests.swift index 2931c09cd..80c4d27c0 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/TemplateString_DeprecationMessage_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/TemplateString_DeprecationMessage_Tests.swift @@ -273,6 +273,7 @@ final class TemplateString_DeprecationMessage_Tests: XCTestCase { let subject = SelectionSetTemplate( definition: operation.irObject, generateInitializers: true, + generateDecodableTypes: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: { "does not matter" }() @@ -403,6 +404,7 @@ final class TemplateString_DeprecationMessage_Tests: XCTestCase { let subject = SelectionSetTemplate( definition: operation.irObject, generateInitializers: true, + generateDecodableTypes: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: { "does not matter" }() diff --git a/Tests/ApolloTests/DataDictTests.swift b/Tests/ApolloTests/DataDictTests.swift new file mode 100644 index 000000000..854c1b21a --- /dev/null +++ b/Tests/ApolloTests/DataDictTests.swift @@ -0,0 +1,161 @@ +import XCTest +@testable import Apollo +import ApolloAPI +import ApolloInternalTestHelpers +import Nimble + +class DataDictTests: XCTestCase { + + class Data: MockSelectionSet { + class Animal: MockSelectionSet { + class Predator: MockSelectionSet { } + } + } + + func test__encoding_simpleDataStructure_works() throws { + // given + let subject = DataDict( + data: [ + "__typename": "Animal", + "name": "Dog" + ], + fulfilledFragments: [], + deferredFragments: [] + ) + + // then + expect( + try JSONEncoder().encode(subject).jsonString() + ).to(match(""" + { + "__typename": "Animal", + "name": "Dog" + } + """)) + } + + func test__encoding_nestedDataStructure_works() throws { + // given + let subject = DataDict( + data: [ + "animals": [ + DataDict( + data: [ + "__typename": "Animal", + "name": "Dog" + ], + fulfilledFragments: [ + ObjectIdentifier(Data.Animal.self), + ], + deferredFragments: [ + ObjectIdentifier(Data.Animal.Predator.self) + ] + ), + DataDict( + data: [ + "__typename": "Animal", + "name": "Cat" + ], + fulfilledFragments: [ + ObjectIdentifier(Data.Animal.self), + ], + deferredFragments: [ + ObjectIdentifier(Data.Animal.Predator.self) + ] + ) + ] + ], + fulfilledFragments: [ + ObjectIdentifier(Data.self), + ] + ) + + // then + expect( + try JSONEncoder().encode(subject).jsonString() + ).to(match(""" + { + "animals": [ + { + "__typename": "Animal", + "name": "Dog" + }, { + "__typename": "Animal", + "name": "Cat" + } + ] + } + """)) + } + + func test__encoding_dataStructureWithArrayProperties_works() throws { + // givens + let subject = DataDict( + data: [ + "animals": [ + DataDict( + data: [ + "__typename": "Animal", + "name": "Dog" + ], + fulfilledFragments: [], + deferredFragments: [] + ), + DataDict( + data: [ + "__typename": "Animal", + "name": "Cat" + ], + fulfilledFragments: [], + deferredFragments: [] + ) + ], + "coordinates": [[1.0, 2.0], [3.0, 4.0], DataDict._NullValue], + ], + fulfilledFragments: [] + ) + + // then + expect( + try JSONEncoder().encode(subject).jsonString() + ).to(match(""" + { + "animals": [ + { + "__typename": "Animal", + "name": "Dog" + }, { + "__typename": "Animal", + "name": "Cat" + } + ], + "coordinates": [ + [1.0, 2.0], + [3.0, 4.0], + null + ] + } + """)) + } + + +} + +extension Data { + public func jsonString(ignoring ignoredKeys: [String] = []) throws -> String { + var object = try JSONSerialization.jsonObject(with: self, options: []) + if !ignoredKeys.isEmpty { + object = (object as? [String: Any?])? + .filter { !ignoredKeys.contains($0.key) } as Any + } + let data = try JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted, .sortedKeys]) + return String(data: data, encoding: .utf8)! + } +} + +func match(_ expectedValue: String) -> Matcher { + let expectedData = Data(expectedValue.utf8) + + let expectedString = try! expectedData.jsonString() + return equal(expectedString) +} diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift index 9810e006f..984ac35c1 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift @@ -165,6 +165,8 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { public let operations: OperationsFileOutput /// The local path structure for the test mock operation object files. public let testMocks: TestMockFileOutput + /// Whether to generate type validation for the generated code. + public let generateDecodableTypes: Bool /// This var helps maintain backwards compatibility with legacy operation manifest generation /// with the new `OperationManifestConfiguration` and will be fully removed in v2.0 @@ -174,6 +176,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { public struct Default { public static let operations: OperationsFileOutput = .inSchemaModule public static let testMocks: TestMockFileOutput = .none + public static let generateDecodableTypes: Bool = false } /// Designated initializer. @@ -188,15 +191,18 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { /// with persisted queries or /// [Automatic Persisted Queries (APQs)](https://www.apollographql.com/docs/apollo-server/performance/apq). /// Defaults to `nil`. + /// - generateDecodableTypes: Whether to generate type validation for the generated code. public init( schemaTypes: SchemaTypesFileOutput, operations: OperationsFileOutput = Default.operations, - testMocks: TestMockFileOutput = Default.testMocks + testMocks: TestMockFileOutput = Default.testMocks, + generateDecodableTypes: Bool = Default.generateDecodableTypes ) { self.schemaTypes = schemaTypes self.operations = operations self.testMocks = testMocks self.operationIDsPath = nil + self.generateDecodableTypes = generateDecodableTypes } // MARK: Codable @@ -206,6 +212,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { case operations case testMocks case operationIdentifiersPath + case generateDecodableTypes } /// `Decodable` implementation to allow for properties to be optional in the encoded JSON with @@ -230,6 +237,10 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { String.self, forKey: .operationIdentifiersPath ) + generateDecodableTypes = try values.decodeIfPresent( + Bool.self, + forKey: .generateDecodableTypes + ) ?? Default.generateDecodableTypes } public func encode(to encoder: any Encoder) throws { @@ -238,6 +249,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { try container.encode(self.schemaTypes, forKey: .schemaTypes) try container.encode(self.operations, forKey: .operations) try container.encode(self.testMocks, forKey: .testMocks) + try container.encode(self.generateDecodableTypes, forKey: .generateDecodableTypes) } } @@ -1698,18 +1710,21 @@ extension ApolloCodegenConfiguration.FileOutput { /// If `.none`, test mocks will not be generated. Defaults to `.none`. /// - operationIdentifiersPath: An absolute location to an operation id JSON map file /// for use with APQ registration. Defaults to `nil`. + /// - generateDecodableTypes: Whether to generate type validation for the generated code. @available(*, deprecated, renamed: "init(schemaTypes:operations:testMocks:)") @_disfavoredOverload public init( schemaTypes: ApolloCodegenConfiguration.SchemaTypesFileOutput, operations: ApolloCodegenConfiguration.OperationsFileOutput = Default.operations, testMocks: ApolloCodegenConfiguration.TestMockFileOutput = Default.testMocks, + generateDecodableTypes: Bool = Default.generateDecodableTypes, operationIdentifiersPath: String? ) { self.schemaTypes = schemaTypes self.operations = operations self.testMocks = testMocks self.operationIDsPath = operationIdentifiersPath + self.generateDecodableTypes = generateDecodableTypes } /// An absolute location to an operation id JSON map file. diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift index 4bd4f5f6a..fd6bb421a 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift @@ -25,6 +25,7 @@ struct FragmentTemplate: TemplateRenderer { struct \(fragment.generatedDefinitionName.asFragmentName): \ \(fragment.renderedSelectionSetType(config)), Fragment\ \(if: fragment.isIdentifiable, ", Identifiable")\ + \(if: config.config.output.generateDecodableTypes, ", Encodable")\ { \(if: includeDefinition, """ \(accessControlModifier(for: .member))\ @@ -36,6 +37,7 @@ struct FragmentTemplate: TemplateRenderer { \(SelectionSetTemplate( definition: fragment, generateInitializers: config.config.shouldGenerateSelectionSetInitializers(for: fragment), + generateDecodableTypes: config.config.output.generateDecodableTypes, config: config, nonFatalErrorRecorder: nonFatalErrorRecorder, renderAccessControl: { accessControlModifier(for: .member) }() diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift index 8f0483c15..0021140a9 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift @@ -33,6 +33,7 @@ struct LocalCacheMutationDefinitionTemplate: OperationTemplateRenderer { \(SelectionSetTemplate( definition: operation, generateInitializers: config.config.shouldGenerateSelectionSetInitializers(for: operation), + generateDecodableTypes: config.config.output.generateDecodableTypes, config: config, nonFatalErrorRecorder: nonFatalErrorRecorder, renderAccessControl: { accessControlModifier(for: .member) }() diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift index 7b25cc5b1..ee22dd942 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift @@ -36,6 +36,7 @@ struct OperationDefinitionTemplate: OperationTemplateRenderer { \(SelectionSetTemplate( definition: operation, generateInitializers: config.config.shouldGenerateSelectionSetInitializers(for: operation), + generateDecodableTypes: config.config.output.generateDecodableTypes, config: config, nonFatalErrorRecorder: nonFatalErrorRecorder, renderAccessControl: { accessControlModifier(for: .member) }() diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift index b5bda9593..35fecff55 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift @@ -9,6 +9,7 @@ struct SelectionSetTemplate { let definition: any IR.Definition let generateInitializers: Bool + let generateDecodableTypes: Bool let config: ApolloCodegen.ConfigurationContext let nonFatalErrorRecorder: ApolloCodegen.NonFatalError.Recorder let renderAccessControl: () -> String @@ -20,12 +21,14 @@ struct SelectionSetTemplate { init( definition: any IR.Definition, generateInitializers: Bool, + generateDecodableTypes: Bool, config: ApolloCodegen.ConfigurationContext, nonFatalErrorRecorder: ApolloCodegen.NonFatalError.Recorder, renderAccessControl: @autoclosure @escaping () -> String ) { self.definition = definition self.generateInitializers = generateInitializers + self.generateDecodableTypes = generateDecodableTypes self.config = config self.nonFatalErrorRecorder = nonFatalErrorRecorder self.renderAccessControl = renderAccessControl @@ -104,6 +107,7 @@ struct SelectionSetTemplate { \(renderAccessControl())\ struct \(fieldSelectionSetName): \(SelectionSetType())\ \(if: selectionSet.isIdentifiable, ", Identifiable")\ + \(if: config.output.generateDecodableTypes, ", Encodable")\ { \(BodyTemplate(context)) } @@ -121,6 +125,7 @@ struct SelectionSetTemplate { struct \(inlineFragment.renderedTypeName): \(SelectionSetType(asInlineFragment: true))\ \(if: inlineFragment.isCompositeInlineFragment, ", \(config.ApolloAPITargetName).CompositeInlineFragment")\ \(if: inlineFragment.isIdentifiable, ", Identifiable")\ + \(if: config.output.generateDecodableTypes, ", Encodable")\ { \(BodyTemplate(context)) } diff --git a/apollo-ios/Sources/ApolloAPI/DataDict.swift b/apollo-ios/Sources/ApolloAPI/DataDict.swift index e6f53873a..2c5aa9003 100644 --- a/apollo-ios/Sources/ApolloAPI/DataDict.swift +++ b/apollo-ios/Sources/ApolloAPI/DataDict.swift @@ -68,7 +68,7 @@ public struct DataDict: Hashable { get { if DataDict._AnyHashableCanBeCoerced { return _data[key] as! T - } else { + } else { let value = _data[key] if value == DataDict._NullValue { return (Optional.none as Any) as! T @@ -183,6 +183,97 @@ extension DataDict { } +// MARK: - Encodable +struct StringKey: CodingKey { + let stringValue: String + + init?(stringValue: String) { + self.stringValue = stringValue + } + + init(_ key: String) { + self.stringValue = key + } + + var intValue: Int? { nil } + + init?(intValue: Int) { + nil + } + +} + +extension DataDict: Encodable { + public func encode(to encoder: any Encoder) throws { + let keys = _data.keys.sorted() + var container = encoder.container(keyedBy: StringKey.self) + for key in keys { + if let value = _data[key] { + try value.encode(in: &container, at: StringKey(key)) + } + } + } +} + +extension AnyHashable { + func encode(in container: inout KeyedEncodingContainer, at key: StringKey) throws { + if let encodableValue = self.base as? (any Encodable) { + try encodableValue.encode(to: container.superEncoder(forKey: key)) + } else if let array = self.base as? [AnyHashable] { + var arrayContainer = container.nestedUnkeyedContainer(forKey: key) + for item in array { + try item.encode(in: &arrayContainer) + } + } else if let array = self.base as? [AnyHashable?] { + var arrayContainer = container.nestedUnkeyedContainer(forKey: key) + for item in array { + if let item = item { + try item.encode(in: &arrayContainer) + } else { + try arrayContainer.encodeNil() + } + } + } else if let optional = self.base as? Optional { + if let value = optional { + try value.encode(in: &container, at: key) + } else { + try container.encodeNil(forKey: key) + } + } else { + throw EncodingError.invalidValue(self.base, .init(codingPath: container.codingPath, debugDescription: "Unexpected type for encoding: \(type(of: self.base))")) + } + } + + func encode(in container: inout any UnkeyedEncodingContainer) throws { + if let encodableValue = self.base as? (any Encodable) { + try encodableValue.encode(to: container.superEncoder()) + } else if let array = self.base as? [AnyHashable] { + var arrayContainer = container.nestedUnkeyedContainer() + for item in array { + try item.encode(in: &arrayContainer) + } + } else if let array = self.base as? [AnyHashable?] { + var arrayContainer = container.nestedUnkeyedContainer() + for item in array { + if let item = item { + try item.encode(in: &arrayContainer) + } else { + try arrayContainer.encodeNil() + } + } + } else if let optional = self.base as? Optional { + if let value = optional { + try value.encode(in: &container) + } else { + try container.encodeNil() + } + } else { + throw EncodingError.invalidValue(self.base, .init(codingPath: container.codingPath, debugDescription: "Unexpected type for encoding: \(type(of: self.base))")) + } + } +} + + // MARK: - Value Conversion Helpers public protocol SelectionSetEntityValue { diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet.swift index c64f2b6d5..c705f4805 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet.swift @@ -121,6 +121,10 @@ extension SelectionSet { public var debugDescription: String { return "\(self.__data._data as AnyObject)" } + + public func encode(to encoder: Encoder) throws { + try self.__data.encode(to: encoder) + } } extension SelectionSet where Fragments: FragmentContainer {