diff --git a/Sources/ProjectSpec/ProjectSpec.swift b/Sources/ProjectSpec/ProjectSpec.swift new file mode 100644 index 00000000..b0566fec --- /dev/null +++ b/Sources/ProjectSpec/ProjectSpec.swift @@ -0,0 +1,90 @@ +import Foundation +import typealias JSONUtilities.JSONDictionary +import PathKit + +extension Project { + + public struct Spec { + public let relativePath: Path + public let jsonDictionary: JSONDictionary + public let subSpecs: [Spec] + + public init(relativePath: Path, jsonDictionary: JSONDictionary, subSpecs: [Spec] = []) { + self.relativePath = relativePath + self.jsonDictionary = jsonDictionary + self.subSpecs = subSpecs + } + + public init(filename: String, basePath: Path, relativePath: Path = "") throws { + let path = basePath + relativePath + filename + + // Depending on the extension we will either load the file as YAML or JSON + var json: [String: Any] + if path.extension?.lowercased() == "json" { + let data: Data = try path.read() + let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments) + guard let jsonDictionary = jsonData as? [String: Any] else { + fatalError("Invalid JSON at path \(path)") + } + json = jsonDictionary + } else { + json = try loadYamlDictionary(path: path) + } + + var includeStrings: [String] + if let includeString = json["include"] as? String { + includeStrings = [includeString] + } else if let includeArray = json["include"] as? [String] { + includeStrings = includeArray + } else { + includeStrings = [] + } + + let includes = try includeStrings.map { include -> Spec in + let path = Path(include) + return try Spec(filename: path.lastComponent, basePath: basePath + relativePath, relativePath: path.parent()) + } + + self.relativePath = relativePath + self.jsonDictionary = json + self.subSpecs = includes + } + + public func resolvedDictionary() -> JSONDictionary { + return jsonDictionary.merged(onto: + subSpecs + .map { $0.resolvedDictionary() } + .reduce([:]) { $1.merged(onto: $0) } + ) + } + } +} + +internal protocol SubSequenceConvertible: Sequence { + + init(_ subsequence: SubSequence) +} + +extension String: SubSequenceConvertible { } +extension Substring: SubSequenceConvertible { } + +extension Dictionary where Key: StringProtocol, Key: SubSequenceConvertible, Value: Any { + + func merged(onto other: Dictionary) -> Dictionary { + var merged = other + + for (key, value) in self { + if key.hasSuffix(":REPLACE") { + let newKey = key[key.startIndex ..< key.index(key.endIndex, offsetBy: -8)] + merged[Key(newKey)] = value + } else if let dictionary = value as? Dictionary, let base = merged[key] as? Dictionary { + merged[key] = dictionary.merged(onto: base) as? Value + } else if let array = value as? [Any], let base = merged[key] as? [Any] { + merged[key] = (base + array) as? Value + } else { + merged[key] = value + } + } + return merged + } +}