diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd32fc5..2a139aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ## 0.4.0-SNAPSHOT +* `or` and `and` keys are parsed correctly for JSON Schema & Swagger, Fixes [#79](https://github.com/metosin/spec-tools/issues/79) * **BREAKING**: `spec-tools.type` is now `spec-tools.parse` with public api of: - * `parse-spec`: given a spec name, form or instance, maybe returns a spec info map with resolved `:type` and optionally other info, e.g. `:keys` for `s/keys` specs. + * `parse-spec`: given a spec name, form or instance, maybe returns a spec info map with resolved `:type` and optionally other info, e.g. `:keys`, `:keys/req` and `:keys/opt` for `s/keys` specs. * `parse-form`: multimethod to parse info out of a form * Spec Records of `s/and` are fully resolved now, fixes https://github.com/metosin/compojure-api/issues/336 diff --git a/README.md b/README.md index 7d327647..40c390d5 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,9 @@ The following Spec keys having a special meaning: | `:name` | Name of the spec. Maps to `title` in JSON Schema. | | `:description` | Description of the spec. Maps to `description` in JSON Schema. | | `:gen` | Generator function for the Spec (set via `s/with-gen`) | -| `:keys`           | Set of map keys that the spec defines. Extracted from `s/keys` Specs.       | +| `:keys`           | Set of all map keys that the spec defines. Extracted from `s/keys` Specs.   | +| `:keys/req`       | Set of required map keys that the spec defines. Extracted from `s/keys` Specs.| +| `:keys/opt`       | Set of optional map keys that the spec defines. Extracted from `s/keys` Specs.| | `:reason` | Value is added to `s/explain-data` problems under key `:reason` | | `:json-schema/...` | Extra data that is merged with unqualifed keys into json-schema | @@ -462,7 +464,8 @@ A tool to walk over and transform specs using the [Visitor-pattern](https://en.w ; {:spec (clojure.spec.alpha/keys ; :req-un [:user$person$orders/id :user$person$orders/description]) ; :type :map -; :keys #{:id :description}}) +; :keys #{:id :description} +; :keys/req #{:id :description}}) ; :into []) ; :type :vector}) ; :user$person$address/street (spec-tools.core/spec @@ -477,7 +480,8 @@ A tool to walk over and transform specs using the [Visitor-pattern](https://en.w ; {:spec (clojure.spec.alpha/keys ; :req-un [:user$person$address/street :user$person$address/zip]) ; :type :map -; :keys #{:street :zip}})) +; :keys #{:street :zip} +; :keys/req #{:street :zip}})) ; :type nil}) ; :user$person/description (spec-tools.core/spec ; {:spec clojure.core/string? diff --git a/src/spec_tools/impl.cljc b/src/spec_tools/impl.cljc index 8a891e8b..7538d912 100644 --- a/src/spec_tools/impl.cljc +++ b/src/spec_tools/impl.cljc @@ -63,7 +63,14 @@ :else x)) (defn polish-un [x] - (-> x polish name keyword)) + (some-> x polish name keyword)) + +(defn parse-keys [form] + (let [m (some->> form (rest) (apply hash-map))] + (cond-> m + (:req m) (update :req #(->> % flatten (keep polish) (into []))) + (:req-un m) (update :req-un #(->> % flatten (keep polish-un) (into []))) + (:opt-un m) (update :opt-un #(->> % (keep polish-un) (into [])))))) (defn extract-keys [form] (let [{:keys [req opt req-un opt-un]} (some->> form (rest) (apply hash-map))] diff --git a/src/spec_tools/json_schema.cljc b/src/spec_tools/json_schema.cljc index 17daafff..986740ee 100644 --- a/src/spec_tools/json_schema.cljc +++ b/src/spec_tools/json_schema.cljc @@ -185,7 +185,7 @@ schema)) (defmethod accept-spec 'clojure.spec.alpha/keys [_ spec children _] - (let [[_ & {:keys [req req-un opt opt-un]}] (impl/extract-form spec) + (let [{:keys [req req-un opt opt-un]} (impl/parse-keys (impl/extract-form spec)) names-un (map name (concat req-un opt-un)) names (map impl/qualified-name (concat req opt)) required (map impl/qualified-name req) diff --git a/src/spec_tools/parse.cljc b/src/spec_tools/parse.cljc index 39315b3a..4fab47b6 100644 --- a/src/spec_tools/parse.cljc +++ b/src/spec_tools/parse.cljc @@ -110,13 +110,11 @@ (defmethod parse-form :clojure.spec.alpha/unknown [_ _]) (defmethod parse-form 'clojure.spec.alpha/keys [_ form] - (let [{:keys [req opt req-un opt-un]} (some->> form (rest) (apply hash-map))] - {:type :map - :keys (set - (flatten - (concat - (map impl/polish (concat req opt)) - (map impl/polish-un (concat req-un opt-un)))))})) + (let [{:keys [req opt req-un opt-un]} (impl/parse-keys form)] + (cond-> {:type :map + :keys (set (concat req opt req-un opt-un))} + (or req req-un) (assoc :keys/req (set (concat req req-un))) + (or opt opt-un) (assoc :keys/opt (set (concat opt opt-un)))))) (defmethod parse-form 'clojure.spec.alpha/or [_ _]) diff --git a/test/cljc/spec_tools/core_test.cljc b/test/cljc/spec_tools/core_test.cljc index bae02a54..6fa123ed 100644 --- a/test/cljc/spec_tools/core_test.cljc +++ b/test/cljc/spec_tools/core_test.cljc @@ -407,7 +407,9 @@ (testing "all keys types are extracted" (is (= {:type :map - :keys #{::age :lat ::truth :uuid}} + :keys #{::age :lat ::truth :uuid} + :keys/req #{::age :lat} + :keys/opt #{::truth :uuid}} ;; named spec (info/parse-spec @@ -432,7 +434,8 @@ (testing "ands and ors are flattened" (is (= {:type :map - :keys #{::age ::lat ::uuid}} + :keys #{::age ::lat ::uuid} + :keys/req #{::age ::lat ::uuid}} (info/parse-spec (s/keys :req [(or ::age (and ::uuid ::lat))])))))) diff --git a/test/cljc/spec_tools/json_schema_test.cljc b/test/cljc/spec_tools/json_schema_test.cljc index 79634ddb..9e3e88cc 100644 --- a/test/cljc/spec_tools/json_schema_test.cljc +++ b/test/cljc/spec_tools/json_schema_test.cljc @@ -12,7 +12,17 @@ (s/def ::integer integer?) (s/def ::string string?) (s/def ::set #{1 2 3}) -(s/def ::keys (s/keys :req-un [::integer])) + +(s/def ::a string?) +(s/def ::b string?) +(s/def ::c string?) +(s/def ::d string?) +(s/def ::e string?) + +(s/def ::keys (s/keys :opt [::e] + :opt-un [::e] + :req [::a (or ::b (and ::c ::d))] + :req-un [::a (or ::b (and ::c ::d))])) (deftest simple-spec-test (testing "primitive predicates" @@ -39,10 +49,26 @@ :properties {"integer" {:type "integer"} "string" {:type "string"}} :required ["integer"]})) (is (= (jsc/transform ::keys) - {:type "object", - :properties {"integer" {:type "integer"}}, - :required ["integer"], - :title "spec-tools.json-schema-test/keys"})) + {:type "object" + :title "spec-tools.json-schema-test/keys" + :properties {"spec-tools.json-schema-test/a" {:type "string"} + "spec-tools.json-schema-test/b" {:type "string"} + "spec-tools.json-schema-test/c" {:type "string"} + "spec-tools.json-schema-test/d" {:type "string"} + "spec-tools.json-schema-test/e" {:type "string"} + "a" {:type "string"} + "b" {:type "string"} + "c" {:type "string"} + "d" {:type "string"} + "e" {:type "string"}} + :required ["spec-tools.json-schema-test/a" + "spec-tools.json-schema-test/b" + "spec-tools.json-schema-test/c" + "spec-tools.json-schema-test/d" + "a" + "b" + "c" + "d"]})) (is (= (jsc/transform (s/or :int integer? :string string?)) {:anyOf [{:type "integer"} {:type "string"}]})) (is (= (jsc/transform (s/and integer? pos?)) diff --git a/test/cljc/spec_tools/parse_test.cljc b/test/cljc/spec_tools/parse_test.cljc new file mode 100644 index 00000000..f52de410 --- /dev/null +++ b/test/cljc/spec_tools/parse_test.cljc @@ -0,0 +1,22 @@ +(ns spec-tools.parse-test + (:require [clojure.test :refer [deftest is]] + [spec-tools.parse :as parse] + [clojure.spec.alpha :as s])) + +(s/def ::a string?) +(s/def ::b string?) +(s/def ::c string?) +(s/def ::d string?) +(s/def ::e string?) + +(s/def ::keys (s/keys :opt [::e] + :opt-un [::e] + :req [::a (or ::b (and ::c ::d))] + :req-un [::a (or ::b (and ::c ::d))])) + +(deftest parse-test + (is (= {:type :map + :keys #{:a :b :c :d :e ::a ::b ::c ::d ::e} + :keys/req #{:a :b :c :d ::a ::b ::c ::d} + :keys/opt #{:e ::e}} + (parse/parse-spec ::keys))))