Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add codegen option about convert enum case name strategy #2478

Merged
merged 26 commits into from
Sep 8, 2022
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
25 changes: 24 additions & 1 deletion Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {
/// directive.
public let warningsOnDeprecatedUsage: Composition

/// The conversion strategy for the generated code is in this set.
public let conversionStrategies: ConversionStrategies

/// Designated initializer.
///
/// - Parameters:
Expand All @@ -319,14 +322,16 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {
/// - warningsOnDeprecatedUsage: Annotate generated Swift code with the Swift `available`
/// attribute and `deprecated` argument for parts of the GraphQL schema annotated with the
/// built-in `@deprecated` directive.
/// - conversionStrategies: Conversion strategy for generated Swift code from GraphQL schema.
public init(
additionalInflectionRules: [InflectionRule] = [],
queryStringLiteralFormat: QueryStringLiteralFormat = .multiline,
deprecatedEnumCases: Composition = .include,
schemaDocumentation: Composition = .include,
apqs: APQConfig = .disabled,
cocoapodsCompatibleImportStatements: Bool = false,
warningsOnDeprecatedUsage: Composition = .include
warningsOnDeprecatedUsage: Composition = .include,
conversionStrategies: ConversionStrategies = .init()
) {
self.additionalInflectionRules = additionalInflectionRules
self.queryStringLiteralFormat = queryStringLiteralFormat
Expand All @@ -335,6 +340,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {
self.apqs = apqs
self.cocoapodsCompatibleImportStatements = cocoapodsCompatibleImportStatements
self.warningsOnDeprecatedUsage = warningsOnDeprecatedUsage
self.conversionStrategies = conversionStrategies
}
}

Expand All @@ -354,6 +360,23 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {
case exclude
}

/// CaseConversionStrategy is a strategy of convert to Swift enum from GraphQL schema.
public enum CaseConversionStrategy: String, Codable, Equatable {
/// Default. Nothing different from the definition of a schema.
case none
/// Convert to lower camel case from `snake_case`, `UpperCamelCase`,`UPPERCASE`.
case camelCase
}

/// `ConversionStrategies` is a set about option of code generation naming rules conversion strategy.
public struct ConversionStrategies: Codable, Equatable {
public let enumCases: CaseConversionStrategy

public init(enumCases: CaseConversionStrategy = .camelCase) {
self.enumCases = enumCases
}
}

/// Enum to enable using
/// [Automatic Persisted Queries (APQs)](https://www.apollographql.com/docs/apollo-server/performance/apq)
/// with your generated operations.
Expand Down
37 changes: 32 additions & 5 deletions Sources/ApolloCodegenLib/Templates/EnumTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,50 @@ struct EnumTemplate: TemplateRenderer {
switch (
config.options.deprecatedEnumCases,
graphqlEnumValue.deprecationReason,
config.options.warningsOnDeprecatedUsage
config.options.warningsOnDeprecatedUsage,
config.options.conversionStrategies.enumCases
) {
case (.exclude, .some, _):
case (.exclude, .some, _, _):
return nil

case let (.include, .some(reason), .include):
case let (.include, .some(reason), .include, .none):
return """
\(documentation: graphqlEnumValue.documentation, config: config)
@available(*, deprecated, message: \"\(reason)\")
case \(graphqlEnumValue.name.asEnumCaseName)
"""

default:
case let (.include, .some(reason), .include, .camelCase):
return """
\(documentation: graphqlEnumValue.documentation, config: config)
@available(*, deprecated, message: \"\(reason)\")
case \(convertToCamelCase(graphqlEnumValue.name).asEnumCaseName) = "\(graphqlEnumValue.name)"
"""
case (_, _, _, .none):
return """
\(documentation: graphqlEnumValue.documentation, config: config)
case \(graphqlEnumValue.name.asEnumCaseName)
"""
default:
return """
\(documentation: graphqlEnumValue.documentation, config: config)
case \(convertToCamelCase(graphqlEnumValue.name).asEnumCaseName) = "\(graphqlEnumValue.name)"
"""
}
}

private func convertToCamelCase(_ value: String) -> String {
if value.allSatisfy({ $0.isUppercase }) {
// For `UPPERCASE`. e.g) UPPER -> upper, STARWARS -> starwards
return value.lowercased()
}
if value.contains("_") {
// For `snake_case`. e.g) snake_case -> snakeCase, UPPER_SNAKE_CASE -> upperSnakeCase
return value.split(separator: "_").enumerated().map { $0.offset == 0 ? $0.element.lowercased() : $0.element.capitalized }.joined()
}
if let firstChar = value.first, firstChar.isUppercase {
// For `UpperCamelCase`. e.g) UpperCamelCase -> upperCamelCase
return [firstChar.lowercased(), String(value.suffix(from: value.index(value.startIndex, offsetBy: 1)))].joined()
}
return value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ class ApolloCodegenConfigurationCodableTests: XCTestCase {
schemaDocumentation: .include,
apqs: .disabled,
cocoapodsCompatibleImportStatements: false,
warningsOnDeprecatedUsage: .include
warningsOnDeprecatedUsage: .include,
conversionStrategies:.init(enumCases: .camelCase)
),
experimentalFeatures: .init(
clientControlledNullability: true,
Expand All @@ -58,7 +59,7 @@ class ApolloCodegenConfigurationCodableTests: XCTestCase {

static var encoded: String {
"""
{"experimentalFeatures" : {"clientControlledNullability" : true,"legacySafelistingCompatibleOperations" : true},"input" : {"operationSearchPaths" : ["/search/path/**/*.graphql"],"schemaSearchPaths" : ["/path/to/schema.graphqls"]},"options" : {"additionalInflectionRules" : [{"pluralization" : {"replacementRegex" : "animals","singularRegex" : "animal"}}],"apqs" : "disabled","cocoapodsCompatibleImportStatements" : false,"deprecatedEnumCases" : "exclude","queryStringLiteralFormat" : "multiline","schemaDocumentation" : "include","warningsOnDeprecatedUsage" : "include"},"output" : {"operations" : {"relative" : {"subpath" : "/relative/subpath"}},"schemaTypes" : {"moduleType" : {"swiftPackageManager" : {}},"path" : "/output/path"},"testMocks" : {"swiftPackage" : {"targetName" : "SchemaTestMocks"}}},"schemaName" : "SerializedSchema"}
{"experimentalFeatures" : {"clientControlledNullability" : true,"legacySafelistingCompatibleOperations" : true},"input" : {"operationSearchPaths" : ["/search/path/**/*.graphql"],"schemaSearchPaths" : ["/path/to/schema.graphqls"]},"options" : {"additionalInflectionRules" : [{"pluralization" : {"replacementRegex" : "animals","singularRegex" : "animal"}}],"apqs" : "disabled","cocoapodsCompatibleImportStatements" : false,"deprecatedEnumCases" : "exclude","queryStringLiteralFormat" : "multiline","schemaDocumentation" : "include","warningsOnDeprecatedUsage" : "include", "conversionStrategies" : {"enumCases" : "camelCase"}},"output" : {"operations" : {"relative" : {"subpath" : "/relative/subpath"}},"schemaTypes" : {"moduleType" : {"swiftPackageManager" : {}},"path" : "/output/path"},"testMocks" : {"swiftPackage" : {"targetName" : "SchemaTestMocks"}}},"schemaName" : "SerializedSchema"}
"""
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class EnumTemplateTests: XCTestCase {
expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true))
}

func test_render_givenSchemaEnum_generatesSwiftEnumRespectingValueCasing() throws {
func test_render_givenCaseConversionStrategy_camelCase_generatesSwiftEnum_convertedToCamelCase() {
// given
buildSubject(
name: "casedEnum",
Expand Down Expand Up @@ -173,6 +173,140 @@ class EnumTemplateTests: XCTestCase {
]
)

let expected = """
enum CasedEnum: String, EnumType {
case lower = "lower"
case upper = "UPPER"
case capitalized = "Capitalized"
case `associatedtype` = "associatedtype"
case `class` = "class"
case `deinit` = "deinit"
case `enum` = "enum"
case `extension` = "extension"
case `fileprivate` = "fileprivate"
case `func` = "func"
case `import` = "import"
case `init` = "init"
case `inout` = "inout"
case `internal` = "internal"
case `let` = "let"
case `operator` = "operator"
case `private` = "private"
case `precedencegroup` = "precedencegroup"
case `protocol` = "protocol"
case `protocol` = "Protocol"
case `public` = "public"
case `rethrows` = "rethrows"
case `static` = "static"
case `struct` = "struct"
case `subscript` = "subscript"
case `typealias` = "typealias"
case `var` = "var"
case `break` = "break"
case `case` = "case"
case `catch` = "catch"
case `continue` = "continue"
case `default` = "default"
case `defer` = "defer"
case `do` = "do"
case `else` = "else"
case `fallthrough` = "fallthrough"
case `guard` = "guard"
case `if` = "if"
case `in` = "in"
case `repeat` = "repeat"
case `return` = "return"
case `throw` = "throw"
case `switch` = "switch"
case `where` = "where"
case `while` = "while"
case `as` = "as"
case `false` = "false"
case `is` = "is"
case `nil` = "nil"
case `self` = "self"
case `self` = "Self"
case `super` = "super"
case `throws` = "throws"
case `true` = "true"
case `try` = "try"
}

"""

// when
let actual = renderSubject()

// then
expect(actual).to(equalLineByLine(expected))
}

func test_render_givenSchemaEnum_noneConveersionStrategies_generatesSwiftEnumRespectingValueCasing() throws {
// given
buildSubject(
name: "casedEnum",
values: [
("lower", nil, nil),
("UPPER", nil, nil),
("Capitalized", nil, nil),
("associatedtype", nil, nil),
("class", nil, nil),
("deinit", nil, nil),
("enum", nil, nil),
("extension", nil, nil),
("fileprivate", nil, nil),
("func", nil, nil),
("import", nil, nil),
("init", nil, nil),
("inout", nil, nil),
("internal", nil, nil),
("let", nil, nil),
("operator", nil, nil),
("private", nil, nil),
("precedencegroup", nil, nil),
("protocol", nil, nil),
("Protocol", nil, nil),
("public", nil, nil),
("rethrows", nil, nil),
("static", nil, nil),
("struct", nil, nil),
("subscript", nil, nil),
("typealias", nil, nil),
("var", nil, nil),
("break", nil, nil),
("case", nil, nil),
("catch", nil, nil),
("continue", nil, nil),
("default", nil, nil),
("defer", nil, nil),
("do", nil, nil),
("else", nil, nil),
("fallthrough", nil, nil),
("guard", nil, nil),
("if", nil, nil),
("in", nil, nil),
("repeat", nil, nil),
("return", nil, nil),
("throw", nil, nil),
("switch", nil, nil),
("where", nil, nil),
("while", nil, nil),
("as", nil, nil),
("false", nil, nil),
("is", nil, nil),
("nil", nil, nil),
("self", nil, nil),
("Self", nil, nil),
("super", nil, nil),
("throws", nil, nil),
("true", nil, nil),
("try", nil, nil),
],
config: .mock(
options: .init(conversionStrategies: .init(enumCases: .none))
)
)

let expected = """
enum CasedEnum: String, EnumType {
case lower
Expand Down Expand Up @@ -259,9 +393,9 @@ class EnumTemplateTests: XCTestCase {

let expected = """
enum TestEnum: String, EnumType {
case ONE
case TWO
case THREE
case one = "ONE"
case two = "TWO"
case three = "THREE"
}

"""
Expand Down Expand Up @@ -289,10 +423,10 @@ class EnumTemplateTests: XCTestCase {

let expected = """
enum TestEnum: String, EnumType {
case ONE
case one = "ONE"
@available(*, deprecated, message: "Deprecated for tests")
case TWO
case THREE
case two = "TWO"
case three = "THREE"
}

"""
Expand Down Expand Up @@ -320,7 +454,7 @@ class EnumTemplateTests: XCTestCase {

let expected = """
enum TestEnum: String, EnumType {
case TWO
case two = "TWO"
}

"""
Expand Down Expand Up @@ -348,7 +482,7 @@ class EnumTemplateTests: XCTestCase {

let expected = """
enum TestEnum: String, EnumType {
case TWO
case two = "TWO"
}

"""
Expand Down Expand Up @@ -384,11 +518,11 @@ class EnumTemplateTests: XCTestCase {
enum TestEnum: String, EnumType {
/// Doc: One
@available(*, deprecated, message: "Deprecated for tests")
case ONE
case one = "ONE"
/// Doc: Two
case TWO
case two = "TWO"
@available(*, deprecated, message: "Deprecated for tests")
case THREE
case three = "THREE"
}

"""
Expand Down Expand Up @@ -421,10 +555,10 @@ class EnumTemplateTests: XCTestCase {
/// \(documentation)
enum TestEnum: String, EnumType {
/// Doc: One
case ONE
case one = "ONE"
/// Doc: Two
case TWO
case THREE
case two = "TWO"
case three = "THREE"
}

"""
Expand Down Expand Up @@ -455,9 +589,9 @@ class EnumTemplateTests: XCTestCase {

let expected = """
enum TestEnum: String, EnumType {
case ONE
case TWO
case THREE
case one = "ONE"
case two = "TWO"
case three = "THREE"
}

"""
Expand Down
3 changes: 3 additions & 0 deletions Tests/CodegenCLITests/Commands/InitializeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ class InitializeTests: XCTestCase {
"additionalInflectionRules" : [

],
"conversionStrategies" : {
"enumCases" : "camelCase"
},
"warningsOnDeprecatedUsage" : "include",
"deprecatedEnumCases" : "include",
"schemaDocumentation" : "include",
Expand Down