Skip to content

Commit

Permalink
feat: support for (mutual) recursion in malli.json-schema
Browse files Browse the repository at this point in the history
fixes #464 #868
  • Loading branch information
opqdonut committed Mar 28, 2023
1 parent 1abb343 commit 77313b9
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 89 deletions.
40 changes: 21 additions & 19 deletions src/malli/json_schema.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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]))

Expand Down Expand Up @@ -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))

Expand Down
27 changes: 25 additions & 2 deletions test/malli/json_schema_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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?]))))
Expand Down
130 changes: 62 additions & 68 deletions test/malli/swagger_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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})}}}))))))

0 comments on commit 77313b9

Please sign in to comment.