clj / test / ird / spec_check.clj
486 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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
(ns ird.spec-check
  "Validate a parsed API response against the OpenAPI schema the committed
  `spec/openapi.json` declares for it.

  This is deliberately small: it checks the properties that actually catch
  drift between the spec and what the API returns — declared types,
  `required` keys, `enum` membership, `oneOf` alternatives, and — because
  every object in this spec is `additionalProperties: false` — fields the
  server sends that the schema does not list. Anything it does not
  understand it passes."
  (:require
   [clojure.string :as str]))

(defn- type-name
  "How VALUE would be spelled as an OpenAPI type, for problem messages."
  [value]
  (cond (nil? value) "null"
        (string? value) "string"
        (boolean? value) "boolean"
        (integer? value) "integer"
        (number? value) "number"
        (sequential? value) "array"
        (map? value) "object"
        :else (str (class value))))

(defn- type-matches?
  [declared value]
  (case declared
    "object" (map? value)
    "array" (sequential? value)
    "string" (string? value)
    "integer" (integer? value)
    "number" (number? value)
    "boolean" (boolean? value)
    "null" (nil? value)
    true))

(declare check)

(defn- check-node
  [schema value path]
  (let [{:strs [type properties required items enum oneOf additionalProperties]} schema]
    (cond
      oneOf
      (when-not (some #(empty? (check % value path)) oneOf)
        [(str path " matches none of oneOf "
              (pr-str (mapv #(get % "type") oneOf))
              " — got " (type-name value))])

      (and enum (not (contains? (set enum) value)))
      [(str path " = " (pr-str value) " is not in the declared enum "
            (pr-str (vec enum)))]

      (and type (not (type-matches? type value)))
      [(str path " is declared " type " but the API returned " (type-name value))]

      :else
      (concat
       (when (and (= type "object") (map? value))
         (for [r (or required []) :when (not (contains? value (keyword r)))]
           (str path "." r " is required by the spec but absent from the response")))
       ;; the inverse, and the one that finds under-documented responses:
       ;; additionalProperties:false makes an unlisted field a violation
       (when (and (= type "object") (map? value) (false? additionalProperties) properties)
         (for [k (keys value) :when (not (contains? properties (name k)))]
           (str path "." (name k) " is returned by the API but absent from the spec")))
       (when (and properties (map? value))
         (mapcat (fn [[pname pschema]]
                   (when (contains? value (keyword pname))
                     (check pschema (get value (keyword pname))
                            (str path "." pname))))
                 properties))
       ;; every row, never a sample: a value that appears only in rare rows
       ;; (an uncommon action type, say) is exactly the one a schema omits
       (when (and items (sequential? value))
         (mapcat #(check items %2 (str path "[" %1 "]")) (range) value))))))

(defn check
  "Problems found validating VALUE against SCHEMA, as a vector of
  human-readable strings; empty when the response conforms. PATH prefixes
  each message so a problem can be located in a nested body."
  [schema value path]
  (if (or (nil? schema) (empty? schema))
    []
    (vec (remove nil? (check-node schema value path)))))

(defn response-schema
  "The schema SPEC declares for METHOD PATH's STATUS response, or nil when
  the spec documents no JSON body for it."
  [spec method path status]
  (get-in spec ["paths" path (name method) "responses" (str status)
                "content" "application/json" "schema"]))

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