diff --git a/Plugins/InstallCLI/InstallCLIPluginCommand.swift b/Plugins/InstallCLI/InstallCLIPluginCommand.swift index 5a6207390c..6d536940b5 100644 --- a/Plugins/InstallCLI/InstallCLIPluginCommand.swift +++ b/Plugins/InstallCLI/InstallCLIPluginCommand.swift @@ -13,7 +13,7 @@ struct InstallCLIPluginCommand: CommandPlugin { let task = Process() task.standardInput = nil task.environment = ProcessInfo.processInfo.environment - task.arguments = ["-c", "ln -f -s \(from.string) \(to.string)"] + task.arguments = ["-c", "ln -f -s '\(from.string)' '\(to.string)'"] task.executableURL = URL(fileURLWithPath: "/bin/zsh") try task.run() task.waitUntilExit() diff --git a/Sources/ApolloCodegenLib/ApolloCodegen.swift b/Sources/ApolloCodegenLib/ApolloCodegen.swift index ed19d15ce7..6853a42ae3 100644 --- a/Sources/ApolloCodegenLib/ApolloCodegen.swift +++ b/Sources/ApolloCodegenLib/ApolloCodegen.swift @@ -19,6 +19,7 @@ public class ApolloCodegen { case cannotLoadSchema case cannotLoadOperations case invalidConfiguration(message: String) + case invalidSchemaName(_ name: String, message: String) public var errorDescription: String? { switch self { @@ -47,6 +48,8 @@ public class ApolloCodegen { return "No GraphQL operations could be found. Please verify the operation search paths." case let .invalidConfiguration(message): return "The codegen configuration has conflicting values: \(message)" + case let .invalidSchemaName(name, message): + return "The schema name `\(name)` is invalid: \(message)" } } } @@ -76,14 +79,14 @@ public class ApolloCodegen { rootURL: rootURL ) - try validate(config: configContext) + try validate(configContext) let compilationResult = try compileGraphQLResult( configContext, experimentalFeatures: configuration.experimentalFeatures ) - try validate(schemaName: configContext.schemaName, compilationResult: compilationResult) + try validate(configContext, with: compilationResult) let ir = IR(compilationResult: compilationResult) @@ -130,15 +133,39 @@ public class ApolloCodegen { } } - /// Performs validation against deterministic errors that will cause code generation to fail. - static func validate(config: ConfigurationContext) throws { - if case .swiftPackage = config.output.testMocks, - config.output.schemaTypes.moduleType != .swiftPackageManager { + /// Validates the configuration against deterministic errors that will cause code generation to + /// fail. This validation step does not take into account schema and operation specific types, it + /// is only a static analysis of the configuration. + /// + /// - Parameter config: Code generation configuration settings. + public static func _validate(config: ApolloCodegenConfiguration) throws { + try validate(ConfigurationContext(config: config)) + } + + static private func validate(_ context: ConfigurationContext) throws { + guard + !context.schemaName.isEmpty, + !context.schemaName.contains(where: { $0.isWhitespace }) + else { + throw Error.invalidSchemaName(context.schemaName, message: """ + Cannot be empty nor contain spaces. If your schema name has spaces consider replacing them \ + with the underscore character. + """) + } + + guard + !SwiftKeywords.DisallowedSchemaNamespaceNames.contains(context.schemaName.lowercased()) + else { + throw Error.schemaNameConflict(name: context.schemaName) + } + + if case .swiftPackage = context.output.testMocks, + context.output.schemaTypes.moduleType != .swiftPackageManager { throw Error.testMocksInvalidSwiftPackageConfiguration } - if case .swiftPackageManager = config.output.schemaTypes.moduleType, - config.options.cocoapodsCompatibleImportStatements == true { + if case .swiftPackageManager = context.output.schemaTypes.moduleType, + context.options.cocoapodsCompatibleImportStatements == true { throw Error.invalidConfiguration(message: """ cocoapodsCompatibleImportStatements cannot be set to 'true' when the output schema types \ module type is Swift Package Manager. Change the cocoapodsCompatibleImportStatements \ @@ -146,10 +173,10 @@ public class ApolloCodegen { """) } - for searchPath in config.input.schemaSearchPaths { + for searchPath in context.input.schemaSearchPaths { try validate(inputSearchPath: searchPath) } - for searchPath in config.input.operationSearchPaths { + for searchPath in context.input.operationSearchPaths { try validate(inputSearchPath: searchPath) } } @@ -160,17 +187,18 @@ public class ApolloCodegen { } } - static func validate(schemaName: String, compilationResult: CompilationResult) throws { + /// Validates the configuration context against the GraphQL compilation result, checking for + /// configuration errors that are dependent on the schema and operations. + static func validate(_ context: ConfigurationContext, with compilationResult: CompilationResult) throws { guard - !SwiftKeywords.DisallowedSchemaNamespaceNames.contains(schemaName.lowercased()), !compilationResult.referencedTypes.contains(where: { namedType in - namedType.swiftName == schemaName.firstUppercased + namedType.swiftName == context.schemaName.firstUppercased }), !compilationResult.fragments.contains(where: { fragmentDefinition in - fragmentDefinition.name == schemaName.firstUppercased + fragmentDefinition.name == context.schemaName.firstUppercased }) else { - throw Error.schemaNameConflict(name: schemaName) + throw Error.schemaNameConflict(name: context.schemaName) } } diff --git a/Sources/CodegenCLI/Commands/Initialize.swift b/Sources/CodegenCLI/Commands/Initialize.swift index ebfc551f75..8017feb9c2 100644 --- a/Sources/CodegenCLI/Commands/Initialize.swift +++ b/Sources/CodegenCLI/Commands/Initialize.swift @@ -64,10 +64,6 @@ public struct Initialize: ParsableCommand { public init() { } public func validate() throws { - guard !schemaName.trimmingCharacters(in: .whitespaces).isEmpty else { - throw ValidationError("--schema-name value cannot be empty.") - } - switch (moduleType, targetName?.isEmpty) { case (.embeddedInTarget, nil), (.embeddedInTarget, true): throw ValidationError(""" @@ -86,11 +82,11 @@ public struct Initialize: ParsableCommand { func _run(fileManager: ApolloFileManager = .default, output: OutputClosure? = nil) throws { let encoded = try ApolloCodegenConfiguration - .minimalJSON( - schemaName: schemaName, - moduleType: moduleType, - targetName: targetName - ).asData() + .minimalJSON(schemaName: schemaName, moduleType: moduleType, targetName: targetName) + .asData() + + let decoded = try JSONDecoder().decode(ApolloCodegenConfiguration.self, from: encoded) + try ApolloCodegen._validate(config: decoded) if print { try print(data: encoded, output: output) diff --git a/Tests/ApolloCodegenTests/ApolloCodegenTests.swift b/Tests/ApolloCodegenTests/ApolloCodegenTests.swift index 234b62f3a0..4e51153f64 100644 --- a/Tests/ApolloCodegenTests/ApolloCodegenTests.swift +++ b/Tests/ApolloCodegenTests/ApolloCodegenTests.swift @@ -1931,86 +1931,86 @@ class ApolloCodegenTests: XCTestCase { func test_validation_givenTestMockConfiguration_asSwiftPackage_withSchemaTypesModule_asEmbeddedInTarget_shouldThrow() throws { // given - let configContext = ApolloCodegen.ConfigurationContext(config: .mock( + let config = ApolloCodegenConfiguration.mock( input: .init(schemaPath: "path"), output: .mock( moduleType: .embeddedInTarget(name: "ModuleTarget"), testMocks: .swiftPackage(targetName: nil) ) - ), rootURL: nil) + ) // then - expect(try ApolloCodegen.validate(config: configContext)) + expect(try ApolloCodegen._validate(config: config)) .to(throwError(ApolloCodegen.Error.testMocksInvalidSwiftPackageConfiguration)) } func test_validation_givenTestMockConfiguration_asSwiftPackage_withSchemaTypesModule_asOther_shouldThrow() throws { // given - let configContext = ApolloCodegen.ConfigurationContext(config: .mock( + let config = ApolloCodegenConfiguration.mock( input: .init(schemaPath: "path"), output: .mock( moduleType: .other, testMocks: .swiftPackage(targetName: nil) ) - ), rootURL: nil) + ) // then - expect(try ApolloCodegen.validate(config: configContext)) + expect(try ApolloCodegen._validate(config: config)) .to(throwError(ApolloCodegen.Error.testMocksInvalidSwiftPackageConfiguration)) } func test_validation_givenTestMockConfiguration_asSwiftPackage_withSchemaTypesModule_asSwiftPackage_shouldNotThrow() throws { // given - let configContext = ApolloCodegen.ConfigurationContext(config: .mock( + let config = ApolloCodegenConfiguration.mock( input: .init(schemaPath: "path.graphqls") - ), rootURL: nil) + ) // then - expect(try ApolloCodegen.validate(config: configContext)) + expect(try ApolloCodegen._validate(config: config)) .notTo(throwError()) } func test_validation_givenOperationSearchPathWithoutFileExtensionComponent_shouldThrow() throws { // given - let configContext = ApolloCodegen.ConfigurationContext(config: .mock( + let config = ApolloCodegenConfiguration.mock( input: .init(schemaPath: "path.graphqls", operationSearchPaths: ["operations/*"]) - ), rootURL: nil) + ) // then - expect(try ApolloCodegen.validate(config: configContext)) + expect(try ApolloCodegen._validate(config: config)) .to(throwError(ApolloCodegen.Error.inputSearchPathInvalid(path: "operations/*"))) } func test_validation_givenOperationSearchPathEndingInPeriod_shouldThrow() throws { // given - let configContext = ApolloCodegen.ConfigurationContext(config: .mock( + let config = ApolloCodegenConfiguration.mock( input: .init(schemaPath: "path.graphqls", operationSearchPaths: ["operations/*."]) - ), rootURL: nil) + ) // then - expect(try ApolloCodegen.validate(config: configContext)) + expect(try ApolloCodegen._validate(config: config)) .to(throwError(ApolloCodegen.Error.inputSearchPathInvalid(path: "operations/*."))) } func test_validation_givenSchemaSearchPathWithoutFileExtensionComponent_shouldThrow() throws { // given - let configContext = ApolloCodegen.ConfigurationContext(config: .mock( + let config = ApolloCodegenConfiguration.mock( input: .init(schemaSearchPaths: ["schema/*"]) - ), rootURL: nil) + ) // then - expect(try ApolloCodegen.validate(config: configContext)) + expect(try ApolloCodegen._validate(config: config)) .to(throwError(ApolloCodegen.Error.inputSearchPathInvalid(path: "schema/*"))) } func test_validation_givenSchemaSearchPathEndingInPeriod_shouldThrow() throws { // given - let configContext = ApolloCodegen.ConfigurationContext(config: .mock( + let config = ApolloCodegenConfiguration.mock( input: .init(schemaSearchPaths: ["schema/*."]) - ), rootURL: nil) + ) // then - expect(try ApolloCodegen.validate(config: configContext)) + expect(try ApolloCodegen._validate(config: config)) .to(throwError(ApolloCodegen.Error.inputSearchPathInvalid(path: "schema/*."))) } @@ -2029,9 +2029,7 @@ class ApolloCodegenTests: XCTestCase { schemaName: name ), rootURL: nil) - expect(try ApolloCodegen.validate( - schemaName: configContext.schemaName, - compilationResult: compilationResult)) + expect(try ApolloCodegen.validate(configContext, with: compilationResult)) .to(throwError(ApolloCodegen.Error.schemaNameConflict(name: configContext.schemaName))) } } @@ -2049,9 +2047,7 @@ class ApolloCodegenTests: XCTestCase { schemaName: name ), rootURL: nil) - expect(try ApolloCodegen.validate( - schemaName: configContext.schemaName, - compilationResult: compilationResult)) + expect(try ApolloCodegen.validate(configContext, with: compilationResult)) .to(throwError(ApolloCodegen.Error.schemaNameConflict(name: configContext.schemaName))) } } @@ -2069,9 +2065,7 @@ class ApolloCodegenTests: XCTestCase { schemaName: name ), rootURL: nil) - expect(try ApolloCodegen.validate( - schemaName: configContext.schemaName, - compilationResult: compilationResult)) + expect(try ApolloCodegen.validate(configContext, with: compilationResult)) .to(throwError(ApolloCodegen.Error.schemaNameConflict(name: configContext.schemaName))) } } @@ -2089,9 +2083,7 @@ class ApolloCodegenTests: XCTestCase { schemaName: name ), rootURL: nil) - expect(try ApolloCodegen.validate( - schemaName: configContext.schemaName, - compilationResult: compilationResult)) + expect(try ApolloCodegen.validate(configContext, with: compilationResult)) .to(throwError(ApolloCodegen.Error.schemaNameConflict(name: configContext.schemaName))) } } @@ -2109,9 +2101,7 @@ class ApolloCodegenTests: XCTestCase { schemaName: name ), rootURL: nil) - expect(try ApolloCodegen.validate( - schemaName: configContext.schemaName, - compilationResult: compilationResult)) + expect(try ApolloCodegen.validate(configContext, with: compilationResult)) .to(throwError(ApolloCodegen.Error.schemaNameConflict(name: configContext.schemaName))) } } @@ -2129,9 +2119,7 @@ class ApolloCodegenTests: XCTestCase { schemaName: name ), rootURL: nil) - expect(try ApolloCodegen.validate( - schemaName: configContext.schemaName, - compilationResult: compilationResult)) + expect(try ApolloCodegen.validate(configContext, with: compilationResult)) .to(throwError(ApolloCodegen.Error.schemaNameConflict(name: configContext.schemaName))) } } @@ -2151,9 +2139,7 @@ class ApolloCodegenTests: XCTestCase { schemaName: name ), rootURL: nil) - expect(try ApolloCodegen.validate( - schemaName: configContext.schemaName, - compilationResult: compilationResult)) + expect(try ApolloCodegen.validate(configContext, with: compilationResult)) .to(throwError(ApolloCodegen.Error.schemaNameConflict(name: configContext.schemaName))) } } @@ -2164,18 +2150,38 @@ class ApolloCodegenTests: XCTestCase { // when for name in disallowedNames { - let configContext = ApolloCodegen.ConfigurationContext(config: .mock( - schemaName: name - ), rootURL: nil) + let config = ApolloCodegenConfiguration.mock(schemaName: name) // then - expect(try ApolloCodegen.validate( - schemaName: configContext.schemaName, - compilationResult: CompilationResult.mock())) - .to(throwError(ApolloCodegen.Error.schemaNameConflict(name: configContext.schemaName))) + expect(try ApolloCodegen._validate(config: config)) + .to(throwError(ApolloCodegen.Error.schemaNameConflict(name: config.schemaName))) } } + func test__validation__givenEmptySchemaName_shouldThrow() throws { + let config = ApolloCodegenConfiguration.mock(schemaName: "") + + // then + expect(try ApolloCodegen._validate(config: config)) + .to(throwError(ApolloCodegen.Error.invalidSchemaName("", message: ""))) + } + + func test__validation__givenWhitespaceOnlySchemaName_shouldThrow() throws { + let config = ApolloCodegenConfiguration.mock(schemaName: " ") + + // then + expect(try ApolloCodegen._validate(config: config)) + .to(throwError(ApolloCodegen.Error.invalidSchemaName(" ", message: ""))) + } + + func test__validation__givenSchemaNameContainingWhitespace_shouldThrow() throws { + let config = ApolloCodegenConfiguration.mock(schemaName: "My Schema") + + // then + expect(try ApolloCodegen._validate(config: config)) + .to(throwError(ApolloCodegen.Error.invalidSchemaName("My Schema", message: ""))) + } + func test__validation__givenUniqueSchemaName_shouldNotThrow() throws { // given let object = GraphQLObjectType.mock("MockObject") @@ -2204,21 +2210,19 @@ class ApolloCodegenTests: XCTestCase { schemaName: "MySchema" ), rootURL: nil) - expect(try ApolloCodegen.validate( - schemaName: configContext.schemaName, - compilationResult: compilationResult)) + expect(try ApolloCodegen.validate(configContext, with: compilationResult)) .notTo(throwError()) } func test__validation__givenSchemaTypesModule_swiftPackageManager_withCocoapodsCompatibleImportStatements_true_shouldThrow() throws { // given - let configContext = ApolloCodegen.ConfigurationContext(config: .mock( + let config = ApolloCodegenConfiguration.mock( .swiftPackageManager, options: .init(cocoapodsCompatibleImportStatements: true) - )) + ) // then - expect(try ApolloCodegen.validate(config: configContext)) + expect(try ApolloCodegen._validate(config: config)) .to(throwError(ApolloCodegen.Error.invalidConfiguration(message: """ cocoapodsCompatibleImportStatements cannot be set to 'true' when the output schema types \ module type is Swift Package Manager. Change the cocoapodsCompatibleImportStatements \ @@ -2228,35 +2232,35 @@ class ApolloCodegenTests: XCTestCase { func test__validation__givenSchemaTypesModule_swiftPackageManager_withCocoapodsCompatibleImportStatements_false_shouldNotThrow() throws { // given - let configContext = ApolloCodegen.ConfigurationContext(config: .mock( + let config = ApolloCodegenConfiguration.mock( .swiftPackageManager, options: .init(cocoapodsCompatibleImportStatements: false) - )) + ) // then - expect(try ApolloCodegen.validate(config: configContext)).notTo(throwError()) + expect(try ApolloCodegen._validate(config: config)).notTo(throwError()) } func test__validation__givenSchemaTypesModule_embeddedInTarget_withCocoapodsCompatibleImportStatements_true_shouldNotThrow() throws { // given - let configContext = ApolloCodegen.ConfigurationContext(config: .mock( + let config = ApolloCodegenConfiguration.mock( .embeddedInTarget(name: "TestTarget"), options: .init(cocoapodsCompatibleImportStatements: true) - )) + ) // then - expect(try ApolloCodegen.validate(config: configContext)).notTo(throwError()) + expect(try ApolloCodegen._validate(config: config)).notTo(throwError()) } func test__validation__givenSchemaTypesModule_other_withCocoapodsCompatibleImportStatements_true_shouldNotThrow() throws { // given - let configContext = ApolloCodegen.ConfigurationContext(config: .mock( + let config = ApolloCodegenConfiguration.mock( .other, options: .init(cocoapodsCompatibleImportStatements: true) - )) + ) // then - expect(try ApolloCodegen.validate(config: configContext)).notTo(throwError()) + expect(try ApolloCodegen._validate(config: config)).notTo(throwError()) } // MARK: Path Match Exclusion Tests diff --git a/Tests/CodegenCLITests/Commands/InitializeTests.swift b/Tests/CodegenCLITests/Commands/InitializeTests.swift index 2fd4efd8fb..8cde73ef17 100644 --- a/Tests/CodegenCLITests/Commands/InitializeTests.swift +++ b/Tests/CodegenCLITests/Commands/InitializeTests.swift @@ -44,17 +44,30 @@ class InitializeTests: XCTestCase { // MARK: - Validation Tests - func test__validation__givenWhitespaceSchemaName_shouldThrowValidationError() throws { + func test__validation__givenWhitespaceOnlySchemaName_shouldThrowError() throws { // given let options = [ "--schema-name= ", "--module-type=swiftPackageManager", ] + let subject = try self.parse(options) + // then - expect { try self.parse(options) }.to(throwUserValidationError( - ValidationError("--schema-name value cannot be empty.") - )) + expect(try subject._run()).to(throwError()) + } + + func test__validation__givenSchemaNameContainingWhitespace_shouldThrowError() throws { + // given + let options = [ + "--schema-name=\"My Schema\"", + "--module-type=swiftPackageManager", + ] + + let subject = try self.parse(options) + + // then + expect(try subject._run()).to(throwError()) } func test__validation__givenModuleType_embeddedInTarget_withNoTargetName_shouldThrowValidationError() throws {