Add structured validation diagnostics, JSON and Yams comparison gates, and lazy token/event parsing so production behavior is measured without retaining full intermediate parser arrays.
225 lines
6.9 KiB
Bash
Executable File
225 lines
6.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
WORK_DIR="$ROOT_DIR/.build/json-compatibility"
|
|
ARTIFACT_DIR="$ROOT_DIR/.build/pureyaml-artifacts/json"
|
|
|
|
mkdir -p "$WORK_DIR/Sources/JSONCompatibility" "$ARTIFACT_DIR"
|
|
|
|
cat > "$WORK_DIR/Package.swift" <<EOF
|
|
// swift-tools-version: 6.1
|
|
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "PureYAMLJSONCompatibility",
|
|
platforms: [
|
|
.macOS(.v13),
|
|
],
|
|
dependencies: [
|
|
.package(path: "$ROOT_DIR"),
|
|
],
|
|
targets: [
|
|
.executableTarget(
|
|
name: "JSONCompatibility",
|
|
dependencies: [
|
|
"PureYAML",
|
|
],
|
|
),
|
|
],
|
|
)
|
|
EOF
|
|
|
|
cat > "$WORK_DIR/Sources/JSONCompatibility/main.swift" <<'EOF'
|
|
import Foundation
|
|
import PureYAML
|
|
|
|
struct ValidCase {
|
|
var name: String
|
|
var json: String
|
|
}
|
|
|
|
struct InvalidCase {
|
|
var name: String
|
|
var json: String
|
|
}
|
|
|
|
let arguments = CommandLine.arguments
|
|
guard arguments.count == 2 else {
|
|
FileHandle.standardError.write(Data("usage: JSONCompatibility <artifact-json>\n".utf8))
|
|
Foundation.exit(64)
|
|
}
|
|
|
|
let validCases = [
|
|
ValidCase(
|
|
name: "compact object",
|
|
json: #"{"openapi":"3.0.0","info":{"title":"JSON API","version":"1"},"paths":{},"enabled":true,"count":3}"#,
|
|
),
|
|
ValidCase(name: "array root", json: #"[{"a":1},{"b":null},{"items":[true,false]}]"#),
|
|
ValidCase(name: "string root", json: #""hello""#),
|
|
ValidCase(name: "integer root", json: #"123"#),
|
|
ValidCase(name: "boolean root", json: #"true"#),
|
|
ValidCase(name: "null root", json: #"null"#),
|
|
ValidCase(
|
|
name: "string escapes",
|
|
json: #"{"quote":"\"","slash":"\\","solidus":"\/","backspace":"\b","formfeed":"\f","newline":"line\nnext","return":"a\rb","tab":"a\tb"}"#,
|
|
),
|
|
ValidCase(name: "unicode escapes", json: #"{"bmp":"\u263A","surrogate":"\uD834\uDD1E"}"#),
|
|
ValidCase(name: "number forms", json: #"{"zero":0,"negative":-3,"fraction":1.25,"small":1e-6,"big":6.02E23}"#),
|
|
ValidCase(
|
|
name: "insignificant whitespace",
|
|
json: """
|
|
{
|
|
"items" : [
|
|
{ "url" : "https://api.example.com/a:b" },
|
|
{ "window" : "10:30" }
|
|
]
|
|
}
|
|
""",
|
|
),
|
|
ValidCase(name: "empty containers", json: #"{"object":{},"array":[]}"#),
|
|
]
|
|
|
|
let invalidCases = [
|
|
InvalidCase(name: "missing value", json: #"{"a":}"#),
|
|
InvalidCase(name: "double comma", json: #"[1,,2]"#),
|
|
InvalidCase(name: "unquoted key", json: #"{a:1}"#),
|
|
InvalidCase(name: "comment", json: #"{"a":1 // no comments in JSON"#),
|
|
InvalidCase(name: "single quoted string", json: #"{'a':1}"#),
|
|
]
|
|
|
|
let validResults = validCases.map { testCase -> [String: Any] in
|
|
do {
|
|
let json = try JSONOracle.decode(testCase.json)
|
|
let yaml = try JSONOracle(pureYAML: PureYAML.parse(testCase.json))
|
|
return [
|
|
"name": testCase.name,
|
|
"acceptedByJSONOracle": true,
|
|
"acceptedByPureYAML": true,
|
|
"structurallyEqual": yaml == json,
|
|
]
|
|
} catch {
|
|
return [
|
|
"name": testCase.name,
|
|
"acceptedByJSONOracle": false,
|
|
"acceptedByPureYAML": false,
|
|
"structurallyEqual": false,
|
|
"error": String(describing: error),
|
|
]
|
|
}
|
|
}
|
|
|
|
let invalidResults = invalidCases.map { testCase -> [String: Any] in
|
|
do {
|
|
_ = try JSONOracle.decode(testCase.json)
|
|
return [
|
|
"name": testCase.name,
|
|
"rejectedByJSONOracle": false,
|
|
]
|
|
} catch {
|
|
return [
|
|
"name": testCase.name,
|
|
"rejectedByJSONOracle": true,
|
|
]
|
|
}
|
|
}
|
|
|
|
let validFailures = validResults.filter { result in
|
|
result["structurallyEqual"] as? Bool != true
|
|
}
|
|
let invalidFailures = invalidResults.filter { result in
|
|
result["rejectedByJSONOracle"] as? Bool != true
|
|
}
|
|
let report: [String: Any] = [
|
|
"summary": [
|
|
"validCases": validCases.count,
|
|
"validCasesStructurallyEqual": validCases.count - validFailures.count,
|
|
"invalidCases": invalidCases.count,
|
|
"invalidCasesRejectedByOracle": invalidCases.count - invalidFailures.count,
|
|
"failures": validFailures.count + invalidFailures.count,
|
|
],
|
|
"valid": validResults,
|
|
"invalid": invalidResults,
|
|
]
|
|
|
|
let artifactURL = URL(fileURLWithPath: arguments[1])
|
|
let data = try JSONSerialization.data(withJSONObject: report, options: [.prettyPrinted, .sortedKeys])
|
|
try data.write(to: artifactURL)
|
|
|
|
print("JSON compatibility valid cases: \(validCases.count)")
|
|
print("JSON compatibility structurally equal: \(validCases.count - validFailures.count)")
|
|
print("JSON invalid oracle rejections: \(invalidCases.count - invalidFailures.count)")
|
|
print("Report: \(artifactURL.path)")
|
|
|
|
if !validFailures.isEmpty || !invalidFailures.isEmpty {
|
|
Foundation.exit(1)
|
|
}
|
|
|
|
enum JSONOracle: Equatable, Decodable {
|
|
case null
|
|
case bool(Bool)
|
|
case int(Int)
|
|
case double(Double)
|
|
case string(String)
|
|
case array([JSONOracle])
|
|
case object([String: JSONOracle])
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.singleValueContainer()
|
|
if container.decodeNil() {
|
|
self = .null
|
|
} else if let value = try? container.decode(Bool.self) {
|
|
self = .bool(value)
|
|
} else if let value = try? container.decode(Int.self) {
|
|
self = .int(value)
|
|
} else if let value = try? container.decode(Double.self) {
|
|
self = .double(value)
|
|
} else if let value = try? container.decode(String.self) {
|
|
self = .string(value)
|
|
} else if let value = try? container.decode([JSONOracle].self) {
|
|
self = .array(value)
|
|
} else {
|
|
self = .object(try container.decode([String: JSONOracle].self))
|
|
}
|
|
}
|
|
|
|
init(pureYAML value: PureYAML.Model.Value) throws {
|
|
switch value {
|
|
case .null:
|
|
self = .null
|
|
case let .bool(value):
|
|
self = .bool(value)
|
|
case let .int(value):
|
|
self = .int(value)
|
|
case let .double(value):
|
|
self = .double(value)
|
|
case let .string(value):
|
|
self = .string(value)
|
|
case let .sequence(values):
|
|
self = .array(try values.map(JSONOracle.init(pureYAML:)))
|
|
case let .mapping(mapping):
|
|
self = .object(try mapping.pairs.reduce(into: [:]) { result, pair in
|
|
guard case let .string(key) = pair.keyNode.value else {
|
|
throw JSONOracleFailure.nonStringKey
|
|
}
|
|
result[key] = try JSONOracle(pureYAML: pair.value)
|
|
})
|
|
}
|
|
}
|
|
|
|
static func decode(_ json: String) throws -> JSONOracle {
|
|
try JSONDecoder().decode(JSONOracle.self, from: Data(json.utf8))
|
|
}
|
|
}
|
|
|
|
enum JSONOracleFailure: Error {
|
|
case nonStringKey
|
|
}
|
|
EOF
|
|
|
|
swift run \
|
|
--package-path "$WORK_DIR" \
|
|
JSONCompatibility \
|
|
"$ARTIFACT_DIR/json-compatibility.json"
|