Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix malli swagger defs #863

Merged
merged 10 commits into from
Mar 17, 2023
13 changes: 11 additions & 2 deletions src/malli/json_schema.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@
(defprotocol JsonSchema
(-accept [this children options] "transforms schema to JSON Schema"))

(defn -ref [x] {:$ref (str "#/definitions/" x)})
(defn -ref [x] {:$ref (apply str "#/definitions/"
(cond
;; / must be encoded as ~1 in JSON Schema
(qualified-keyword? x) [(namespace x) "~1"
(name x)]
opqdonut marked this conversation as resolved.
Show resolved Hide resolved
(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)]
(do (swap! definitions assoc ref result) (-ref ref))
(let [ref* (-ref ref)]
(when-not (= ref* result) ; don't create circular definitions
(swap! definitions assoc ref result))
ref*)
result)))
opqdonut marked this conversation as resolved.
Show resolved Hide resolved

(defn select [m] (select-keys m [:title :description :default]))
Expand Down
87 changes: 87 additions & 0 deletions src/malli/swagger.cljc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns malli.swagger
(:require [clojure.set :as set]
[clojure.walk :as walk]
[malli.core :as m]
[malli.json-schema :as json-schema]))

Expand Down Expand Up @@ -54,6 +55,10 @@

(defn -transform [?schema options] (m/walk ?schema -swagger-walker options))

(defn -remove-empty-keys
[m]
(into (empty m) (filter (comp not nil? val) m)))

;;
;; public api
;;
Expand All @@ -67,3 +72,85 @@
::json-schema/definitions definitions
::json-schema/transform -transform})]
(cond-> (-transform ?schema options) (seq @definitions) (assoc :definitions @definitions)))))

(defmulti extract-parameter (fn [in _] in))
opqdonut marked this conversation as resolved.
Show resolved Hide resolved

(defmethod extract-parameter :body [_ schema]
(let [swagger-schema (transform schema {:in :body, :type :parameter})]
[{:in "body"
:name (:title swagger-schema "body")
:description (:description swagger-schema "")
:required (not= :maybe (m/type schema))
:schema swagger-schema}]))

(defmethod extract-parameter :default [in schema]
(let [{:keys [properties required definitions]} (transform schema {:in in, :type :parameter})]
(println "\nextract-parameter definitions:"
ikitommi marked this conversation as resolved.
Show resolved Hide resolved
(with-out-str (clojure.pprint/pprint definitions)))
opqdonut marked this conversation as resolved.
Show resolved Hide resolved
(mapv
(fn [[k {:keys [type] :as schema}]]
(merge
{:in (name in)
:name k
:description (:description schema "")
:type type
:required (contains? (set required) k)}
schema))
properties)))

(defmulti expand (fn [k _ _ _] k))
opqdonut marked this conversation as resolved.
Show resolved Hide resolved

(defmethod expand ::responses [_ v acc _]
{:responses
(into
(or (:responses acc) {})
(for [[status response] v]
[status (-> response
(update :schema transform {:type :schema})
(update :description (fnil identity ""))
-remove-empty-keys)]))})

(defmethod expand ::parameters [_ v acc _]
(let [old (or (:parameters acc) [])
new (mapcat (fn [[in spec]] (extract-parameter in spec)) v)
merged (->> (into old new)
reverse
(reduce
(fn [[ps cache :as acc] p]
(let [c (select-keys p [:in :name])]
(if (cache c)
acc
[(conj ps p) (conj cache c)])))
[[] #{}])
first
reverse
vec)]
{:parameters merged}))

(defn expand-qualified-keywords
opqdonut marked this conversation as resolved.
Show resolved Hide resolved
[x options]
(let [accept? (-> expand methods keys set)]
(walk/postwalk
opqdonut marked this conversation as resolved.
Show resolved Hide resolved
(fn [x]
(if (map? x)
(reduce-kv
(fn [acc k v]
(if (accept? k)
opqdonut marked this conversation as resolved.
Show resolved Hide resolved
(let [expanded (expand k v acc options)
parameters (:parameters expanded)
responses (:responses expanded)
definitions (if parameters
(-> parameters first :schema :definitions)
(->> responses vals (map :schema)
(map :definitions) (apply merge)))]
(-> acc (dissoc k) (merge expanded) (update :definitions merge definitions)))
acc))
x x)
x))
x)))
opqdonut marked this conversation as resolved.
Show resolved Hide resolved

(defn swagger-spec
([x]
(swagger-spec x nil))
([x options]
(expand-qualified-keywords x options)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see some tests for swagger-spec

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see what I can put together from existing spec / schema swagger-spec tests and whatever was in reitit-malli for its roughly-equivalent code.

8 changes: 4 additions & 4 deletions test/malli/experimental/time/json_schema_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
(t/is
(= {:type "object",
:properties
{:date {:$ref "#/definitions/:time/local-date"},
:time {:$ref "#/definitions/:time/offset-time"},
:date-time {:$ref "#/definitions/:time/offset-date-time"},
:duration {:$ref "#/definitions/:time/duration"}},
{:date {:$ref "#/definitions/time~1local-date"},
:time {:$ref "#/definitions/time~1offset-time"},
:date-time {:$ref "#/definitions/time~1offset-date-time"},
:duration {:$ref "#/definitions/time~1duration"}},
:required [:date :time :date-time :duration],
:definitions
#:time{:local-date {:type "string", :format "date"},
Expand Down
110 changes: 58 additions & 52 deletions test/malli/json_schema_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -205,58 +205,64 @@
{:registry registry}))))))

(deftest references-test
(is (= {:$ref "#/definitions/Order",
:definitions {"Country" {:type "object",
:properties {:name {:type "string"
:enum [:FI :PO]},
:neighbors {:type "array"
:items {:$ref "#/definitions/Country"}}},
:required [:name :neighbors]},
"Burger" {:type "object",
:properties {:name {:type "string"},
:description {:type "string"},
:origin {:oneOf [{:$ref "#/definitions/Country"} {:type "null"}]},
:price {:type "integer"
:minimum 1}},
:required [:name :origin :price]},
"OrderLine" {:type "object",
:properties {:burger {:$ref "#/definitions/Burger"},
:amount {:type "integer"}},
:required [:burger :amount]},
"Order" {:type "object",
:properties {:lines {:type "array"
:items {:$ref "#/definitions/OrderLine"}},
:delivery {:type "object",
:properties {:delivered {:type "boolean"},
:address {:type "object",
:properties {:street {:type "string"},
:zip {:type "integer"},
:country {:$ref "#/definitions/Country"}},
:required [:street :zip :country]}},
:required [:delivered :address]}},
:required [:lines :delivery]}}}
(json-schema/transform
[:schema
{:registry {"Country" [:map
[:name [:enum :FI :PO]]
[:neighbors [:vector [:ref "Country"]]]]
"Burger" [:map
[:name string?]
[:description {:optional true} string?]
[:origin [:maybe "Country"]]
[:price pos-int?]]
"OrderLine" [:map
[:burger "Burger"]
[:amount int?]]
"Order" [:map
[:lines [:vector "OrderLine"]]
[:delivery [:map
[:delivered boolean?]
[:address [:map
[:street string?]
[:zip int?]
[:country "Country"]]]]]]}}
"Order"]))))
(testing "absolute doc root definitions are created for ref schemas"
(is (= {:$ref "#/definitions/Order",
:definitions {"Country" {:type "object",
:properties {:name {:type "string"
:enum [:FI :PO]},
:neighbors {:type "array"
:items {:$ref "#/definitions/Country"}}},
:required [:name :neighbors]},
"Burger" {:type "object",
:properties {:name {:type "string"},
:description {:type "string"},
:origin {:oneOf [{:$ref "#/definitions/Country"} {:type "null"}]},
:price {:type "integer"
:minimum 1}},
:required [:name :origin :price]},
"OrderLine" {:type "object",
:properties {:burger {:$ref "#/definitions/Burger"},
:amount {:type "integer"}},
:required [:burger :amount]},
"Order" {:type "object",
:properties {:lines {:type "array"
:items {:$ref "#/definitions/OrderLine"}},
:delivery {:type "object",
:properties {:delivered {:type "boolean"},
:address {:type "object",
:properties {:street {:type "string"},
:zip {:type "integer"},
:country {:$ref "#/definitions/Country"}},
:required [:street :zip :country]}},
:required [:delivered :address]}},
:required [:lines :delivery]}}}
(json-schema/transform
[:schema
{:registry {"Country" [:map
[:name [:enum :FI :PO]]
[:neighbors [:vector [:ref "Country"]]]]
"Burger" [:map
[:name string?]
[:description {:optional true} string?]
[:origin [:maybe "Country"]]
[:price pos-int?]]
"OrderLine" [:map
[:burger "Burger"]
[:amount int?]]
"Order" [:map
[:lines [:vector "OrderLine"]]
[:delivery [:map
[:delivered boolean?]
[:address [:map
[:street string?]
[: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"]))))))

(deftest function-schema-test
(is (= {} (json-schema/transform [:=> [:cat int? int?] int?]))))
Expand Down
Loading