diff --git a/src/malli/json_schema.cljc b/src/malli/json_schema.cljc index 80b5a91a5..ceaed24cd 100644 --- a/src/malli/json_schema.cljc +++ b/src/malli/json_schema.cljc @@ -7,24 +7,26 @@ (defprotocol JsonSchema (-accept [this children options] "transforms schema to JSON Schema")) -(defn -ref [x] {:$ref (apply str "#/definitions/" - (cond - ;; / must be encoded as ~1 in JSON Schema - ;; https://json-schema.org/draft/2019-09/relative-json-pointer.html - ;; https://www.rfc-editor.org/rfc/rfc6901 - (qualified-keyword? x) [(namespace x) "~1" - (name x)] - (keyword? x) [(name x)] - :else [x]))}) - -(defn -schema [schema {::keys [transform definitions] :as options}] - (let [result (transform (m/deref schema) options)] - (if-let [ref (m/-ref schema)] - (let [ref* (-ref ref)] - (when-not (= ref* result) ; don't create circular definitions - (swap! definitions assoc ref result)) - ref*) - result))) +(defn -ref [schema {::keys [transform definitions] :as options}] + (let [x (m/-ref schema)] + (when-not (contains? @definitions x) + (let [child (m/deref schema)] + (swap! definitions assoc x ::recursion-stopper) + (swap! definitions assoc x (transform child options)))) + {:$ref (apply str "#/definitions/" + (cond + ;; / must be encoded as ~1 in JSON Schema + ;; https://json-schema.org/draft/2019-09/relative-json-pointer.html + ;; https://www.rfc-editor.org/rfc/rfc6901 + (qualified-keyword? x) [(namespace x) "~1" + (name x)] + (keyword? x) [(name x)] + :else [x]))})) + +(defn -schema [schema {::keys [transform] :as options}] + (if (m/-ref schema) + (-ref schema options) + (transform (m/deref schema) options))) (defn select [m] (select-keys m [:title :description :default])) @@ -174,7 +176,7 @@ (defmethod accept :=> [_ _ _ _] {}) (defmethod accept :function [_ _ _ _] {}) -(defmethod accept :ref [_ schema _ _] (-ref (m/-ref schema))) +(defmethod accept :ref [_ schema _ options] (-ref schema options)) (defmethod accept :schema [_ schema _ options] (-schema schema options)) (defmethod accept ::m/schema [_ schema _ options] (-schema schema options)) diff --git a/test/malli/json_schema_test.cljc b/test/malli/json_schema_test.cljc index 1d5fe9bfc..59a2aca73 100644 --- a/test/malli/json_schema_test.cljc +++ b/test/malli/json_schema_test.cljc @@ -287,11 +287,34 @@ [:zip int?] [:country "Country"]]]]]]}} "Order"])))) - (testing "circular definitions are not created" (is (= {:$ref "#/definitions/Foo", :definitions {"Foo" {:type "integer"}}} (json-schema/transform - (mu/closed-schema [:schema {:registry {"Foo" :int}} "Foo"])))))) + [:schema {:registry {"Foo" :int}} "Foo"])))) + (testing "circular definitions are not created for closed schemas" + (is (= {:$ref "#/definitions/Foo", :definitions {"Foo" {:type "integer"}}} + (json-schema/transform + (mu/closed-schema [:schema {:registry {"Foo" :int}} "Foo"])))))) + +(deftest mutual-recursion-test + (is (= {:$ref "#/definitions/Foo" + :definitions {"Bar" {:$ref "#/definitions/Foo"} + "Foo" {:items {:$ref "#/definitions/Bar"} :type "array"}}} + (json-schema/transform [:schema {:registry {"Foo" [:vector [:schema "Bar"]] ;; NB! :schema instead of :ref + "Bar" [:ref "Foo"]}} + "Foo"]))) + (is (= {:$ref "#/definitions/Foo" + :definitions {"Bar" {:$ref "#/definitions/Foo"} + "Foo" {:items {:$ref "#/definitions/Bar"} :type "array"}}} + (json-schema/transform [:schema {:registry {"Foo" [:vector [:ref "Bar"]] + "Bar" [:ref "Foo"]}} + "Foo"]))) + (is (= {:$ref "#/definitions/Bar", + :definitions {"Bar" {:$ref "#/definitions/Foo"}, + "Foo" {:items {:$ref "#/definitions/Bar"}, :type "array"}}} + (json-schema/transform [:schema {:registry {"Foo" [:vector [:ref "Bar"]] + "Bar" [:ref "Foo"]}} + "Bar"])))) (deftest function-schema-test (is (= {} (json-schema/transform [:=> [:cat int? int?] int?])))) diff --git a/test/malli/swagger_test.cljc b/test/malli/swagger_test.cljc index 1b5319fc2..893c7b6c6 100644 --- a/test/malli/swagger_test.cljc +++ b/test/malli/swagger_test.cljc @@ -362,71 +362,65 @@ 400 {:schema (m/schema ::error-resp {:registry registry})}}}))))) - ;; This test currently fails due to https://github.com/metosin/malli/issues/464 - ;; TODO: Uncomment it when #464 is fixed - #_(testing "generates swagger for ::parameters and ::responses w/ recursive schema + registry" - (let [registry (merge (m/base-schemas) (m/type-schemas) - (m/comparator-schemas) (m/sequence-schemas) - {::a [:or - :string - [:ref ::b]] - ::b [:or - :keyword - [:ref ::c]] - ::c [:or - :symbol - [:ref ::a]] - ;; test would pass if the schema below were e.g. - ;; [:map [:a ::a] [:b ::b] [:c ::c]] (and the - ;; ::req-body expected adjusted accordingly) - ;; b/c then ::b & ::c would be directly used, not just refs - ::req-body [:map [:a ::a]] - ::success-resp [:map-of :keyword :string] - ::error-resp :string})] - (is (= {:definitions {::a {:type "string", - :x-anyOf [{:type "string"} - {:$ref "#/definitions/malli.swagger-test~1b"}]}, - ::b {:type "string" - :x-anyOf [{:type "string"} - {:$ref "#/definitions/malli.swagger-test~1c"}]} - ::c {:type "string" - :x-anyOf [{:type "string"} - {:$ref "#/definitions/malli.swagger-test~1a"}]} - ::error-resp {:type "string"}, - ::req-body {:properties {:a {:$ref "#/definitions/malli.swagger-test~1a"}}, - :required [:a], - :type "object"}, - ::success-resp {:additionalProperties {:type "string"}, - :type "object"}}, - :parameters [{:description "", - :in "body", - :name "body", - :required true, - :schema {:$ref "#/definitions/malli.swagger-test~1req-body", - :definitions {::a {:type "string", - :x-anyOf [{:type "string"} - {:$ref "#/definitions/malli.swagger-test~1b"}]}, - ::b {:type "string" - :x-anyOf [{:type "string"} - {:$ref "#/definitions/malli.swagger-test~1c"}]} - ::c {:type "string" - :x-anyOf [{:type "string"} - {:$ref "#/definitions/malli.swagger-test~1a"}]} - ::req-body {:properties {:a {:$ref "#/definitions/malli.swagger-test~1a"}}, - :required [:a], - :type "object"}}}}], - :responses {200 {:description "", - :schema {:$ref "#/definitions/malli.swagger-test~1success-resp", - :definitions {:malli.swagger-test/success-resp {:additionalProperties {:type "string"}, - :type "object"}}}}, - 400 {:description "", - :schema {:$ref "#/definitions/malli.swagger-test~1error-resp", - :definitions {:malli.swagger-test/error-resp {:type "string"}}}}}} - (swagger/swagger-spec {::swagger/parameters - {:body (m/schema ::req-body - {:registry registry})} - ::swagger/responses - {200 {:schema (m/schema ::success-resp - {:registry registry})} - 400 {:schema (m/schema ::error-resp - {:registry registry})}}})))))) + (testing "generates swagger for ::parameters and ::responses w/ recursive schema + registry" + (let [registry (merge (m/base-schemas) (m/type-schemas) + (m/comparator-schemas) (m/sequence-schemas) + {::a [:or + :string + [:ref ::b]] + ::b [:or + :keyword + [:ref ::c]] + ::c [:or + :symbol + [:ref ::a]] + ::req-body [:map [:a ::a]] + ::success-resp [:map-of :keyword :string] + ::error-resp :string})] + (is (= {:definitions {::a {:type "string", + :x-anyOf [{:type "string"} + {:$ref "#/definitions/malli.swagger-test~1b"}]}, + ::b {:type "string" + :x-anyOf [{:type "string"} + {:$ref "#/definitions/malli.swagger-test~1c"}]} + ::c {:type "string" + :x-anyOf [{:type "string"} + {:$ref "#/definitions/malli.swagger-test~1a"}]} + ::error-resp {:type "string"}, + ::req-body {:properties {:a {:$ref "#/definitions/malli.swagger-test~1a"}}, + :required [:a], + :type "object"}, + ::success-resp {:additionalProperties {:type "string"}, + :type "object"}}, + :parameters [{:description "", + :in "body", + :name "body", + :required true, + :schema {:$ref "#/definitions/malli.swagger-test~1req-body", + :definitions {::a {:type "string", + :x-anyOf [{:type "string"} + {:$ref "#/definitions/malli.swagger-test~1b"}]}, + ::b {:type "string" + :x-anyOf [{:type "string"} + {:$ref "#/definitions/malli.swagger-test~1c"}]} + ::c {:type "string" + :x-anyOf [{:type "string"} + {:$ref "#/definitions/malli.swagger-test~1a"}]} + ::req-body {:properties {:a {:$ref "#/definitions/malli.swagger-test~1a"}}, + :required [:a], + :type "object"}}}}], + :responses {200 {:description "", + :schema {:$ref "#/definitions/malli.swagger-test~1success-resp", + :definitions {:malli.swagger-test/success-resp {:additionalProperties {:type "string"}, + :type "object"}}}}, + 400 {:description "", + :schema {:$ref "#/definitions/malli.swagger-test~1error-resp", + :definitions {:malli.swagger-test/error-resp {:type "string"}}}}}} + (swagger/swagger-spec {::swagger/parameters + {:body (m/schema ::req-body + {:registry registry})} + ::swagger/responses + {200 {:schema (m/schema ::success-resp + {:registry registry})} + 400 {:schema (m/schema ::error-resp + {:registry registry})}}}))))))