import Foundation
@testable import IrdAPI
/// A small JSON-schema checker for the live conformance sweep. Typed decoding proves
/// the model reads what the server sends, but `Codable` ignores unknown keys; since
/// every object in the spec is `additionalProperties: false`, this reports the fields
/// the server returns that the schema never mentions, plus type, enum, const and
/// required-key drift. Anything it does not understand passes.
enum SpecShape {
/// The success schema of `GET path` for `status`.
static func responseSchema(_ path: String, status: Int) -> JSONValue? {
Spec.json["paths"]?[path]?["get"]?["responses"]?[String(status)]?["content"]?["application/json"]?["schema"]
}
static func check(_ schema: JSONValue?, _ value: JSONValue, at path: String = "$") -> [String] {
guard let schema, let s = schema.objectValue else { return [] }
var problems: [String] = []
if let alts = s["oneOf"]?.arrayValue {
let matches = alts.map { check($0, value, at: path) }
if !matches.contains(where: \.isEmpty) {
problems.append("\(path): matches none of the oneOf alternatives (\(matches.map { $0.joined(separator: "; ") }.joined(separator: " | ")))")
}
return problems
}
if let c = s["const"] {
if value != c { problems.append("\(path): expected const \(c), got \(value)") }
return problems
}
if let type = s["type"]?.stringValue, !typeMatches(type, value) {
return ["\(path): expected \(type), got \(describe(value))"]
}
if let e = s["enum"]?.arrayValue, !e.contains(value) {
problems.append("\(path): \(value) is not one of \(e.map { $0.stringValue ?? "\($0)" })")
}
if let o = value.objectValue {
let props = s["properties"]?.objectValue ?? [:]
for key in (s["required"]?.arrayValue ?? []).compactMap(\.stringValue) where o[key] == nil {
problems.append("\(path): missing required key \(key)")
}
for (key, v) in o.sorted(by: { $0.key < $1.key }) {
if let sub = props[key] {
problems += check(sub, v, at: "\(path).\(key)")
} else if let ap = s["additionalProperties"] {
if ap == .bool(false) { problems.append("\(path).\(key): undocumented field (value \(describe(v)))") }
else if ap.objectValue != nil { problems += check(ap, v, at: "\(path).\(key)") }
}
}
}
if let a = value.arrayValue {
if let prefix = s["prefixItems"]?.arrayValue {
if a.count != prefix.count { problems.append("\(path): tuple of \(a.count), expected \(prefix.count)") }
for (i, (sub, v)) in zip(prefix, a).enumerated() { problems += check(sub, v, at: "\(path)[\(i)]") }
} else if let items = s["items"], items.objectValue != nil {
for (i, v) in a.enumerated() { problems += check(items, v, at: "\(path)[\(i)]") }
}
}
return problems
}
static func typeMatches(_ type: String, _ v: JSONValue) -> Bool {
switch (type, v) {
case ("null", .null), ("boolean", .bool), ("string", .string), ("array", .array), ("object", .object), ("number", .number): return true
case ("integer", .number(let d)): return d.rounded() == d
default: return false
}
}
static func describe(_ v: JSONValue) -> String {
switch v {
case .null: return "null"
case .bool(let b): return "bool \(b)"
case .number(let d): return "number \(d)"
case .string(let s): return "string \"\(s.prefix(40))\""
case .array(let a): return "array[\(a.count)]"
case .object(let o): return "object{\(o.keys.sorted().prefix(6).joined(separator: ","))}"
}
}
}