diff --git a/Tests/YAMLTestSuiteConformanceTests.swift b/Tests/YAMLTestSuiteConformanceTests.swift index 936aa2d..426d18e 100644 --- a/Tests/YAMLTestSuiteConformanceTests.swift +++ b/Tests/YAMLTestSuiteConformanceTests.swift @@ -127,14 +127,38 @@ struct YAMLTestSuiteConformanceTests { } /// A JSON-comparable normalization: ints and floats both become a number, so a value-level - /// match reflects structure and resolved value the way `in.json` records them. - private indirect enum JSONNode: Equatable { + /// match reflects structure and resolved value the way `in.json` records them. `Decodable` so the + /// expected `in.json` is read with `JSONDecoder`, which distinguishes a JSON bool from a number + /// natively. The previous `JSONSerialization` path needed CoreFoundation (`CFGetTypeID`) for that, + /// which is Darwin-only and failed to compile on Linux and Windows. + private indirect enum JSONNode: Equatable, Decodable { case null case bool(Bool) case number(Double) case string(String) case array([JSONNode]) case object([String: JSONNode]) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + // Order matters: a JSON bool must be tried before a number, since `true`/`false` are a + // distinct JSON type the number decode would otherwise not see. + if container.decodeNil() { + self = .null + } else if let bool = try? container.decode(Bool.self) { + self = .bool(bool) + } else if let number = try? container.decode(Double.self) { + self = .number(number) + } else if let string = try? container.decode(String.self) { + self = .string(string) + } else if let array = try? container.decode([JSONNode].self) { + self = .array(array) + } else if let object = try? container.decode([String: JSONNode].self) { + self = .object(object) + } else { + throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "unrecognized JSON value")) + } + } } private func jsonNode(from value: PureYAML.Model.Value) -> JSONNode? { @@ -168,39 +192,13 @@ struct YAMLTestSuiteConformanceTests { } private func jsonNode(fromJSONText text: String) -> JSONNode? { - guard let data = text.data(using: .utf8), - let object = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) - else { - return nil - } - return jsonNode(fromJSONObject: object) - } - - private func jsonNode(fromJSONObject object: Any) -> JSONNode? { - switch object { - case is NSNull: - return .null - case let number as NSNumber: - return CFGetTypeID(number) == CFBooleanGetTypeID() ? .bool(number.boolValue) : .number(number.doubleValue) - case let text as String: - return .string(text) - case let array as [Any]: - var nodes: [JSONNode] = [] - for item in array { - guard let node = jsonNode(fromJSONObject: item) else { return nil } - nodes.append(node) - } - return .array(nodes) - case let dictionary as [String: Any]: - var result: [String: JSONNode] = [:] - for (key, value) in dictionary { - guard let node = jsonNode(fromJSONObject: value) else { return nil } - result[key] = node - } - return .object(result) - default: + guard let data = text.data(using: .utf8) else { return nil } + // The suite's `in.json` may be a top-level fragment (a bare string, number, or null), which + // `JSONDecoder` accepts in Swift 6. `JSONNode`'s decoder uses a single-value container, so it + // reads fragments and containers alike, cross-platform and without CoreFoundation. + return try? JSONDecoder().decode(JSONNode.self, from: data) } private func percent(_ part: Int, _ whole: Int) -> String {