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 } }