api-swift / Tests / IrdAPITests / Live / SpecShape.swift
466 words
 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
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: ","))}"
        }
    }
}

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/api-swift.git