From d0f0807566624ba23601ed21dec15aee45b50849 Mon Sep 17 00:00:00 2001 From: Zach FettersMoore <4425109+BobaFetters@users.noreply.github.com> Date: Sun, 21 Jan 2024 20:32:49 -0500 Subject: [PATCH 1/4] Adding config options for custom schema type names --- SampleConfig.json | 55 +++++++++++++++++++ ...ation+RenameSchemaTypesConfiguration.swift | 43 +++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 SampleConfig.json create mode 100644 apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+RenameSchemaTypesConfiguration.swift diff --git a/SampleConfig.json b/SampleConfig.json new file mode 100644 index 000000000..f3c9479ca --- /dev/null +++ b/SampleConfig.json @@ -0,0 +1,55 @@ +{ + "schemaNamespace" : "MySchema", + "schemaDownload": { + ... + }, + "input" : { + ... + }, + "output" : { + ... + }, + "options" : { + ... + "schemaCustomization" : { + "customTypeNames" : { + "InterfaceName": { + "interface": { + "name": "CustomInterfaceName" + } + }, + "ScalarName": { + "customScalar": { + "name": "CustomScalarName" + } + }, + "InputObjectName": { + "inputObject": { + "fields": { + "fieldOne": "CustomFieldOne" + }, + "name": "CustomInputObjectName" + } + }, + "ObjectName": { + "object": { + "name": "CustomObjectName" + } + }, + "UnionName": { + "union": { + "name": "CustomUnionName" + } + }, + "EnumName": { + "enum": { + "name": "CustomEnumName", + "cases": { + "enumCase": "CustomEnumCase" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+RenameSchemaTypesConfiguration.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+RenameSchemaTypesConfiguration.swift new file mode 100644 index 000000000..50271f1c1 --- /dev/null +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+RenameSchemaTypesConfiguration.swift @@ -0,0 +1,43 @@ +import Foundation + +struct SchemaCustomization: Codable { + + let customTypeNames: [String: CustomSchemaTypeName] + + // MARK: - Codable + + enum CodingKeys: CodingKey, CaseIterable { + case customTypeNames + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + try throwIfContainsUnexpectedKey( + container: values, + type: Self.self, + decoder: decoder + ) + + customTypeNames = try values.decode( + [String: CustomSchemaTypeName].self, + forKey: .customTypeNames + ) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.customTypeNames, forKey: .customTypeNames) + } + + // MARK: - Enums + + enum CustomSchemaTypeName: Codable { + case object(name: String) + case interface(name: String) + case customScalar(name: String) + case union(name: String) + case `enum`(name: String, cases: [String: String]) + case inputObject(name: String, fields: [String: String]) + } +} From 5e4dd2b992be42f2aa9b56421ae05a51786564a7 Mon Sep 17 00:00:00 2001 From: Zach FettersMoore <4425109+BobaFetters@users.noreply.github.com> Date: Fri, 26 Jan 2024 13:16:51 -0500 Subject: [PATCH 2/4] Updating SchemaCustomization and enums --- ...ation+RenameSchemaTypesConfiguration.swift | 43 ------ ...genConfiguration+SchemaCustomization.swift | 129 ++++++++++++++++++ 2 files changed, 129 insertions(+), 43 deletions(-) delete mode 100644 apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+RenameSchemaTypesConfiguration.swift create mode 100644 apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+SchemaCustomization.swift diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+RenameSchemaTypesConfiguration.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+RenameSchemaTypesConfiguration.swift deleted file mode 100644 index 50271f1c1..000000000 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+RenameSchemaTypesConfiguration.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation - -struct SchemaCustomization: Codable { - - let customTypeNames: [String: CustomSchemaTypeName] - - // MARK: - Codable - - enum CodingKeys: CodingKey, CaseIterable { - case customTypeNames - } - - public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - try throwIfContainsUnexpectedKey( - container: values, - type: Self.self, - decoder: decoder - ) - - customTypeNames = try values.decode( - [String: CustomSchemaTypeName].self, - forKey: .customTypeNames - ) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(self.customTypeNames, forKey: .customTypeNames) - } - - // MARK: - Enums - - enum CustomSchemaTypeName: Codable { - case object(name: String) - case interface(name: String) - case customScalar(name: String) - case union(name: String) - case `enum`(name: String, cases: [String: String]) - case inputObject(name: String, fields: [String: String]) - } -} diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+SchemaCustomization.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+SchemaCustomization.swift new file mode 100644 index 000000000..8bc20a2cb --- /dev/null +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+SchemaCustomization.swift @@ -0,0 +1,129 @@ +import Foundation + +struct SchemaCustomization: Codable { + + let customTypeNames: [String: CustomSchemaTypeName] + + // MARK: - Codable + + enum CodingKeys: CodingKey, CaseIterable { + case customTypeNames + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + try throwIfContainsUnexpectedKey( + container: values, + type: Self.self, + decoder: decoder + ) + + customTypeNames = try values.decode( + [String: CustomSchemaTypeName].self, + forKey: .customTypeNames + ) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.customTypeNames, forKey: .customTypeNames) + } + + // MARK: - Enums + + enum CustomSchemaTypeName: Codable, ExpressibleByStringLiteral { + case type(name: String) + case `enum`(name: String?, cases: [String: String]?) + case inputObject(name: String?, fields: [String: String]?) + + public init(stringLiteral value: String) { + self = .type(name: value) + } + + enum CodingKeys: CodingKey, CaseIterable { + case type + case `enum` + case inputObject + } + + enum TypeCodingKeys: CodingKey, CaseIterable { + case name + } + + enum EnumCodingKeys: CodingKey, CaseIterable { + case name + case cases + } + + enum InputObjectCodingKeys: CodingKey, CaseIterable { + case name + case fields + } + + public init(from decoder: Decoder) throws { + var customTypeName: CustomSchemaTypeName? + + if let container = try? decoder.container(keyedBy: CodingKeys.self) { + switch container.allKeys.first { + case .type: + let subContainer = try container.nestedContainer(keyedBy: TypeCodingKeys.self, forKey: .type) + let name = try subContainer.decode(String.self, forKey: .name) + customTypeName = .type(name: name) + break + case .enum: + let subContainer = try container.nestedContainer(keyedBy: EnumCodingKeys.self, forKey: .enum) + let name = try subContainer.decodeIfPresent(String.self, forKey: .name) + let cases = try subContainer.decodeIfPresent([String: String].self, forKey: .cases) + customTypeName = .enum(name: name, cases: cases) + break + case .inputObject: + let subContainer = try container.nestedContainer(keyedBy: InputObjectCodingKeys.self, forKey: .inputObject) + let name = try subContainer.decodeIfPresent(String.self, forKey: .name) + let fields = try subContainer.decodeIfPresent([String: String].self, forKey: .fields) + customTypeName = .inputObject(name: name, fields: fields) + break + case .none: + fatalError() + } + } else if let container = try? decoder.singleValueContainer() { + let name = try container.decode(String.self) + customTypeName = .type(name: name) + } + + if let customTypeName = customTypeName { + self = customTypeName + } else { + fatalError() + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .type(let name): + var container = encoder.singleValueContainer() + try container.encode(name) + case .enum(let name, let cases): + if cases == nil { + var container = encoder.singleValueContainer() + try container.encode(name) + } else { + var container = encoder.container(keyedBy: CodingKeys.self) + var subContainer = container.nestedContainer(keyedBy: EnumCodingKeys.self, forKey: .enum) + try subContainer.encodeIfPresent(name, forKey: .name) + try subContainer.encodeIfPresent(cases, forKey: .cases) + } + case .inputObject(let name, let fields): + if name != nil, fields == nil { + var container = encoder.singleValueContainer() + try container.encode(name) + } else { + var container = encoder.container(keyedBy: CodingKeys.self) + var subContainer = container.nestedContainer(keyedBy: InputObjectCodingKeys.self, forKey: .inputObject) + try subContainer.encodeIfPresent(name, forKey: .name) + try subContainer.encodeIfPresent(fields, forKey: .fields) + } + } + } + } +} From ff251858c1078fe1de81dd8626f1a955c1ec85ae Mon Sep 17 00:00:00 2001 From: Zach FettersMoore <4425109+BobaFetters@users.noreply.github.com> Date: Sat, 3 Feb 2024 05:38:36 -0500 Subject: [PATCH 3/4] Addressing feedback and adding tests --- ...olloCodegenConfigurationCodableTests.swift | 26 + ...nfiguration+SchemaCustomizationTests.swift | 605 ++++++++++++++++++ .../ApolloCodegenConfiguration.swift | 14 + ...genConfiguration+SchemaCustomization.swift | 306 ++++++--- 4 files changed, 850 insertions(+), 101 deletions(-) create mode 100644 Tests/ApolloCodegenTests/Configuration/ApolloCodegenConfiguration+SchemaCustomizationTests.swift diff --git a/Tests/ApolloCodegenTests/ApolloCodegenConfigurationCodableTests.swift b/Tests/ApolloCodegenTests/ApolloCodegenConfigurationCodableTests.swift index 1ef927d1b..706185a85 100644 --- a/Tests/ApolloCodegenTests/ApolloCodegenConfigurationCodableTests.swift +++ b/Tests/ApolloCodegenTests/ApolloCodegenConfigurationCodableTests.swift @@ -43,6 +43,18 @@ class ApolloCodegenConfigurationCodableTests: XCTestCase { ], deprecatedEnumCases: .exclude, schemaDocumentation: .exclude, + schemaCustomization: .init( + customTypeNames: [ + "MyEnum": .enum( + name: "CustomEnum", + cases: [ + "CaseOne": "CustomCaseOne" + ] + ), + "MyInterface": .type(name: "CustomInterface"), + "MyObject": .type(name: "CustomObject") + ] + ), cocoapodsCompatibleImportStatements: true, warningsOnDeprecatedUsage: .exclude, conversionStrategies:.init( @@ -104,6 +116,20 @@ class ApolloCodegenConfigurationCodableTests: XCTestCase { "definition" ], "pruneGeneratedFiles" : false, + "schemaCustomization" : { + "customTypeNames" : { + "MyEnum" : { + "enum" : { + "cases" : { + "CaseOne" : "CustomCaseOne" + }, + "name" : "CustomEnum" + } + }, + "MyInterface" : "CustomInterface", + "MyObject" : "CustomObject" + } + }, "schemaDocumentation" : "exclude", "selectionSetInitializers" : { "localCacheMutations" : true diff --git a/Tests/ApolloCodegenTests/Configuration/ApolloCodegenConfiguration+SchemaCustomizationTests.swift b/Tests/ApolloCodegenTests/Configuration/ApolloCodegenConfiguration+SchemaCustomizationTests.swift new file mode 100644 index 000000000..dc55f713f --- /dev/null +++ b/Tests/ApolloCodegenTests/Configuration/ApolloCodegenConfiguration+SchemaCustomizationTests.swift @@ -0,0 +1,605 @@ +import XCTest +import ApolloCodegenLib +import Nimble + +final class ApolloCodegenConfiguration_SchemaCustomizationTests: XCTestCase { + + var testJSONEncoder: JSONEncoder! + + override func setUpWithError() throws { + try super.setUpWithError() + testJSONEncoder = JSONEncoder() + testJSONEncoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes] + } + + override func tearDownWithError() throws { + testJSONEncoder = nil + try super.tearDownWithError() + } + + // MARK: - Custom Type Names + + func test__encodeType_withCustomName() throws { + // given + let subject = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyObject": .type(name: "CustomObject") + ] + ) + + let expected = """ + { + "customTypeNames" : { + "MyObject" : "CustomObject" + } + } + """ + + // when + let encodedJSON = try testJSONEncoder.encode(subject) + let actual = encodedJSON.asString + + //then + expect(actual).to(equalLineByLine(expected)) + } + + func test__decodeType_withCustomName() throws { + // given + let subject = """ + { + "customTypeNames" : { + "MyObject" : "CustomObject" + } + } + """ + + let expected = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyObject": .type(name: "CustomObject") + ] + ) + + // when + let actual = try JSONDecoder().decode(ApolloCodegenConfiguration.SchemaCustomization.self, from: subject.asData) + + //then + expect(actual).to(equal(expected)) + } + + func test__encodeEmptyType_shouldThrowError() throws { + // given + let subject = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyObject": .type(name: "") + ] + ) + + //then + expect { + _ = try self.testJSONEncoder.encode(subject) + }.to(throwError { error in + guard case let ApolloCodegenConfiguration.SchemaCustomization.Error.emptyCustomization(type) = error else { + fail("Expected .emptyCustomization, got .\(error)") + return + } + expect(type).to(equal("MyObject")) + }) + } + + func test__decodeEmptyType_shouldThrowError() throws { + // given + let subject = """ + { + "customTypeNames" : { + "MyObject" : "" + } + } + """ + + ///then + expect { + try JSONDecoder().decode(ApolloCodegenConfiguration.SchemaCustomization.self, from: subject.asData) + }.to(throwError { error in + guard case let ApolloCodegenConfiguration.SchemaCustomization.Error.emptyCustomization(type) = error else { + fail("Expected .emptyCustomization, got .\(error)") + return + } + expect(type).to(equal("MyObject")) + }) + } + + // MARK: - Custom Enum Names + + func test__encodeEnum_withCustomNameAndCases() throws { + // given + let subject = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyEnum": .enum( + name: "CustomEnum", + cases: [ + "CaseOne": "CustomCaseOne" + ] + ) + ] + ) + + let expected = """ + { + "customTypeNames" : { + "MyEnum" : { + "enum" : { + "cases" : { + "CaseOne" : "CustomCaseOne" + }, + "name" : "CustomEnum" + } + } + } + } + """ + + // when + let encodedJSON = try testJSONEncoder.encode(subject) + let actual = encodedJSON.asString + + //then + expect(actual).to(equalLineByLine(expected)) + } + + func test__decodeEnum_withCustomNameAndCases() throws { + // given + let subject = """ + { + "customTypeNames" : { + "MyEnum" : { + "enum" : { + "cases" : { + "CaseOne" : "CustomCaseOne" + }, + "name" : "CustomEnum" + } + } + } + } + """ + + let expected = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyEnum": .enum( + name: "CustomEnum", + cases: [ + "CaseOne": "CustomCaseOne" + ] + ) + ] + ) + + // when + let actual = try JSONDecoder().decode(ApolloCodegenConfiguration.SchemaCustomization.self, from: subject.asData) + + //then + expect(actual).to(equal(expected)) + } + + func test__encodeEnum_withCustomName_asType() throws { + // given + let subject = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyEnum": .enum( + name: "CustomEnum", + cases: nil + ) + ] + ) + + let expected = """ + { + "customTypeNames" : { + "MyEnum" : "CustomEnum" + } + } + """ + + // when + let encodedJSON = try testJSONEncoder.encode(subject) + let actual = encodedJSON.asString + + //then + expect(actual).to(equalLineByLine(expected)) + } + + func test__decodeEnum_withCustomName_asType() throws { + // given + let subject = """ + { + "customTypeNames" : { + "MyEnum" : { + "enum" : { + "name" : "CustomEnum" + } + } + } + } + """ + + let expected = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyEnum": .type(name: "CustomEnum") + ] + ) + + // when + let actual = try JSONDecoder().decode(ApolloCodegenConfiguration.SchemaCustomization.self, from: subject.asData) + + //then + expect(actual).to(equal(expected)) + } + + func test__encodeEnum_withCustomCases() throws { + // given + let subject = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyEnum": .enum( + name: nil, + cases: [ + "CaseOne": "CustomCaseOne" + ] + ) + ] + ) + + let expected = """ + { + "customTypeNames" : { + "MyEnum" : { + "enum" : { + "cases" : { + "CaseOne" : "CustomCaseOne" + } + } + } + } + } + """ + + // when + let encodedJSON = try testJSONEncoder.encode(subject) + let actual = encodedJSON.asString + + //then + expect(actual).to(equalLineByLine(expected)) + } + + func test__decodeEnum_withCustomCases() throws { + // given + let subject = """ + { + "customTypeNames" : { + "MyEnum" : { + "enum" : { + "cases" : { + "CaseOne" : "CustomCaseOne" + } + } + } + } + } + """ + + let expected = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyEnum": .enum( + name: nil, + cases: [ + "CaseOne": "CustomCaseOne" + ] + ) + ] + ) + + // when + let actual = try JSONDecoder().decode(ApolloCodegenConfiguration.SchemaCustomization.self, from: subject.asData) + + //then + expect(actual).to(equal(expected)) + } + + func test__encodeEmptyEnum_shouldThrowError() throws { + // given + let subject = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyEnum": .enum( + name: nil, + cases: [:] + ) + ] + ) + + //then + expect { + _ = try self.testJSONEncoder.encode(subject) + }.to(throwError { error in + guard case let ApolloCodegenConfiguration.SchemaCustomization.Error.emptyCustomization(type) = error else { + fail("Expected .emptyCustomization, got .\(error)") + return + } + expect(type).to(equal("MyEnum")) + }) + } + + func test__decodeEmptyEnum_shouldThrowError() throws { + // given + let subject = """ + { + "customTypeNames" : { + "MyEnum" : { + "enum" : { + "cases" : { + }, + "name" : "" + } + } + } + } + """ + + //then + expect { + try JSONDecoder().decode(ApolloCodegenConfiguration.SchemaCustomization.self, from: subject.asData) + }.to(throwError { error in + guard case let ApolloCodegenConfiguration.SchemaCustomization.Error.emptyCustomization(type) = error else { + fail("Expected .emptyCustomization, got .\(error)") + return + } + expect(type).to(equal("MyEnum")) + }) + } + + // MARK: - Custom InputObjects Names + + func test__encodeInputObject_withCustomNameAndFields() throws { + // given + let subject = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyInputObject": .inputObject( + name: "CustomInputObject", + fields: [ + "FieldOne": "CustomFieldOne" + ] + ) + ] + ) + + let expected = """ + { + "customTypeNames" : { + "MyInputObject" : { + "inputObject" : { + "fields" : { + "FieldOne" : "CustomFieldOne" + }, + "name" : "CustomInputObject" + } + } + } + } + """ + + // when + let encodedJSON = try testJSONEncoder.encode(subject) + let actual = encodedJSON.asString + + //then + expect(actual).to(equalLineByLine(expected)) + } + + func test__decodeInputObject_withCustomNameAndFields() throws { + // given + let subject = """ + { + "customTypeNames" : { + "MyInputObject" : { + "inputObject" : { + "fields" : { + "FieldOne" : "CustomFieldOne" + }, + "name" : "CustomInputObject" + } + } + } + } + """ + + let expected = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyInputObject": .inputObject( + name: "CustomInputObject", + fields: [ + "FieldOne": "CustomFieldOne" + ] + ) + ] + ) + + // when + let actual = try JSONDecoder().decode(ApolloCodegenConfiguration.SchemaCustomization.self, from: subject.asData) + + //then + expect(actual).to(equal(expected)) + } + + func test__encodeInputObject_withCustomName_asType() throws { + // given + let subject = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyInputObject": .inputObject( + name: "CustomInputObject", + fields: nil + ) + ] + ) + + let expected = """ + { + "customTypeNames" : { + "MyInputObject" : "CustomInputObject" + } + } + """ + + // when + let encodedJSON = try testJSONEncoder.encode(subject) + let actual = encodedJSON.asString + + //then + expect(actual).to(equalLineByLine(expected)) + } + + func test__decodeInputObject_withCustomName_asType() throws { + // given + let subject = """ + { + "customTypeNames" : { + "MyInputObject" : { + "inputObject" : { + "name" : "CustomInputObject" + } + } + } + } + """ + + let expected = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyInputObject": .type(name: "CustomInputObject") + ] + ) + + // when + let actual = try JSONDecoder().decode(ApolloCodegenConfiguration.SchemaCustomization.self, from: subject.asData) + + //then + expect(actual).to(equal(expected)) + } + + func test__encodeInputObject_withCustomFields() throws { + // given + let subject = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyInputObject": .inputObject( + name: nil, + fields: [ + "FieldOne": "CustomFieldOne" + ] + ) + ] + ) + + let expected = """ + { + "customTypeNames" : { + "MyInputObject" : { + "inputObject" : { + "fields" : { + "FieldOne" : "CustomFieldOne" + } + } + } + } + } + """ + + // when + let encodedJSON = try testJSONEncoder.encode(subject) + let actual = encodedJSON.asString + + //then + expect(actual).to(equalLineByLine(expected)) + } + + func test__decodeInputObject_withCustomFields() throws { + // given + let subject = """ + { + "customTypeNames" : { + "MyInputObject" : { + "inputObject" : { + "fields" : { + "FieldOne" : "CustomFieldOne" + } + } + } + } + } + """ + + let expected = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyInputObject": .inputObject( + name: nil, + fields: [ + "FieldOne": "CustomFieldOne" + ] + ) + ] + ) + + // when + let actual = try JSONDecoder().decode(ApolloCodegenConfiguration.SchemaCustomization.self, from: subject.asData) + + //then + expect(actual).to(equal(expected)) + } + + func test__encodeEmptyInputObject_shouldThrowError() throws { + // given + let subject = ApolloCodegenConfiguration.SchemaCustomization( + customTypeNames: [ + "MyInputObject": .inputObject( + name: nil, + fields: [:] + ) + ] + ) + + //then + expect { + _ = try self.testJSONEncoder.encode(subject) + }.to(throwError { error in + guard case let ApolloCodegenConfiguration.SchemaCustomization.Error.emptyCustomization(type) = error else { + fail("Expected .emptyCustomization, got .\(error)") + return + } + expect(type).to(equal("MyInputObject")) + }) + } + + func test__decodeEmptyInputObject_shouldThrowError() throws { + // given + let subject = """ + { + "customTypeNames" : { + "MyInputObject" : { + "inputObject" : { + "fields" : { + }, + "name" : "" + } + } + } + } + """ + + ///then + expect { + try JSONDecoder().decode(ApolloCodegenConfiguration.SchemaCustomization.self, from: subject.asData) + }.to(throwError { error in + guard case let ApolloCodegenConfiguration.SchemaCustomization.Error.emptyCustomization(type) = error else { + fail("Expected .emptyCustomization, got .\(error)") + return + } + expect(type).to(equal("MyInputObject")) + }) + } + +} diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift index 1746668c6..be82e542c 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift @@ -468,6 +468,8 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { public let selectionSetInitializers: SelectionSetInitializers /// How to generate the operation documents for your generated operations. public let operationDocumentFormat: OperationDocumentFormat + /// Customization options to be applie to the schema during code generation. + public let schemaCustomization: SchemaCustomization /// Generate import statements that are compatible with including `Apollo` via Cocoapods. /// /// Cocoapods bundles all files from subspecs into the main target for a pod. This means that @@ -514,6 +516,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { public static let schemaDocumentation: Composition = .include public static let selectionSetInitializers: SelectionSetInitializers = [.localCacheMutations] public static let operationDocumentFormat: OperationDocumentFormat = .definition + public static let schemaCustomization: SchemaCustomization = .init() public static let cocoapodsCompatibleImportStatements: Bool = false public static let warningsOnDeprecatedUsage: Composition = .include public static let conversionStrategies: ConversionStrategies = .init() @@ -546,6 +549,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { schemaDocumentation: Composition = Default.schemaDocumentation, selectionSetInitializers: SelectionSetInitializers = Default.selectionSetInitializers, operationDocumentFormat: OperationDocumentFormat = Default.operationDocumentFormat, + schemaCustomization: SchemaCustomization = Default.schemaCustomization, cocoapodsCompatibleImportStatements: Bool = Default.cocoapodsCompatibleImportStatements, warningsOnDeprecatedUsage: Composition = Default.warningsOnDeprecatedUsage, conversionStrategies: ConversionStrategies = Default.conversionStrategies, @@ -557,6 +561,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { self.schemaDocumentation = schemaDocumentation self.selectionSetInitializers = selectionSetInitializers self.operationDocumentFormat = operationDocumentFormat + self.schemaCustomization = schemaCustomization self.cocoapodsCompatibleImportStatements = cocoapodsCompatibleImportStatements self.warningsOnDeprecatedUsage = warningsOnDeprecatedUsage self.conversionStrategies = conversionStrategies @@ -574,6 +579,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { case selectionSetInitializers case apqs case operationDocumentFormat + case schemaCustomization case cocoapodsCompatibleImportStatements case warningsOnDeprecatedUsage case conversionStrategies @@ -614,6 +620,11 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { forKey: .apqs )?.operationDocumentFormat ?? Default.operationDocumentFormat + + schemaCustomization = try values.decodeIfPresent( + SchemaCustomization.self, + forKey: .schemaCustomization + ) ?? Default.schemaCustomization cocoapodsCompatibleImportStatements = try values.decodeIfPresent( Bool.self, @@ -649,6 +660,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { try container.encode(self.schemaDocumentation, forKey: .schemaDocumentation) try container.encode(self.selectionSetInitializers, forKey: .selectionSetInitializers) try container.encode(self.operationDocumentFormat, forKey: .operationDocumentFormat) + try container.encode(self.schemaCustomization, forKey: .schemaCustomization) try container.encode(self.cocoapodsCompatibleImportStatements, forKey: .cocoapodsCompatibleImportStatements) try container.encode(self.warningsOnDeprecatedUsage, forKey: .warningsOnDeprecatedUsage) try container.encode(self.conversionStrategies, forKey: .conversionStrategies) @@ -1368,6 +1380,7 @@ extension ApolloCodegenConfiguration.OutputOptions { self.conversionStrategies = conversionStrategies self.pruneGeneratedFiles = pruneGeneratedFiles self.markOperationDefinitionsAsFinal = markOperationDefinitionsAsFinal + self.schemaCustomization = Default.schemaCustomization } /// Deprecated initializer. @@ -1417,6 +1430,7 @@ extension ApolloCodegenConfiguration.OutputOptions { self.conversionStrategies = conversionStrategies self.pruneGeneratedFiles = pruneGeneratedFiles self.markOperationDefinitionsAsFinal = markOperationDefinitionsAsFinal + self.schemaCustomization = Default.schemaCustomization } /// Whether the generated operations should use Automatic Persisted Queries. diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+SchemaCustomization.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+SchemaCustomization.swift index 8bc20a2cb..5512078e1 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+SchemaCustomization.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+SchemaCustomization.swift @@ -1,129 +1,233 @@ import Foundation -struct SchemaCustomization: Codable { +extension ApolloCodegenConfiguration { - let customTypeNames: [String: CustomSchemaTypeName] - - // MARK: - Codable - - enum CodingKeys: CodingKey, CaseIterable { - case customTypeNames - } - - public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - try throwIfContainsUnexpectedKey( - container: values, - type: Self.self, - decoder: decoder - ) + public struct SchemaCustomization: Codable, Equatable { - customTypeNames = try values.decode( - [String: CustomSchemaTypeName].self, - forKey: .customTypeNames - ) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) + // MARK: - Properties - try container.encode(self.customTypeNames, forKey: .customTypeNames) - } - - // MARK: - Enums - - enum CustomSchemaTypeName: Codable, ExpressibleByStringLiteral { - case type(name: String) - case `enum`(name: String?, cases: [String: String]?) - case inputObject(name: String?, fields: [String: String]?) - - public init(stringLiteral value: String) { - self = .type(name: value) + /// Dictionary with Keys representing the types being renamed/customized, and + public let customTypeNames: [String: CustomSchemaTypeName] + + /// Default property values + public struct Default { + public static let customTypeNames: [String: CustomSchemaTypeName] = [:] } - enum CodingKeys: CodingKey, CaseIterable { - case type - case `enum` - case inputObject + // MARK: - Initialization + + /// Designated initializer + /// + /// - Parameters: + /// - customTypeNames: Dictionary repsenting the types to be renamed and how to rename them. + public init( + customTypeNames: [String: CustomSchemaTypeName] = Default.customTypeNames + ) { + self.customTypeNames = customTypeNames } - enum TypeCodingKeys: CodingKey, CaseIterable { - case name + // MARK: - Codable + + enum CodingKeys: CodingKey, CaseIterable { + case customTypeNames } - enum EnumCodingKeys: CodingKey, CaseIterable { - case name - case cases + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + try throwIfContainsUnexpectedKey( + container: values, + type: Self.self, + decoder: decoder + ) + + customTypeNames = try values.decode( + [String: CustomSchemaTypeName].self, + forKey: .customTypeNames + ) } - enum InputObjectCodingKeys: CodingKey, CaseIterable { - case name - case fields + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.customTypeNames, forKey: .customTypeNames) } - public init(from decoder: Decoder) throws { - var customTypeName: CustomSchemaTypeName? + // MARK: - Enums + + public enum CustomSchemaTypeName: Codable, ExpressibleByStringLiteral, Equatable { + case type(name: String) + case `enum`(name: String?, cases: [String: String]?) + case inputObject(name: String?, fields: [String: String]?) + + public init(stringLiteral value: String) { + self = .type(name: value) + } - if let container = try? decoder.container(keyedBy: CodingKeys.self) { - switch container.allKeys.first { - case .type: - let subContainer = try container.nestedContainer(keyedBy: TypeCodingKeys.self, forKey: .type) - let name = try subContainer.decode(String.self, forKey: .name) - customTypeName = .type(name: name) - break - case .enum: - let subContainer = try container.nestedContainer(keyedBy: EnumCodingKeys.self, forKey: .enum) - let name = try subContainer.decodeIfPresent(String.self, forKey: .name) - let cases = try subContainer.decodeIfPresent([String: String].self, forKey: .cases) - customTypeName = .enum(name: name, cases: cases) - break - case .inputObject: - let subContainer = try container.nestedContainer(keyedBy: InputObjectCodingKeys.self, forKey: .inputObject) - let name = try subContainer.decodeIfPresent(String.self, forKey: .name) - let fields = try subContainer.decodeIfPresent([String: String].self, forKey: .fields) - customTypeName = .inputObject(name: name, fields: fields) - break - case .none: - fatalError() - } - } else if let container = try? decoder.singleValueContainer() { - let name = try container.decode(String.self) - customTypeName = .type(name: name) + enum CodingKeys: CodingKey, CaseIterable { + case type + case `enum` + case inputObject } - if let customTypeName = customTypeName { - self = customTypeName - } else { - fatalError() + enum TypeCodingKeys: CodingKey, CaseIterable { + case name } - } - - public func encode(to encoder: Encoder) throws { - switch self { - case .type(let name): - var container = encoder.singleValueContainer() - try container.encode(name) - case .enum(let name, let cases): - if cases == nil { - var container = encoder.singleValueContainer() - try container.encode(name) + + enum EnumCodingKeys: CodingKey, CaseIterable { + case name + case cases + } + + enum InputObjectCodingKeys: CodingKey, CaseIterable { + case name + case fields + } + + public init(from decoder: Decoder) throws { + guard let originalTypeName = decoder.codingPath.last?.stringValue else { + preconditionFailure("Unable to get original type name value from JSON during decoding.") + } + var customTypeName: CustomSchemaTypeName? + + if let container = try? decoder.container(keyedBy: CodingKeys.self) { + switch container.allKeys.first { + case .type: + let subContainer = try container.nestedContainer(keyedBy: TypeCodingKeys.self, forKey: .type) + let name = try subContainer.decodeIfPresentOrEmpty(type: String.self, key: .name) + guard let name = name else { + throw Error.emptyCustomization(type: originalTypeName) + } + customTypeName = .type(name: name) + break + case .enum: + let subContainer = try container.nestedContainer(keyedBy: EnumCodingKeys.self, forKey: .enum) + let name = try subContainer.decodeIfPresentOrEmpty(type: String.self, key: .name) + let cases = try subContainer.decodeIfPresentOrEmpty(type: [String: String].self, key: .cases) + + guard name != nil || cases != nil else { + throw Error.emptyCustomization(type: originalTypeName) + } + + if let name = name, cases == nil { + customTypeName = .type(name: name) + } else { + customTypeName = .enum(name: name, cases: cases) + } + break + case .inputObject: + let subContainer = try container.nestedContainer(keyedBy: InputObjectCodingKeys.self, forKey: .inputObject) + let name = try subContainer.decodeIfPresentOrEmpty(type: String.self, key: .name) + let fields = try subContainer.decodeIfPresentOrEmpty(type: [String: String].self, key: .fields) + + guard name != nil || fields != nil else { + throw Error.emptyCustomization(type: originalTypeName) + } + + if let name = name, fields == nil { + customTypeName = .type(name: name) + } else { + customTypeName = .inputObject(name: name, fields: fields) + } + break + case .none: + break + } + } else if let container = try? decoder.singleValueContainer() { + let name = try container.decode(String.self) + guard !name.isEmpty else { + throw Error.emptyCustomization(type: originalTypeName) + } + customTypeName = .type(name: name) + } + + if let customTypeName = customTypeName { + self = customTypeName } else { - var container = encoder.container(keyedBy: CodingKeys.self) - var subContainer = container.nestedContainer(keyedBy: EnumCodingKeys.self, forKey: .enum) - try subContainer.encodeIfPresent(name, forKey: .name) - try subContainer.encodeIfPresent(cases, forKey: .cases) + throw Error.decodingFailure(type: originalTypeName) + } + } + + public func encode(to encoder: Encoder) throws { + guard let originalTypeName = encoder.codingPath.last?.stringValue else { + preconditionFailure("Unable to get original type name value from type during decoding.") } - case .inputObject(let name, let fields): - if name != nil, fields == nil { + switch self { + case .type(let name): + guard !name.isEmpty else { + throw Error.emptyCustomization(type: originalTypeName) + } var container = encoder.singleValueContainer() try container.encode(name) - } else { - var container = encoder.container(keyedBy: CodingKeys.self) - var subContainer = container.nestedContainer(keyedBy: InputObjectCodingKeys.self, forKey: .inputObject) - try subContainer.encodeIfPresent(name, forKey: .name) - try subContainer.encodeIfPresent(fields, forKey: .fields) + case .enum(let name, let cases): + guard (name != nil && !(name ?? "").isEmpty) || (cases != nil && !(cases ?? [:]).isEmpty) else { + throw Error.emptyCustomization(type: originalTypeName) + } + + if cases == nil { + var container = encoder.singleValueContainer() + try container.encode(name) + } else { + var container = encoder.container(keyedBy: CodingKeys.self) + var subContainer = container.nestedContainer(keyedBy: EnumCodingKeys.self, forKey: .enum) + try subContainer.encodeIfPresent(name, forKey: .name) + try subContainer.encodeIfPresent(cases, forKey: .cases) + } + case .inputObject(let name, let fields): + guard (name != nil && !(name ?? "").isEmpty) || (fields != nil && !(fields ?? [:]).isEmpty) else { + throw Error.emptyCustomization(type: originalTypeName) + } + + if name != nil, fields == nil { + var container = encoder.singleValueContainer() + try container.encode(name) + } else { + var container = encoder.container(keyedBy: CodingKeys.self) + var subContainer = container.nestedContainer(keyedBy: InputObjectCodingKeys.self, forKey: .inputObject) + try subContainer.encodeIfPresent(name, forKey: .name) + try subContainer.encodeIfPresent(fields, forKey: .fields) + } + } + } + + } + + public enum Error: Swift.Error, LocalizedError { + case decodingFailure(type: String) + case emptyCustomization(type: String) + + public var errorDescription: String? { + switch self { + case let .decodingFailure(type): + return """ + Unable to decode type '\(type)' when processing custom schema + type names. + """ + case let .emptyCustomization(type): + return """ + No customization data was provided for type '\(type)', customization + will be ignored. + """ } } } + + } + +} + +extension KeyedDecodingContainer { + fileprivate func decodeIfPresentOrEmpty(type: String.Type, key: KeyedDecodingContainer.Key) throws -> String? { + if let value = try decodeIfPresent(type, forKey: key) { + return value.isEmpty ? nil : value + } + return nil + } + + fileprivate func decodeIfPresentOrEmpty(type: [String: String].Type, key: KeyedDecodingContainer.Key) throws -> [String: String]? { + if let value = try decodeIfPresent(type, forKey: key) { + return value.isEmpty ? nil : value + } + return nil } } + From ee0315ac450c3ad84dc760a29ac3893c06eb1db3 Mon Sep 17 00:00:00 2001 From: Zach FettersMoore <4425109+BobaFetters@users.noreply.github.com> Date: Mon, 5 Feb 2024 21:48:02 -0500 Subject: [PATCH 4/4] Updating sample json --- SampleConfig.json | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/SampleConfig.json b/SampleConfig.json index f3c9479ca..cef65ca43 100644 --- a/SampleConfig.json +++ b/SampleConfig.json @@ -13,16 +13,8 @@ ... "schemaCustomization" : { "customTypeNames" : { - "InterfaceName": { - "interface": { - "name": "CustomInterfaceName" - } - }, - "ScalarName": { - "customScalar": { - "name": "CustomScalarName" - } - }, + "InterfaceName": "CustomInterfaceName", + "ScalarName": "CustomScalarName", "InputObjectName": { "inputObject": { "fields": { @@ -31,16 +23,8 @@ "name": "CustomInputObjectName" } }, - "ObjectName": { - "object": { - "name": "CustomObjectName" - } - }, - "UnionName": { - "union": { - "name": "CustomUnionName" - } - }, + "ObjectName": "CustomObjectName", + "UnionName": "CustomUnionName", "EnumName": { "enum": { "name": "CustomEnumName",