diff --git a/Sources/ProjectSpec/Config.swift b/Sources/ProjectSpec/Config.swift index 2b4edf9b..474894f4 100644 --- a/Sources/ProjectSpec/Config.swift +++ b/Sources/ProjectSpec/Config.swift @@ -22,6 +22,8 @@ public struct Config: Equatable { public static func ==(lhs: Config, rhs: Config) -> Bool { return lhs.name == rhs.name && lhs.type == rhs.type } + + public static var defaultConfigs: [Config] = [Config(name: "Debug", type: .debug), Config(name: "Release", type: .release)] } public enum ConfigType: String { diff --git a/Sources/ProjectSpec/ProjectSpec.swift b/Sources/ProjectSpec/ProjectSpec.swift index 7789f9fe..5fb3e319 100644 --- a/Sources/ProjectSpec/ProjectSpec.swift +++ b/Sources/ProjectSpec/ProjectSpec.swift @@ -57,7 +57,7 @@ public struct ProjectSpec { } } - public init(basePath: Path, name: String, configs: [Config] = [], targets: [Target] = [], settings: Settings = .empty, settingGroups: [String: Settings] = [:], schemes: [Scheme] = [], options: Options = Options(), fileGroups: [String] = [], configFiles: [String: String] = [:], attributes: [String: Any] = [:]) { + public init(basePath: Path, name: String, configs: [Config] = Config.defaultConfigs, targets: [Target] = [], settings: Settings = .empty, settingGroups: [String: Settings] = [:], schemes: [Scheme] = [], options: Options = Options(), fileGroups: [String] = [], configFiles: [String: String] = [:], attributes: [String: Any] = [:]) { self.basePath = basePath self.name = name self.targets = targets @@ -135,7 +135,7 @@ extension ProjectSpec { settings = jsonDictionary.json(atKeyPath: "settings") ?? .empty settingGroups = jsonDictionary.json(atKeyPath: "settingGroups") ?? jsonDictionary.json(atKeyPath: "settingPresets") ?? [:] let configs: [String: String] = jsonDictionary.json(atKeyPath: "configs") ?? [:] - self.configs = configs.map { Config(name: $0, type: ConfigType(rawValue: $1)) }.sorted { $0.name < $1.name } + self.configs = configs.isEmpty ? Config.defaultConfigs : configs.map { Config(name: $0, type: ConfigType(rawValue: $1)) }.sorted { $0.name < $1.name } targets = try jsonDictionary.json(atKeyPath: "targets").sorted { $0.name < $1.name } schemes = try jsonDictionary.json(atKeyPath: "schemes") fileGroups = jsonDictionary.json(atKeyPath: "fileGroups") ?? [] diff --git a/Sources/XcodeGenKit/SpecValidation.swift b/Sources/ProjectSpec/ProjectSpecValidation.swift similarity index 85% rename from Sources/XcodeGenKit/SpecValidation.swift rename to Sources/ProjectSpec/ProjectSpecValidation.swift index d2841cdc..84cbd726 100644 --- a/Sources/XcodeGenKit/SpecValidation.swift +++ b/Sources/ProjectSpec/ProjectSpecValidation.swift @@ -6,21 +6,16 @@ // import Foundation -import ProjectSpec import PathKit extension ProjectSpec { - public mutating func validate() throws { + public func validate() throws { - if configs.isEmpty { - configs = [Config(name: "Debug", type: .debug), Config(name: "Release", type: .release)] - } + var errors: [SpecValidationError.ValidationError] = [] - var errors: [SpecValidationError.Error] = [] - - func validateSettings(_ settings: Settings) -> [SpecValidationError.Error] { - var errors: [SpecValidationError.Error] = [] + func validateSettings(_ settings: Settings) -> [SpecValidationError.ValidationError] { + var errors: [SpecValidationError.ValidationError] = [] for group in settings.groups { if let settings = settingGroups[group] { errors += validateSettings(settings) @@ -30,12 +25,14 @@ extension ProjectSpec { } for config in settings.configSettings.keys { if !configs.contains(where: { $0.name.lowercased().contains(config.lowercased())}) { - errors.append(.invalidConfigReference(config)) + errors.append(.invalidBuildSettingConfig(config)) } } return errors } + errors += validateSettings(settings) + for fileGroup in fileGroups { if !(basePath + fileGroup).exists { errors.append(.invalidFileGroup(fileGroup)) @@ -46,6 +43,9 @@ extension ProjectSpec { if !(basePath + configFile).exists { errors.append(.invalidConfigFile(configFile: configFile, config: config)) } + if getConfig(config) == nil { + errors.append(.invalidConfigFileConfig(config)) + } } for settings in settingGroups.values { @@ -61,13 +61,10 @@ extension ProjectSpec { for (config, configFile) in target.configFiles { if !(basePath + configFile).exists { - errors.append(.invalidTargetConfigFile(configFile: configFile, config: config, target: target.name)) + errors.append(.invalidTargetConfigFile(target: target.name, configFile: configFile, config: config)) } - } - - for config in target.settings.configSettings.keys { if getConfig(config) == nil { - errors.append(.invalidBuildSettingConfig(config)) + errors.append(.invalidConfigFileConfig(config)) } } @@ -110,7 +107,7 @@ extension ProjectSpec { if case let .path(pathString) = script.script { let scriptPath = basePath + pathString if !scriptPath.exists { - errors.append(.invalidBuildScriptPath(target: target.name, path: pathString)) + errors.append(.invalidBuildScriptPath(target: target.name, name: script.name, path: pathString)) } } } @@ -149,39 +146,39 @@ extension ProjectSpec { public struct SpecValidationError: Error, CustomStringConvertible { - public var errors: [Error] + public var errors: [ValidationError] - public enum Error: CustomStringConvertible { + public enum ValidationError: Error, CustomStringConvertible { case invalidTargetDependency(target: String, dependency: String) + case missingTargetSource(target: String, source: String) + case invalidTargetConfigFile(target: String, configFile: String, config: String) + case invalidTargetSchemeConfigVariant(target: String, configVariant: String, configType: ConfigType) + case invalidTargetSchemeTest(target: String, testTarget: String) case invalidSchemeTarget(scheme: String, target: String) case invalidSchemeConfig(scheme: String, config: String) case invalidConfigFile(configFile: String, config: String) - case invalidTargetConfigFile(configFile: String, config: String, target: String) case invalidBuildSettingConfig(String) case invalidSettingsGroup(String) - case missingTargetSource(target: String, source: String) - case invalidBuildScriptPath(target: String, path: String) - case invalidTargetSchemeConfigVariant(target: String, configVariant: String, configType: ConfigType) - case invalidTargetSchemeTest(target: String, testTarget: String) + case invalidBuildScriptPath(target: String, name: String?, path: String) case invalidFileGroup(String) - case invalidConfigReference(String) + case invalidConfigFileConfig(String) case missingConfigTypeForGeneratedTargetScheme(target: String, configType: ConfigType) public var description: String { switch self { case let .invalidTargetDependency(target, dependency): return "Target \(target.quoted) has invalid dependency: \(dependency.quoted)" - case let .invalidTargetConfigFile(configFile, config, target): return "Target \(target.quoted) has invalid config file \(configFile.quoted) for config \(config.quoted)" + case let .invalidTargetConfigFile(target, configFile, config): return "Target \(target.quoted) has invalid config file \(configFile.quoted) for config \(config.quoted)" + case let .missingTargetSource(target, source): return "Target \(target.quoted) has a missing source directory \(source.quoted)" + case let .invalidTargetSchemeConfigVariant(target, configVariant, configType): return "Target \(target.quoted) has an invalid scheme config variant which requires a config that has a \(configType.rawValue.quoted) type and contains the name \(configVariant.quoted)" + case let .invalidTargetSchemeTest(target, test): return "Target \(target.quoted) scheme has invalid test \(test.quoted)" case let .invalidConfigFile(configFile, config): return "Invalid config file \(configFile.quoted) for config \(config.quoted)" case let .invalidSchemeTarget(scheme, target): return "Scheme \(scheme.quoted) has invalid build target \(target.quoted)" case let .invalidSchemeConfig(scheme, config): return "Scheme \(scheme.quoted) has invalid build configuration \(config.quoted)" case let .invalidBuildSettingConfig(config): return "Build setting has invalid build configuration \(config.quoted)" - case let .missingTargetSource(target, source): return "Target \(target.quoted) has a missing source directory \(source.quoted)" case let .invalidSettingsGroup(group): return "Invalid settings group \(group.quoted)" - case let .invalidBuildScriptPath(target, path): return "Target \(target.quoted) has a script path that doesn't exist \(path.quoted)" - case let .invalidTargetSchemeConfigVariant(target, configVariant, configType): return "Target \(target.quoted) has an invalid scheme config variant which requires a config that has a \(configType.rawValue.quoted) type and contains the name \(configVariant.quoted)" - case let .invalidTargetSchemeTest(target, test): return "Target \(target.quoted) scheme has invalid test \(test.quoted)" + case let .invalidBuildScriptPath(target, name, path): return "Target \(target.quoted) has a script \(name != nil ? "\(name!.quoted) which has a " : "")path that doesn't exist \(path.quoted)" case let .invalidFileGroup(group): return "Invalid file group \(group.quoted)" - case let .invalidConfigReference(config): return "Invalid config reference \(config.quoted)" + case let .invalidConfigFileConfig(config): return "Config file has invalid config \(config.quoted)" case let .missingConfigTypeForGeneratedTargetScheme(target, configType): return "Target \(target.quoted) is missing a config of type \(configType.rawValue) to generate its scheme" } } diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index d0277b98..d5667faa 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -135,8 +135,8 @@ extension Target: Equatable { } public struct TargetScheme { - public let testTargets: [String] - public let configVariants: [String] + public var testTargets: [String] + public var configVariants: [String] public init(testTargets: [String] = [], configVariants: [String] = []) { self.testTargets = testTargets diff --git a/Sources/XcodeGenKit/ProjectGenerator.swift b/Sources/XcodeGenKit/ProjectGenerator.swift index 1670e7a8..bf647892 100644 --- a/Sources/XcodeGenKit/ProjectGenerator.swift +++ b/Sources/XcodeGenKit/ProjectGenerator.swift @@ -15,7 +15,7 @@ import ProjectSpec public class ProjectGenerator { - var spec: ProjectSpec + let spec: ProjectSpec let currentXcodeVersion = "0900" public init(spec: ProjectSpec) { diff --git a/Tests/XcodeGenKitTests/ProjectSpecTests.swift b/Tests/XcodeGenKitTests/ProjectSpecTests.swift index ae5ec8ad..dc16db9e 100644 --- a/Tests/XcodeGenKitTests/ProjectSpecTests.swift +++ b/Tests/XcodeGenKitTests/ProjectSpecTests.swift @@ -24,5 +24,86 @@ func projectSpecTests() { try expect(dynamicLibrary.type.isLibrary).to.beTrue() } } + + func expectValidationError(_ spec: ProjectSpec, _ expectedError: SpecValidationError.ValidationError) throws { + do { + try spec.validate() + } catch let error as SpecValidationError { + if !error.errors.contains( where: { $0.description == expectedError.description }) { + throw failure("Supposed to fail with:\n\(expectedError)\nbut got:\n\(error.errors.map { $0.description }.joined(separator: "\n"))") + } + return + } catch { + throw failure("Supposed to fail with \"\(expectedError)\"") + } + throw failure("Supposed to fail with \"\(expectedError)\"") + } + + $0.describe("Validation") { + + let baseSpec = ProjectSpec(basePath: "", name: "", configs: [Config(name: "invalid")]) + let invalidSettings = Settings(configSettings: ["invalidConfig": [:]], + groups: ["invalidSettingGroup"]) + $0.it("fails with invalid project") { + var spec = baseSpec + spec.settings = invalidSettings + spec.configFiles = ["invalidConfig": "invalidConfigFile"] + spec.fileGroups = ["invalidFileGroup"] + spec.settingGroups = ["settingGroup1": Settings(configSettings: ["invalidSettingGroupConfig": [:]], + groups: ["invalidSettingGroupSettingGroup"])] + + try expectValidationError(spec, .invalidConfigFileConfig("invalidConfig")) + try expectValidationError(spec, .invalidBuildSettingConfig("invalidConfig")) + try expectValidationError(spec, .invalidConfigFile(configFile: "invalidConfigFile", config: "invalidConfig")) + try expectValidationError(spec, .invalidSettingsGroup("invalidSettingGroup")) + try expectValidationError(spec, .invalidFileGroup("invalidFileGroup")) + try expectValidationError(spec, .invalidSettingsGroup("invalidSettingGroupSettingGroup")) + try expectValidationError(spec, .invalidBuildSettingConfig("invalidSettingGroupConfig")) + } + + $0.it("fails with invalid target") { + var spec = baseSpec + spec.targets = [Target(name: "target1", + type: .application, + platform: .iOS, + settings: invalidSettings, + configFiles: ["invalidConfig": "invalidConfigFile"], + sources: ["invalidSource"], + dependencies: [Dependency(type: .target, reference: "invalidDependency")], + prebuildScripts: [BuildScript(script: .path("invalidPrebuildScript"), name: "prebuildScript1", inputFiles: + [], outputFiles: [])], + postbuildScripts: [BuildScript(script: .path("invalidPostbuildScript"), inputFiles: + [], outputFiles: [])], + scheme: TargetScheme(testTargets: ["invalidTarget"]) + )] + + try expectValidationError(spec, .invalidTargetDependency(target: "target1", dependency: "invalidDependency")) + try expectValidationError(spec, .invalidTargetConfigFile(target: "target1", configFile: "invalidConfigFile", config: "invalidConfig")) + try expectValidationError(spec, .invalidTargetSchemeTest(target: "target1", testTarget: "invalidTarget")) + try expectValidationError(spec, .missingTargetSource(target: "target1", source: "invalidSource")) + try expectValidationError(spec, .invalidBuildSettingConfig("invalidConfig")) + try expectValidationError(spec, .invalidSettingsGroup("invalidSettingGroup")) + try expectValidationError(spec, .invalidBuildScriptPath(target:"target1", name: "prebuildScript1", path: "invalidPrebuildScript")) + try expectValidationError(spec, .invalidBuildScriptPath(target:"target1", name: nil, path: "invalidPostbuildScript")) + + try expectValidationError(spec, .missingConfigTypeForGeneratedTargetScheme(target:"target1", configType: .debug)) + try expectValidationError(spec, .missingConfigTypeForGeneratedTargetScheme(target:"target1", configType: .release)) + + spec.targets[0].scheme?.configVariants = ["invalidVariant"] + try expectValidationError(spec, .invalidTargetSchemeConfigVariant(target: "target1", configVariant: "invalidVariant", configType: .debug)) + } + + $0.it("fails with invalid scheme") { + var spec = baseSpec + spec.schemes = [Scheme(name: "scheme1", + targets: [Scheme.BuildTarget(target: "invalidTarget")], + debugConfig: "debugInvalid", + releaseConfig: "releaseInvalid")] + + try expectValidationError(spec, .invalidSchemeTarget(scheme: "scheme1", target: "invalidTarget")) + try expectValidationError(spec, .invalidSchemeConfig(scheme: "scheme1", config: "debugInvalid")) + try expectValidationError(spec, .invalidSchemeConfig(scheme: "scheme1", config: "releaseInvalid")) + } + } } } diff --git a/Tests/XcodeGenKitTests/SpecLoadingTests.swift b/Tests/XcodeGenKitTests/SpecLoadingTests.swift index 60279410..f2827e26 100644 --- a/Tests/XcodeGenKitTests/SpecLoadingTests.swift +++ b/Tests/XcodeGenKitTests/SpecLoadingTests.swift @@ -48,7 +48,7 @@ func specLoadingTests() { } } - describe("Project Spec") { + describe("Project Spec Parser") { $0.it("fails with incorrect platform") { var target = validTarget