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

Add option to create references for models #274

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Posts:

[![Clojars Project](http://clojars.org/metosin/spec-tools/latest-version.svg)](http://clojars.org/metosin/spec-tools)

Requires Java 1.8, tested with Clojure `1.10.0` and ClojureScript `1.10.520`+.
Requires Java 1.8, tested with Clojure `1.11.0` and ClojureScript `1.11.4`+.

Status: **Alpha** (as spec is still alpha too).

Expand Down
190 changes: 138 additions & 52 deletions src/spec_tools/swagger/core.cljc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns spec-tools.swagger.core
(:require [clojure.walk :as walk]
(:require [clojure.string :as string]
[clojure.walk :as walk]
[spec-tools.json-schema :as json-schema]
[spec-tools.visitor :as visitor]
[spec-tools.impl :as impl]
Expand Down Expand Up @@ -30,18 +31,22 @@

(defn- accept-merge [children]
;; Use x-anyOf and x-allOf instead of normal versions
{:type "object"
:properties (->> (concat children
(mapcat :x-anyOf children)
(mapcat :x-allOf children))
(map :properties)
(reduce merge {}))
;; Don't include top schema from s/or.
:required (->> (concat (remove :x-anyOf children)
(mapcat :x-allOf children))
(map :required)
(reduce into (sorted-set))
(into []))})
(let [children' (map #(if (contains? % :$ref)
(first (vals (::definitions %)))
Copy link
Member

Choose a reason for hiding this comment

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

should this actually look up the value of the ref instead of assuming that ::definitions is a singleton?

%)
children)]
{:type "object"
:properties (->> (concat children'
(mapcat :x-anyOf children')
(mapcat :x-allOf children'))
(map :properties)
(reduce merge {}))
;; Don't include top schema from s/or.
:required (->> (concat (remove :x-anyOf children')
(mapcat :x-allOf children'))
(map :required)
(reduce into (sorted-set))
(into []))}))

(defmethod accept-spec 'clojure.spec.alpha/merge [_ _ children _]
(accept-merge children))
Expand Down Expand Up @@ -89,62 +94,122 @@
(defmethod accept-spec ::default [dispatch spec children options]
(json-schema/accept-spec dispatch spec children options))

(defn- update-if [m k f & args]
(if (contains? m k)
(apply update m k f args)
m))

(defmulti create-or-raise-refs (fn [{:keys [type]} _] type))

(defmethod create-or-raise-refs "object" [swagger options]
(if (and (or (= :schema (:type options))
(= :body (:in options)))
(contains? swagger :title))
(let [title (string/replace (:title swagger) #"/" ".")
swagger' (create-or-raise-refs (dissoc swagger :title) options)]
{:$ref (str "#/definitions/" title)
::definitions (merge {title (dissoc swagger' ::definitions)} (::definitions swagger'))})
(let [definitions (apply merge
(::definitions (:additionalProperties swagger))
(map ::definitions (vals (:properties swagger))))]
(if definitions
(-> swagger
(assoc ::definitions definitions)
(update-if :properties update-vals #(dissoc % ::definitions))
(update-if :additionalProperties dissoc ::definitions))
swagger))))

(defmethod create-or-raise-refs "array" [swagger _]
(let [definitions (get-in swagger [:items ::definitions])]
(if definitions
(-> swagger
(update ::definitions merge definitions)
(update :items dissoc ::definitions))
swagger)))

(defmethod create-or-raise-refs :default [swagger _]
swagger)

(defn- accept-spec-with-refs [dispatch spec children options]
(create-or-raise-refs
(accept-spec dispatch spec children options)
options))

(defn transform
"Generate Swagger schema matching the given clojure.spec spec.

Since clojure.spec is more expressive than Swagger schemas, everything that
satisfies the spec should satisfy the resulting schema, but the converse is
not true."
not true.

Available options:

| Key | Description
|----------|-----------------------------------------------------------
| `:refs?` | Whether refs should be created for objects. Default: false

"
([spec]
(transform spec nil))
([spec options]
(visitor/visit spec accept-spec options)))
(if (:refs? options)
(visitor/visit spec accept-spec-with-refs options)
(visitor/visit spec accept-spec options))))

;;
;; extract swagger2 parameters
;;

(defmulti extract-parameter (fn [in _] in))

(defmethod extract-parameter :body [_ spec]
(let [schema (transform spec {:in :body, :type :parameter})]
[{:in "body"
:name (-> spec st/spec-name impl/qualified-name (or "body"))
:description (-> spec st/spec-description (or ""))
:required (not (impl/nilable-spec? spec))
:schema schema}]))

(defmethod extract-parameter :default [in spec]
(let [{:keys [properties required]} (transform spec {:in in, :type :parameter})]
(mapv
(fn [[k {:keys [type] :as schema}]]
(merge
{:in (name in)
:name k
:description (-> spec st/spec-description (or ""))
:type type
:required (contains? (set required) k)}
schema))
properties)))
(defmulti extract-parameter (fn [in _ & _] in))

(defmethod extract-parameter :body
([in spec]
(extract-parameter in spec nil))
([_ spec options]
(let [schema (transform spec (merge options {:in :body, :type :parameter}))]
[{:in "body"
:name (-> spec st/spec-name impl/qualified-name (or "body"))
:description (-> spec st/spec-description (or ""))
:required (not (impl/nilable-spec? spec))
:schema schema}])))

(defmethod extract-parameter :default
([in spec]
(extract-parameter in spec nil))
([in spec options]
(let [{:keys [properties required]} (transform spec (merge options {:in in, :type :parameter}))]
(mapv
(fn [[k {:keys [type] :as schema}]]
(merge
{:in (name in)
:name k
:description (-> spec st/spec-description (or ""))
:type type
:required (contains? (set required) k)}
schema))
properties))))

;;
;; expand the spec
;;

(defmulti expand (fn [k _ _ _] k))

(defmethod expand ::responses [_ v acc _]
{:responses
(into
(or (:responses acc) {})
(for [[status response] v]
[status (as-> response $
(if (:schema $) (update $ :schema transform {:type :schema}) $)
(update $ :description (fnil identity "")))]))})
(defmethod expand ::responses [_ v acc options]
(let [responses (into
(or (:responses acc) {})
(for [[status response] v]
[status (as-> response $
(if (:schema $) (update $ :schema transform (merge options {:type :schema})) $)
(update $ :description (fnil identity "")))]))]
(if (:refs? options)
{:responses (update-vals responses #(update-if % :schema dissoc ::definitions))
:definitions (apply merge (map #(get-in % [:schema ::definitions]) (vals responses)))}
{:responses responses})))

(defmethod expand ::parameters [_ v acc _]
(defmethod expand ::parameters [_ v acc options]
(let [old (or (:parameters acc) [])
new (mapcat (fn [[in spec]] (extract-parameter in spec)) v)
new (mapcat (fn [[in spec]] (extract-parameter in spec options)) v)
merged (->> (into old new)
(reverse)
(reduce
Expand All @@ -157,23 +222,36 @@
(first)
(reverse)
(vec))]
{:parameters merged}))
(if (:refs? options)
{:parameters (mapv #(update-if % :schema dissoc ::definitions) merged)
:definitions (apply merge (map #(get-in % [:schema ::definitions]) merged))}
{:parameters merged})))

(defn expand-qualified-keywords [x options]
(let [accept? (set (keys (methods expand)))]
(let [accept? (set (keys (methods expand)))
merge-only-maps (fn [& colls] (if (every? map? colls) (apply merge colls) (last colls)))]
(walk/postwalk
(fn [x]
(if (map? x)
(reduce-kv
(fn [acc k v]
(if (accept? k)
(-> acc (dissoc k) (merge (expand k v acc options)))
(merge-with merge-only-maps (dissoc acc k) (expand k v acc options))
Copy link
Member

Choose a reason for hiding this comment

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

why was this change needed?

acc))
x
x)
x))
x)))

(defn- raise-refs-to-top [swagger-doc]
(let [swagger-doc'
(cond-> swagger-doc
(:paths swagger-doc) (->
(assoc :definitions (apply merge (map :definitions (mapcat vals (vals (:paths swagger-doc))))))
(update :paths update-vals (fn [path] (update-vals path #(dissoc % :definitions))))))]
(cond-> swagger-doc'
(nil? (:definitions swagger-doc')) (dissoc swagger-doc' :definitions))))

;;
;; generate the swagger spec
;;
Expand All @@ -182,8 +260,16 @@
"Transforms data into a swagger2 spec. Input data must conform
to the Swagger2 Spec (https://swagger.io/specification/v2/) with a
exception that it can have any qualified keywords that are expanded
with the `spec-tools.swagger.core/expand` multimethod."
with the `spec-tools.swagger.core/expand` multimethod.

Available options:

| Key | Description
|----------|-----------------------------------------------------------
| `:refs?` | Whether refs should be created for objects. Default: false
"
([x]
(swagger-spec x nil))
([x options]
(expand-qualified-keywords x options)))
(cond-> (expand-qualified-keywords x options)
(:refs? options) (raise-refs-to-top))))
Loading