Skip to content

Commit

Permalink
Throw error when an invalid key is present in a JSON Apollo configu…
Browse files Browse the repository at this point in the history
…ration (#3125)

Co-authored-by: Calvin Cestari <[email protected]>
  • Loading branch information
Iron-Ham and calvincestari authored Jul 19, 2023
1 parent 9961d2b commit c338ee9
Show file tree
Hide file tree
Showing 2 changed files with 242 additions and 6 deletions.
56 changes: 50 additions & 6 deletions Sources/ApolloCodegenLib/ApolloCodegenConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {

// MARK: Codable

enum CodingKeys: CodingKey {
enum CodingKeys: CodingKey, CaseIterable {
case schemaTypes
case operations
case testMocks
Expand All @@ -214,7 +214,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {
/// specified defaults when not present.
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)

try throwIfContainsUnexpectedKey(container: values, type: Self.self, decoder: decoder)
schemaTypes = try values.decode(
SchemaTypesFileOutput.self,
forKey: .schemaTypes
Expand Down Expand Up @@ -617,7 +617,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {

// MARK: Codable

enum CodingKeys: CodingKey {
enum CodingKeys: CodingKey, CaseIterable {
case additionalInflectionRules
case queryStringLiteralFormat
case deprecatedEnumCases
Expand All @@ -633,6 +633,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {

public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
try throwIfContainsUnexpectedKey(container: values, type: Self.self, decoder: decoder)

additionalInflectionRules = try values.decodeIfPresent(
[InflectionRule].self,
Expand Down Expand Up @@ -757,6 +758,13 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {

public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
guard values.allKeys.first != nil else {
throw DecodingError.typeMismatch(Self.self, DecodingError.Context.init(
codingPath: values.codingPath,
debugDescription: "Invalid number of keys found, expected one.",
underlyingError: nil
))
}

enumCases = try values.decodeIfPresent(
CaseConversionStrategy.self,
Expand Down Expand Up @@ -931,7 +939,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {

// MARK: Codable

public enum CodingKeys: CodingKey {
public enum CodingKeys: CodingKey, CaseIterable {
case clientControlledNullability
case legacySafelistingCompatibleOperations
}
Expand Down Expand Up @@ -1008,7 +1016,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {

// MARK: Codable

enum CodingKeys: CodingKey {
enum CodingKeys: CodingKey, CaseIterable {
case schemaName
case schemaNamespace
case input
Expand All @@ -1034,6 +1042,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable {

public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
try throwIfContainsUnexpectedKey(container: values, type: Self.self, decoder: decoder)

func getSchemaNamespaceValue() throws -> String {
if let value = try values.decodeIfPresent(String.self, forKey: .schemaNamespace) {
Expand Down Expand Up @@ -1152,7 +1161,7 @@ extension ApolloCodegenConfiguration.SelectionSetInitializers {

// MARK: Codable

enum CodingKeys: CodingKey {
enum CodingKeys: CodingKey, CaseIterable {
case operations
case namedFragments
case localCacheMutations
Expand All @@ -1161,6 +1170,7 @@ extension ApolloCodegenConfiguration.SelectionSetInitializers {

public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
try throwIfContainsUnexpectedKey(container: values, type: Self.self, decoder: decoder)
var options: Options = []

func decode(option: @autoclosure () -> Options, forKey key: CodingKeys) throws {
Expand Down Expand Up @@ -1370,3 +1380,37 @@ extension ApolloCodegenConfiguration.OutputOptions {
}
}
}

private struct AnyCodingKey: CodingKey {
var stringValue: String

init?(stringValue: String) {
self.stringValue = stringValue
}

var intValue: Int?

init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
}

private func throwIfContainsUnexpectedKey<T, C: CodingKey & CaseIterable>(
container: KeyedDecodingContainer<C>,
type: T.Type,
decoder: Decoder
) throws {
// Map all keys from the input object
let allKeys = Set(try decoder.container(keyedBy: AnyCodingKey.self).allKeys.map(\.stringValue))
// Map all valid keys from the given `CodingKey` enum
let validKeys = Set(C.allCases.map(\.stringValue))
guard allKeys.isSubset(of: validKeys) else {
let invalidKeys = allKeys.subtracting(validKeys).sorted()
throw DecodingError.typeMismatch(type, DecodingError.Context.init(
codingPath: container.codingPath,
debugDescription: "Unrecognized \(invalidKeys.count > 1 ? "keys" : "key") found: \(invalidKeys.joined(separator: ", "))",
underlyingError: nil
))
}
}
192 changes: 192 additions & 0 deletions Tests/ApolloCodegenTests/ApolloCodegenConfigurationCodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -923,4 +923,196 @@ class ApolloCodegenConfigurationCodableTests: XCTestCase {
))
)
}

func test__decodeApolloCodegenConfiguration__withInvalidFileOutput() throws {
// given
let subject = """
{
"schemaName": "MySchema",
"input": {
"operationSearchPaths": ["/search/path/**/*.graphql"],
"schemaSearchPaths": ["/path/to/schema.graphqls"]
},
"output": {
"testMocks": {
"none": {}
},
"schemaTypes": {
"path": "./MySchema",
"moduleType": {
"swiftPackageManager": {}
}
},
"operations": {
"inSchemaModule": {}
},
"options": {
"selectionSetInitializers" : {
"operations": true,
"namedFragments": true,
"localCacheMutations" : true
},
"queryStringLiteralFormat": "multiline",
"schemaDocumentation": "include",
"apqs": "disabled",
"warningsOnDeprecatedUsage": "include"
}
}
}
""".asData

func decodeConfiguration(subject: Data) throws -> ApolloCodegenConfiguration {
try JSONDecoder().decode(ApolloCodegenConfiguration.self, from: subject)
}
XCTAssertThrowsError(try decodeConfiguration(subject: subject)) { error in
guard case let DecodingError.typeMismatch(type, context) = error else { return fail("Incorrect error type") }
XCTAssertEqual("\(type)", String(describing: ApolloCodegenConfiguration.FileOutput.self))
XCTAssertEqual(context.debugDescription, "Unrecognized key found: options")
}
}

func test__decodeApolloCodegenConfiguration__withInvalidOptions() throws {
// given
let subject = """
{
"schemaName": "MySchema",
"input": {
"operationSearchPaths": ["/search/path/**/*.graphql"],
"schemaSearchPaths": ["/path/to/schema.graphqls"]
},
"output": {
"testMocks": {
"none": {}
},
"schemaTypes": {
"path": "./MySchema",
"moduleType": {
"swiftPackageManager": {}
}
},
"operations": {
"inSchemaModule": {}
}
},
"options": {
"secret_feature": "flappy_bird",
"selectionSetInitializers" : {
"operations": true,
"namedFragments": true,
"localCacheMutations" : true
},
"queryStringLiteralFormat": "multiline",
"schemaDocumentation": "include",
"apqs": "disabled",
"warningsOnDeprecatedUsage": "include"
}
}
""".asData

func decodeConfiguration(subject: Data) throws -> ApolloCodegenConfiguration {
try JSONDecoder().decode(ApolloCodegenConfiguration.self, from: subject)
}
XCTAssertThrowsError(try decodeConfiguration(subject: subject)) { error in
guard case let DecodingError.typeMismatch(type, context) = error else { return fail("Incorrect error type") }
XCTAssertEqual("\(type)", String(describing: ApolloCodegenConfiguration.OutputOptions.self))
XCTAssertEqual(context.debugDescription, "Unrecognized key found: secret_feature")
}
}

func test__decodeApolloCodegenConfiguration__withInvalidBaseConfiguration() throws {
// given
let subject = """
{
"contact_info": "42 Wallaby Way, Sydney",
"schemaName": "MySchema",
"input": {
"operationSearchPaths": ["/search/path/**/*.graphql"],
"schemaSearchPaths": ["/path/to/schema.graphqls"]
},
"output": {
"testMocks": {
"none": {}
},
"schemaTypes": {
"path": "./MySchema",
"moduleType": {
"swiftPackageManager": {}
}
},
"operations": {
"inSchemaModule": {}
}
},
"options": {
"selectionSetInitializers" : {
"operations": true,
"namedFragments": true,
"localCacheMutations" : true
},
"queryStringLiteralFormat": "multiline",
"schemaDocumentation": "include",
"apqs": "disabled",
"warningsOnDeprecatedUsage": "include"
}
}
""".asData

func decodeConfiguration(subject: Data) throws -> ApolloCodegenConfiguration {
try JSONDecoder().decode(ApolloCodegenConfiguration.self, from: subject)
}
XCTAssertThrowsError(try decodeConfiguration(subject: subject)) { error in
guard case let DecodingError.typeMismatch(type, context) = error else { return fail("Incorrect error type") }
XCTAssertEqual("\(type)", String(describing: ApolloCodegenConfiguration.self))
XCTAssertEqual(context.debugDescription, "Unrecognized key found: contact_info")
}
}

func test__decodeApolloCodegenConfiguration__withInvalidBaseConfiguration_multipleErrors() throws {
// given
let subject = """
{
"contact_info": "42 Wallaby Way, Sydney",
"motto": "Just keep swimming",
"schemaName": "MySchema",
"input": {
"operationSearchPaths": ["/search/path/**/*.graphql"],
"schemaSearchPaths": ["/path/to/schema.graphqls"]
},
"output": {
"testMocks": {
"none": {}
},
"schemaTypes": {
"path": "./MySchema",
"moduleType": {
"swiftPackageManager": {}
}
},
"operations": {
"inSchemaModule": {}
}
},
"options": {
"selectionSetInitializers" : {
"operations": true,
"namedFragments": true,
"localCacheMutations" : true
},
"queryStringLiteralFormat": "multiline",
"schemaDocumentation": "include",
"apqs": "disabled",
"warningsOnDeprecatedUsage": "include"
}
}
""".asData

func decodeConfiguration(subject: Data) throws -> ApolloCodegenConfiguration {
try JSONDecoder().decode(ApolloCodegenConfiguration.self, from: subject)
}
XCTAssertThrowsError(try decodeConfiguration(subject: subject)) { error in
guard case let DecodingError.typeMismatch(type, context) = error else { return fail("Incorrect error type") }
XCTAssertEqual("\(type)", String(describing: ApolloCodegenConfiguration.self))
XCTAssertEqual(context.debugDescription, "Unrecognized keys found: contact_info, motto")
}
}
}

0 comments on commit c338ee9

Please sign in to comment.