forked from yonaskolb/XcodeGen
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSpecFile.swift
More file actions
182 lines (152 loc) · 6.82 KB
/
SpecFile.swift
File metadata and controls
182 lines (152 loc) · 6.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import Foundation
import JSONUtilities
import PathKit
public struct SpecFile {
public let basePath: Path
public let relativePath: Path
public let jsonDictionary: JSONDictionary
public let subSpecs: [SpecFile]
private let filename: String
fileprivate struct Include {
let path: Path
let relativePaths: Bool
static let defaultRelativePaths = true
init?(any: Any) {
if let string = any as? String {
path = Path(string)
relativePaths = Include.defaultRelativePaths
} else if let dictionary = any as? JSONDictionary,
let path = dictionary["path"] as? String {
self.path = Path(path)
relativePaths = dictionary["relativePaths"] as? Bool ?? Include.defaultRelativePaths
} else {
return nil
}
}
static func parse(json: Any?) -> [Include] {
if let array = json as? [Any] {
return array.compactMap(Include.init)
} else if let object = json, let include = Include(any: object) {
return [include]
} else {
return []
}
}
}
public init(path: Path) throws {
try self.init(filename: path.lastComponent, basePath: path.parent())
}
public init(filename: String, jsonDictionary: JSONDictionary, basePath: Path = "", relativePath: Path = "", subSpecs: [SpecFile] = []) {
self.basePath = basePath
self.relativePath = relativePath
self.jsonDictionary = jsonDictionary
self.subSpecs = subSpecs
self.filename = filename
}
fileprivate init(include: Include, basePath: Path, relativePath: Path) throws {
let basePath = include.relativePaths ? (basePath + relativePath) : (basePath + relativePath + include.path.parent())
let relativePath = include.relativePaths ? include.path.parent() : Path()
try self.init(filename: include.path.lastComponent, basePath: basePath, relativePath: relativePath)
}
fileprivate init(filename: String, basePath: Path, relativePath: Path = "") throws {
let path = basePath + relativePath + filename
let jsonDictionary = try SpecFile.loadDictionary(path: path)
let includes = Include.parse(json: jsonDictionary["include"])
let subSpecs: [SpecFile] = try includes.map { include in
try SpecFile(include: include, basePath: basePath, relativePath: relativePath)
}
self.init(filename: filename, jsonDictionary: jsonDictionary, basePath: basePath, relativePath: relativePath, subSpecs: subSpecs)
}
static func loadDictionary(path: Path) throws -> JSONDictionary {
// Depending on the extension we will either load the file as YAML or JSON
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)")
}
return jsonDictionary
} else {
return try loadYamlDictionary(path: path)
}
}
public func resolvedDictionary(variables: [String: String] = [:]) -> JSONDictionary {
let resolvedDictionary = resolvedDictionaryWithUniqueTargets()
return substitute(variables: variables, in: resolvedDictionary)
}
private func resolvedDictionaryWithUniqueTargets() -> JSONDictionary {
let resolvedSpec = resolvingPaths()
var value = Set<String>()
return resolvedSpec.mergedDictionary(set: &value)
}
private func substitute(variables: [String: String], in mergedDictionary: JSONDictionary) -> JSONDictionary {
var resolvedSpec = mergedDictionary
for (key, value) in variables {
resolvedSpec = resolvedSpec.replaceString("${\(key)}", with: value)
}
return resolvedSpec
}
func mergedDictionary(set mergedTargets: inout Set<String>) -> JSONDictionary {
let name = (basePath + relativePath + Path(filename)).description
guard !mergedTargets.contains(name) else { return [:] }
mergedTargets.insert(name)
return jsonDictionary.merged(onto:
subSpecs
.map { $0.mergedDictionary(set: &mergedTargets) }
.reduce([:]) { $1.merged(onto: $0) })
}
func resolvingPaths(relativeTo basePath: Path = Path()) -> SpecFile {
let relativePath = (basePath + self.relativePath).normalize()
guard relativePath != Path() else {
return self
}
let jsonDictionary = Project.pathProperties.resolvingPaths(in: self.jsonDictionary, relativeTo: relativePath)
return SpecFile(
filename: filename,
jsonDictionary: jsonDictionary,
relativePath: self.relativePath,
subSpecs: subSpecs.map { $0.resolvingPaths(relativeTo: relativePath) }
)
}
}
extension Dictionary where Key == String, Value: Any {
func merged(onto other: [Key: Value]) -> [Key: Value] {
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? [Key: Value], let base = merged[key] as? [Key: Value] {
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
}
func replaceString(_ template: String, with replacement: String) -> JSONDictionary {
var replaced: JSONDictionary = self
for (key, value) in self {
replaced[key] = replace(value: value, template, with: replacement)
}
return replaced
}
func replace(value: Any, _ template: String, with replacement: String) -> Any {
switch value {
case let dictionary as JSONDictionary:
return dictionary.replaceString(template, with: replacement)
case let string as String:
return string.replacingOccurrences(of: template, with: replacement)
case let array as [JSONDictionary]:
return array.map { $0.replaceString(template, with: replacement) }
case let array as [String]:
return array.map { $0.replacingOccurrences(of: template, with: replacement) }
case let anyArray as [Any]:
return anyArray.map { self.replace(value: $0, template, with: replacement) }
default:
return value
}
}
}