diff --git a/src/spec_tools/json_schema.cljc b/src/spec_tools/json_schema.cljc index 97335d5..413bac7 100644 --- a/src/spec_tools/json_schema.cljc +++ b/src/spec_tools/json_schema.cljc @@ -184,12 +184,19 @@ (assoc schema :title (impl/qualified-name title)) schema))) -(def key-group-mapping - {'or :anyOf ;; there is no 'xor' key-group, so 'anyOf' is more appropriate than 'oneOf' +(def key-group-mapping + {'or :anyOf ;; there is no 'xor' key-group, so 'anyOf' is more appropriate than 'oneOf' 'and :allOf}) -(defn- parse-required1 - "Helper for generating correct schemas for :req/:req-un keys, +(defn- simplify-allOf-anyOf [expr] + (let [[k v] (first expr)] + (cond + (= 1 (count v)) (first v) + (and (= k :allOf) (every? :required v)) {:required (into [] (mapcat :required) v)} + :else expr))) + +(defn- parse-required1 + "Helper for generating correct schemas for :req/:req-un keys, taking into account potential or/and key-goups." [name-fn x] (if (list? x) ;; found key-group @@ -197,24 +204,12 @@ (throw (ex-info "unsupported key-group expression" {:expression (first x)}))) v (mapv (partial parse-required1 name-fn) (next x))] - {k (if (and (= k :allOf) - (every? :required v)) - [{:required (into [] (mapcat :required) v)}] - v)}) + (simplify-allOf-anyOf {k v})) {:required [(name-fn x)]})) (def parse-req* (partial parse-required1 impl/qualified-name)) (def parse-req-un* (partial parse-required1 name)) -(comment - - (parse-req-un* '(or :foo (and :bar :baz))) - ;; => - {:anyOf [{:required ["foo"]} - {:allOf [{:required ["bar"]} - {:required ["baz"]}]}]} - ) - (defmethod accept-spec 'clojure.spec.alpha/keys [_ spec children options] (let [form (impl/extract-form spec) {:keys [req req-un opt opt-un]} (impl/parse-keys form) @@ -229,10 +224,7 @@ {:type "object" :properties (zipmap (concat names names-un) children)} (when all-required - (if (every? :required all-required) - ;; avoid changing the simple case & break existing tests - {:required (into [] (mapcat :required) all-required)} - {:allOf (vec all-required)}))) + (simplify-allOf-anyOf {:allOf (vec all-required)}))) spec options))) diff --git a/test/cljc/spec_tools/json_schema_test.cljc b/test/cljc/spec_tools/json_schema_test.cljc index 670e463..c8b58aa 100644 --- a/test/cljc/spec_tools/json_schema_test.cljc +++ b/test/cljc/spec_tools/json_schema_test.cljc @@ -92,11 +92,11 @@ "e" {:type "string"}} :allOf [{:required ["spec-tools.json-schema-test/a"]} {:anyOf [{:required ["spec-tools.json-schema-test/b"]} - {:allOf [{:required ["spec-tools.json-schema-test/c" - "spec-tools.json-schema-test/d"]}]}]} + {:required ["spec-tools.json-schema-test/c" + "spec-tools.json-schema-test/d"]}]} {:required ["a"]} {:anyOf [{:required ["b"]} - {:allOf [{:required ["c" "d"]}]}]}]})) + {:required ["c" "d"]}]}]})) (is (= (jsc/transform ::keys-no-req) {:type "object" :title "spec-tools.json-schema-test/keys-no-req" @@ -156,6 +156,63 @@ (is (not= (jsc/transform (s/coll-of (s/tuple string? any?) :into {})) {:type "object", :additionalProperties {:type "string"}})))) +(deftest parse-req-test + (is (= {:required ["foo"]} + (jsc/parse-req-un* :foo))) + (is (= {:required ["foo.bar/baz"]} + (jsc/parse-req* :foo.bar/baz))) + (is (= {:anyOf [{:required ["foo"]} + {:required ["bar" "baz"]}]} + (jsc/parse-req-un* '(or :foo (and :bar :baz))))) + (is (= {:required ["foo"]} + (jsc/parse-req-un* '(or :foo)))) + (is (= {:required ["foo"]} + (jsc/parse-req-un* '(and :foo)))) + (is (= {:anyOf [{:required ["foo" "bar"]} + {:allOf [{:required ["baz"]} + {:anyOf [{:required ["quux"]} + {:required ["ding"]}]} + {:required ["bim"]}]} + {:required ["bam"]}]} + (jsc/parse-req-un* '(or (and :foo :bar) + (and :baz (or :quux :ding) :bim) + :bam))))) + +(s/def ::keys2 (s/keys :req [::zipcode ::id ::age])) +(s/def ::keys2-or (s/keys :req [::zipcode (or ::id ::age)])) +(s/def ::keys2-or-and (s/keys :req [(or ::id (and ::age ::zipcode))])) +(s/def ::keys2-and (s/keys :req [::zipcode (and ::id ::age)])) ; should be equivalent to ::keys2 +(s/def ::keys2-and-or (s/keys :req [(and ::zipcode (or ::id ::age))])) ; should be equivalent to ::keys2-or + +(deftest keys-test + (is (= {:type "object" + :properties {"spec-tools.json-schema-test/zipcode" {:type "string"} + "spec-tools.json-schema-test/id" {:type "integer"} + "spec-tools.json-schema-test/age" {:type "integer"}} + :required ["spec-tools.json-schema-test/zipcode" + "spec-tools.json-schema-test/id" + "spec-tools.json-schema-test/age"]} + (dissoc (jsc/transform ::keys2) :title) + (dissoc (jsc/transform ::keys2-and) :title))) + (is (= {:type "object" + :properties {"spec-tools.json-schema-test/zipcode" {:type "string"} + "spec-tools.json-schema-test/id" {:type "integer"} + "spec-tools.json-schema-test/age" {:type "integer"}} + :allOf [{:required ["spec-tools.json-schema-test/zipcode"]} + {:anyOf + [{:required ["spec-tools.json-schema-test/id"]} + {:required ["spec-tools.json-schema-test/age"]}]}]} + (dissoc (jsc/transform ::keys2-or) :title) + (dissoc (jsc/transform ::keys2-and-or) :title))) + (is (= {:type "object" + :properties {"spec-tools.json-schema-test/zipcode" {:type "string"} + "spec-tools.json-schema-test/id" {:type "integer"} + "spec-tools.json-schema-test/age" {:type "integer"}} + :anyOf [{:required ["spec-tools.json-schema-test/id"]} + {:required ["spec-tools.json-schema-test/age" + "spec-tools.json-schema-test/zipcode"]}]} + (dissoc (jsc/transform ::keys2-or-and) :title)))) + (s/def ::id string?) (s/def ::age string?) (s/def ::zipcode string?)