diff --git a/Sources/SwiftGraphQLCLI/main.swift b/Sources/SwiftGraphQLCLI/main.swift index 58f611b..efb91bf 100755 --- a/Sources/SwiftGraphQLCLI/main.swift +++ b/Sources/SwiftGraphQLCLI/main.swift @@ -122,7 +122,8 @@ struct SwiftGraphQLCLI: ParsableCommand { files = try generator.generate( schema: schema, generateStaticFields: config.generateStaticFields != false, - singleFile: singleFileOutput + singleFile: singleFileOutput, + enumFallbackCase: config.enumFallbackCase ) generateCodeSpinner.success("API generated successfully!") } catch CodegenError.formatting(let err) { @@ -197,6 +198,9 @@ struct Config: Codable, Equatable { /// Whether to generate static lookups for object fields var generateStaticFields: Bool? + /// An extra fallback case for all enums if decoding fails. "unknown": is a recommended value. If the case already exists in the enum it will use that, otherwise generate a new case + var enumFallbackCase: String? + // MARK: - Initializers /// Creates an empty configuration instance. diff --git a/Sources/SwiftGraphQLCodegen/Generator.swift b/Sources/SwiftGraphQLCodegen/Generator.swift index 237b012..523cef1 100644 --- a/Sources/SwiftGraphQLCodegen/Generator.swift +++ b/Sources/SwiftGraphQLCodegen/Generator.swift @@ -21,7 +21,7 @@ public struct GraphQLCodegen { /// - generateStaticFields: Whether to generate static selections for fields on objects /// - singleFile: Whether to return all the swift code in a single file /// - Returns: A list of generated files - public func generate(schema: Schema, generateStaticFields: Bool, singleFile: Bool = false) throws -> [GeneratedFile] { + public func generate(schema: Schema, generateStaticFields: Bool, singleFile: Bool = false, enumFallbackCase: String? = nil) throws -> [GeneratedFile] { let context = Context(schema: schema, scalars: self.scalars) let subscription = schema.operations.first { $0.isSubscription }?.type.name @@ -87,7 +87,7 @@ public struct GraphQLCodegen { } for enumSchema in schema.enums { - try addFile(name: "Enums/\(enumSchema.name)", contents: enumSchema.declaration) + try addFile(name: "Enums/\(enumSchema.name)", contents: enumSchema.declaration(fallbackCase: enumFallbackCase)) } for interface in schema.interfaces { diff --git a/Sources/SwiftGraphQLCodegen/Generator/Enum.swift b/Sources/SwiftGraphQLCodegen/Generator/Enum.swift index 24f58ba..1b15298 100644 --- a/Sources/SwiftGraphQLCodegen/Generator/Enum.swift +++ b/Sources/SwiftGraphQLCodegen/Generator/Enum.swift @@ -5,17 +5,17 @@ import SwiftGraphQLUtils extension EnumType { /// Represents the enum structure. - var declaration: String { + func declaration(fallbackCase: String? = nil) -> String { """ extension Enums { \(docs) public enum \(name.pascalCase): String, CaseIterable, Codable { - \(values) + \(values(fallbackCase: fallbackCase)) } } extension Enums.\(name.pascalCase): GraphQLScalar { - \(decode) + \(decode(fallbackCase: fallbackCase)) \(mock) } @@ -29,13 +29,24 @@ extension EnumType { } /// Represents possible enum cases. - private var values: String { - enumValues.map { $0.declaration }.joined(separator: "\n") + private func values(fallbackCase: String?) -> String { + var cases = enumValues.map { $0.declaration } + if let fallbackCase, !enumValues.contains(where: { $0.name == fallbackCase }) { + let fallbackEnumValue = EnumValue( + name: fallbackCase, + description: "Fallback in case decoding fails.", + isDeprecated: false, + deprecationReason: nil + ) + cases.append("") + cases.append(fallbackEnumValue.declaration) + } + return cases.joined(separator: "\n") } // MARK: - GraphQL Scalar - private var decode: String { + private func decode(fallbackCase: String?) -> String { return """ public init(from data: AnyCodable) throws { switch data.value { @@ -43,7 +54,7 @@ extension EnumType { if let value = Enums.\(self.name.pascalCase)(rawValue: string) { self = value } else { - throw ScalarDecodingError.unknownEnumCase(value: string) + \(fallbackCase != nil ? "self = .\(fallbackCase!.camelCasePreservingSurroundingUnderscores.normalize)" : "throw ScalarDecodingError.unknownEnumCase(value: string)") } default: throw ScalarDecodingError.unexpectedScalarType( diff --git a/Tests/SwiftGraphQLCodegenTests/Generator/EnumTests.swift b/Tests/SwiftGraphQLCodegenTests/Generator/EnumTests.swift index 17bcb1b..583cb8f 100644 --- a/Tests/SwiftGraphQLCodegenTests/Generator/EnumTests.swift +++ b/Tests/SwiftGraphQLCodegenTests/Generator/EnumTests.swift @@ -38,7 +38,7 @@ final class EnumTests: XCTestCase { ] ) - let generated = try type.declaration.format() + let generated = try type.declaration().format() generated.assertInlineSnapshot(matching: """ extension Enums { @@ -78,4 +78,113 @@ final class EnumTests: XCTestCase { } """) } + + func testFallbackCase() throws { + + let type = EnumType( + name: "Episodes", + description: "Collection of all StarWars episodes.\nEarliest trilogy.", + enumValues: [ + EnumValue( + name: "NEWHOPE", + description: "Released in 1977.", + isDeprecated: false, + deprecationReason: nil + ), + ] + ) + + let generated = try type.declaration(fallbackCase: "unknown").format() + + generated.assertInlineSnapshot(matching: """ + extension Enums { + /// Collection of all StarWars episodes. + /// Earliest trilogy. + public enum Episodes: String, CaseIterable, Codable { + /// Released in 1977. + case newhope = "NEWHOPE" + + /// Fallback in case decoding fails. + case unknown = "unknown" + } + } + + extension Enums.Episodes: GraphQLScalar { + public init(from data: AnyCodable) throws { + switch data.value { + case let string as String: + if let value = Enums.Episodes(rawValue: string) { + self = value + } else { + self = .unknown + } + default: + throw ScalarDecodingError.unexpectedScalarType( + expected: "Episodes", + received: data.value + ) + } + } + + public static var mockValue = Self.newhope + } + """) + } + + func testMatchingFallbackCase() throws { + + let type = EnumType( + name: "Episodes", + description: "Collection of all StarWars episodes.\nEarliest trilogy.", + enumValues: [ + EnumValue( + name: "NEWHOPE", + description: "Released in 1977.", + isDeprecated: false, + deprecationReason: nil + ), + EnumValue( + name: "unknown", + description: "An unknown episode", + isDeprecated: false, + deprecationReason: nil + ), + ] + ) + + let generated = try type.declaration(fallbackCase: "unknown").format() + + generated.assertInlineSnapshot(matching: """ + extension Enums { + /// Collection of all StarWars episodes. + /// Earliest trilogy. + public enum Episodes: String, CaseIterable, Codable { + /// Released in 1977. + case newhope = "NEWHOPE" + /// An unknown episode + case unknown = "unknown" + } + } + + extension Enums.Episodes: GraphQLScalar { + public init(from data: AnyCodable) throws { + switch data.value { + case let string as String: + if let value = Enums.Episodes(rawValue: string) { + self = value + } else { + self = .unknown + } + default: + throw ScalarDecodingError.unexpectedScalarType( + expected: "Episodes", + received: data.value + ) + } + } + + public static var mockValue = Self.newhope + } + """) + } }