|
1 | 1 | import Foundation |
2 | 2 |
|
3 | | -// MARK: - Welcome |
| 3 | +typealias JSONObject = [String: Any] |
4 | 4 |
|
5 | | -public struct TestPlanModel: Codable { |
6 | | - public var configurations: [Configuration] |
7 | | - public var defaultOptions: DefaultOptions |
| 5 | +public struct TestPlanModel { |
| 6 | + private var rawJSON: JSONObject |
8 | 7 | public var testTargets: [TestTarget] |
9 | | - public var version: Int |
10 | | -} |
11 | | - |
12 | | -// MARK: - Configuration |
13 | | - |
14 | | -public struct Configuration: Codable { |
15 | | - public var id, name: String |
16 | | - public var options: Options |
| 8 | + |
| 9 | + public init(data: Data) throws { |
| 10 | + let object = try JSONSerialization.jsonObject(with: data, options: []) |
| 11 | + guard let dictionary = object as? JSONObject else { |
| 12 | + throw TestPlanModelError.invalidFormat |
| 13 | + } |
| 14 | + |
| 15 | + self.rawJSON = dictionary |
| 16 | + self.testTargets = try Self.decodeTargets(from: dictionary["testTargets"]) |
| 17 | + } |
| 18 | + |
| 19 | + func encodedData() throws -> Data { |
| 20 | + var json = rawJSON |
| 21 | + json["testTargets"] = try Self.encodeTargets(testTargets) |
| 22 | + return try JSONSerialization.data(withJSONObject: json, |
| 23 | + options: [.prettyPrinted, .sortedKeys]) |
| 24 | + } |
| 25 | + |
| 26 | + private static func decodeTargets(from value: Any?) throws -> [TestTarget] { |
| 27 | + guard let value, !(value is NSNull) else { return [] } |
| 28 | + guard let array = value as? [Any] else { |
| 29 | + throw TestPlanModelError.invalidTargets |
| 30 | + } |
| 31 | + |
| 32 | + return try array.map { element in |
| 33 | + guard let dictionary = element as? JSONObject else { |
| 34 | + throw TestPlanModelError.invalidTargets |
| 35 | + } |
| 36 | + return try TestTarget(json: dictionary) |
| 37 | + } |
| 38 | + } |
| 39 | + |
| 40 | + private static func encodeTargets(_ targets: [TestTarget]) throws -> [Any] { |
| 41 | + return try targets.map { try $0.encodeJSON() } |
| 42 | + } |
17 | 43 | } |
18 | 44 |
|
19 | | -// MARK: - Options |
20 | | - |
21 | | -public struct Options: Codable { |
22 | | - public var targetForVariableExpansion: Target? |
| 45 | +public enum TestPlanModelError: Error { |
| 46 | + case invalidFormat |
| 47 | + case invalidTargets |
| 48 | + case invalidTargetObject |
23 | 49 | } |
24 | 50 |
|
25 | 51 | // MARK: - Target |
26 | 52 |
|
27 | | -public struct Target: Codable { |
28 | | - public var containerPath, identifier, name: String |
29 | | -} |
30 | | - |
31 | | -// MARK: - DefaultOptions |
32 | | - |
33 | | -public struct DefaultOptions: Codable { |
34 | | - public var commandLineArgumentEntries: [CommandLineArgumentEntry]? |
35 | | - public var environmentVariableEntries: [EnvironmentVariableEntry]? |
36 | | - public var language: String? |
37 | | - public var region: String? |
38 | | - public var locationScenario: LocationScenario? |
39 | | - public var testTimeoutsEnabled: Bool? |
40 | | - public var testRepetitionMode: String? |
41 | | - public var maximumTestRepetitions: Int? |
42 | | - public var maximumTestExecutionTimeAllowance: Int? |
43 | | - public var targetForVariableExpansion: Target? |
44 | | -} |
45 | | - |
46 | | -// MARK: - CommandLineArgumentEntry |
47 | | - |
48 | | -public struct CommandLineArgumentEntry: Codable { |
49 | | - public let argument: String |
50 | | - public let enabled: Bool? |
51 | | -} |
52 | | - |
53 | | -// MARK: - EnvironmentVariableEntry |
54 | | - |
55 | | -public struct EnvironmentVariableEntry: Codable { |
56 | | - public var key, value: String |
57 | | - public let enabled: Bool? |
58 | | -} |
59 | | - |
60 | | -// MARK: - LocationScenario |
61 | | - |
62 | | -public struct LocationScenario: Codable { |
| 53 | +public struct Target { |
| 54 | + private var rawJSON: JSONObject |
| 55 | + public var containerPath: String |
63 | 56 | public var identifier: String |
| 57 | + public var name: String |
| 58 | + |
| 59 | + init(json: JSONObject) throws { |
| 60 | + guard let containerPath = json["containerPath"] as? String, |
| 61 | + let identifier = json["identifier"] as? String, |
| 62 | + let name = json["name"] as? String else { |
| 63 | + throw TestPlanModelError.invalidTargetObject |
| 64 | + } |
| 65 | + |
| 66 | + self.rawJSON = json |
| 67 | + self.containerPath = containerPath |
| 68 | + self.identifier = identifier |
| 69 | + self.name = name |
| 70 | + } |
| 71 | + |
| 72 | + func encodeJSON() -> JSONObject { |
| 73 | + var json = rawJSON |
| 74 | + json["containerPath"] = containerPath |
| 75 | + json["identifier"] = identifier |
| 76 | + json["name"] = name |
| 77 | + return json |
| 78 | + } |
64 | 79 | } |
65 | 80 |
|
66 | 81 | // MARK: - TestTarget |
67 | 82 |
|
68 | | -public struct TestTarget: Codable { |
| 83 | +public struct TestTarget { |
| 84 | + private var rawJSON: JSONObject |
69 | 85 | public var parallelizable: Bool? |
70 | 86 | public var skippedTests: Tests? |
71 | 87 | public var selectedTests: Tests? |
72 | 88 | public var target: Target |
73 | 89 | public var enabled: Bool? |
| 90 | + |
| 91 | + init(json: JSONObject) throws { |
| 92 | + guard let targetJSON = json["target"] as? JSONObject else { |
| 93 | + throw TestPlanModelError.invalidTargetObject |
| 94 | + } |
| 95 | + self.rawJSON = json |
| 96 | + self.parallelizable = json["parallelizable"] as? Bool |
| 97 | + self.enabled = json["enabled"] as? Bool |
| 98 | + self.target = try Target(json: targetJSON) |
| 99 | + self.skippedTests = try Tests.fromJSONValue(json["skippedTests"]) |
| 100 | + self.selectedTests = try Tests.fromJSONValue(json["selectedTests"]) |
| 101 | + } |
| 102 | + |
| 103 | + func encodeJSON() throws -> JSONObject { |
| 104 | + var json = rawJSON |
| 105 | + json.setJSONValue(parallelizable, forKey: "parallelizable") |
| 106 | + json.setJSONValue(enabled, forKey: "enabled") |
| 107 | + json.setJSONValue(try skippedTests?.jsonValue(), forKey: "skippedTests") |
| 108 | + json.setJSONValue(try selectedTests?.jsonValue(), forKey: "selectedTests") |
| 109 | + json["target"] = target.encodeJSON() |
| 110 | + return json |
| 111 | + } |
74 | 112 | } |
75 | 113 |
|
76 | 114 | public enum Tests: Codable { |
77 | | - case array([String]) |
78 | | - case dictionary(Suites) |
79 | | - |
80 | | - public struct Suites: Codable { |
81 | | - let suites: [Suite] |
82 | | - |
83 | | - public struct Suite: Codable { |
84 | | - let name: String |
| 115 | + case array([String]) |
| 116 | + case dictionary(Suites) |
| 117 | + |
| 118 | + public struct Suites: Codable { |
| 119 | + let suites: [Suite] |
| 120 | + |
| 121 | + public struct Suite: Codable { |
| 122 | + let name: String |
| 123 | + } |
85 | 124 | } |
86 | | - } |
87 | | - |
88 | | - public init(from decoder: Decoder) throws { |
89 | | - let container = try decoder.singleValueContainer() |
90 | | - if let array = try? container.decode([String].self) { |
91 | | - self = .array(array) |
92 | | - return |
| 125 | + |
| 126 | + public init(from decoder: Decoder) throws { |
| 127 | + let container = try decoder.singleValueContainer() |
| 128 | + if let array = try? container.decode([String].self) { |
| 129 | + self = .array(array) |
| 130 | + return |
| 131 | + } |
| 132 | + |
| 133 | + if let dict = try? container.decode(Suites.self) { |
| 134 | + self = .dictionary(dict) |
| 135 | + return |
| 136 | + } |
| 137 | + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid type for skippedTests") |
| 138 | + } |
| 139 | + |
| 140 | + public func encode(to encoder: Encoder) throws { |
| 141 | + var container = encoder.singleValueContainer() |
| 142 | + switch self { |
| 143 | + case .array(let array): |
| 144 | + try container.encode(array) |
| 145 | + case .dictionary(let dict): |
| 146 | + try container.encode(dict) |
| 147 | + } |
93 | 148 | } |
| 149 | +} |
94 | 150 |
|
95 | | - if let dict = try? container.decode(Suites.self) { |
96 | | - self = .dictionary(dict) |
97 | | - return |
| 151 | +extension Tests { |
| 152 | + static func fromJSONValue(_ value: Any?) throws -> Tests? { |
| 153 | + guard let value, !(value is NSNull) else { return nil } |
| 154 | + let data = try JSONSerialization.data(withJSONObject: value) |
| 155 | + let decoder = JSONDecoder() |
| 156 | + return try decoder.decode(Tests.self, from: data) |
| 157 | + } |
| 158 | + |
| 159 | + func jsonValue() throws -> Any { |
| 160 | + let encoder = JSONEncoder() |
| 161 | + let data = try encoder.encode(self) |
| 162 | + return try JSONSerialization.jsonObject(with: data, options: []) |
98 | 163 | } |
99 | | - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid type for skippedTests") |
100 | | - } |
| 164 | +} |
101 | 165 |
|
102 | | - public func encode(to encoder: Encoder) throws { |
103 | | - var container = encoder.singleValueContainer() |
104 | | - switch self { |
105 | | - case .array(let array): |
106 | | - try container.encode(array) |
107 | | - case .dictionary(let dict): |
108 | | - try container.encode(dict) |
| 166 | +extension Dictionary where Key == String, Value == Any { |
| 167 | + mutating func setJSONValue(_ value: Any?, forKey key: String) { |
| 168 | + if let value { |
| 169 | + self[key] = value |
| 170 | + } else { |
| 171 | + self.removeValue(forKey: key) |
| 172 | + } |
109 | 173 | } |
110 | | - } |
111 | 174 | } |
0 commit comments