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

Throw error when an invalid key is present in a JSON Apollo configuration #3125

Merged
merged 9 commits into from
Jul 19, 2023
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
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")
}
}
}