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

Convert JSON schema to malli #915

Draft
wants to merge 90 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
e099544
Initial implementation of json-schema import
hkupty Jun 30, 2020
74f62cf
Simplify functions, rename and make code simpler
hkupty Jul 2, 2020
9690651
Add support for anyOf and allOf
hkupty Jul 2, 2020
b3f6a9a
Rename import->parse
hkupty Jul 3, 2020
0ad4ca5
Implement properties for strings, some int checks
hkupty Jul 4, 2020
404e664
Fix missing `:else` clause
hkupty Sep 17, 2020
fc5059b
Add annotations as malli properties
hkupty Sep 17, 2020
18332a6
Add min/max properties
hkupty Jan 6, 2021
9f380a7
add min/max properties to object
hkupty Jan 6, 2021
1d7a210
Merge branch 'master' of github.com:metosin/malli into js-schema-to-m…
PavlosMelissinos Mar 3, 2023
d7a9f1f
Support uuid-formatted strings
PavlosMelissinos Mar 3, 2023
cf67b24
Support `{..., "type": ["integer","string"], ...}` syntax
PavlosMelissinos Mar 3, 2023
d4d32a5
Fix const
PavlosMelissinos Mar 4, 2023
33fd1b6
Add test for parser
PavlosMelissinos Mar 4, 2023
90b0f39
Fix numbers
PavlosMelissinos Mar 4, 2023
a08acfc
Tweak string parsing (omit :min/:max when {min,max}Length is missing)
PavlosMelissinos Mar 4, 2023
5d2ac57
Prefer :nil over nil?
PavlosMelissinos Mar 4, 2023
69722c8
Remove ambiguous test cases
PavlosMelissinos Mar 4, 2023
7e25ef0
Fix string enums
PavlosMelissinos Mar 4, 2023
f54739a
Remove ambiguous/invalid test cases
PavlosMelissinos Mar 4, 2023
61b174e
Fix case of empty json schema
PavlosMelissinos Mar 4, 2023
0bc83f0
Remove ambiguous test case
PavlosMelissinos Mar 4, 2023
1fbf35a
Fix set parsing
PavlosMelissinos Mar 4, 2023
22572dc
Fix additionalProperties in objects
PavlosMelissinos Mar 4, 2023
b849ddb
Fix typo
PavlosMelissinos Mar 4, 2023
2abfde4
Prefer string keyword
PavlosMelissinos Mar 4, 2023
a4ebd9d
Prefer keywords
PavlosMelissinos Mar 4, 2023
18a49e6
Add support for more types - custom (when single attribute) and file
PavlosMelissinos Mar 4, 2023
66a5e94
Add tests for schema properties and improve test case diffs
PavlosMelissinos Mar 4, 2023
c40e18b
Support annotations
PavlosMelissinos Mar 4, 2023
169d9ea
Specify one-way expectations (mutating conversions)
PavlosMelissinos Mar 5, 2023
81fd361
Tweak
PavlosMelissinos Mar 5, 2023
dcf63bd
Fix indentation
PavlosMelissinos Mar 5, 2023
7160138
Merge branch 'master' of github.com:metosin/malli into js-schema-to-m…
PavlosMelissinos Mar 6, 2023
050b93e
Merge branch 'master' of github.com:metosin/malli into js-schema-to-m…
PavlosMelissinos Mar 12, 2023
f9ad572
BREAKING: -simple-schema :compile
ikitommi Mar 10, 2023
8e6e4ed
BREAKING: -collection-schema :compile
ikitommi Mar 10, 2023
6f5014f
add missing type-hint for :schema
ikitommi Mar 10, 2023
3a8341b
allow old style with DEPRECATED message
ikitommi Mar 13, 2023
6c62326
format
ikitommi Mar 13, 2023
f303d3d
README
ikitommi Mar 13, 2023
7e823e1
Fix inconsistencies on UUID transform helper
niwinz Mar 14, 2023
d2e14f1
Add helper script for start a REPL with rebel-readline
niwinz Mar 14, 2023
fb0a174
cleanup / deloc
ikitommi Mar 17, 2023
f72e692
validator & explain
ikitommi Mar 15, 2023
a782abe
transform + test, WIP
ikitommi Mar 15, 2023
47bca75
fix tests
ikitommi Mar 15, 2023
d42044f
parsers & unparsers
ikitommi Mar 15, 2023
8815f36
reorg tests
ikitommi Mar 15, 2023
e259a3b
parser & unparser
ikitommi Mar 16, 2023
56c250b
json-schema + m/explicit-keys
ikitommi Mar 16, 2023
29592cd
.
ikitommi Mar 16, 2023
a199fb4
fix gens
ikitommi Mar 16, 2023
597c293
m/default-schema
ikitommi Mar 16, 2023
70a23d2
deep merge like a boss
ikitommi Mar 16, 2023
57784bc
deeply recursive test
ikitommi Mar 16, 2023
205658a
fix ::m/default generation on maps
ikitommi Mar 16, 2023
d33258d
fix generators
ikitommi Mar 16, 2023
ef08a73
format
ikitommi Mar 16, 2023
d72099c
strip-extra-keys
ikitommi Mar 17, 2023
880db0e
docs
ikitommi Mar 17, 2023
af91665
cleanup
ikitommi Mar 17, 2023
6b46a6b
CHANGELOG
ikitommi Mar 17, 2023
ac672ac
CHANGELOG
ikitommi Mar 17, 2023
035f873
remove ::m/default wrapping from parsing
ikitommi Mar 17, 2023
d7822a4
better doc
ikitommi Mar 17, 2023
65effca
test: one more test case for ::m/default + strip-extra-keys
opqdonut Mar 17, 2023
58f1ed7
Move swagger gen code from reitit-malli to here
cap10morgan Mar 6, 2023
c5968a3
Properly encode the / in qualified keywords as ~1
cap10morgan Mar 6, 2023
98dfbeb
Don't define circular refs in definitions
cap10morgan Mar 6, 2023
4732c01
Clean up commented-out code & printlns
cap10morgan Mar 8, 2023
ff2c1b7
Prefix remove-empty-keys w/ - & move out of public api
cap10morgan Mar 8, 2023
ba2fc9c
Update test expectations for no leading : & ~1
cap10morgan Mar 8, 2023
0da94b8
Fix circular definition prevention comparison
cap10morgan Mar 8, 2023
9e14bd2
Add a test for circular definitions avoidance
cap10morgan Mar 15, 2023
1ab8cba
Add tests for swagger/swagger-spec
cap10morgan Mar 15, 2023
a4a489d
Add a swagger-spec test w/o a registry
cap10morgan Mar 16, 2023
06495d6
doc: add some citations for ~1 encoding
opqdonut Mar 17, 2023
db3606f
format
ikitommi Mar 18, 2023
40c915a
fix #874
ikitommi Mar 18, 2023
42c7244
CHANGELOG
ikitommi Mar 18, 2023
2524fd3
0.10.3
ikitommi Mar 18, 2023
9fcc8c0
Update deps
ikitommi Mar 18, 2023
576b3ae
remove println, fail on reitit
ikitommi Mar 19, 2023
ff23346
format
ikitommi Mar 19, 2023
feb52a9
CHANGELOG
ikitommi Mar 19, 2023
be3aba1
0.10.4
ikitommi Mar 19, 2023
7d24786
Fix typo
simonacca Mar 29, 2023
d3754ce
Update tips.md
ikitommi Apr 2, 2023
2bac51d
Merge branch 'master' of github.com:metosin/malli into js-schema-to-m…
PavlosMelissinos Apr 7, 2023
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
179 changes: 179 additions & 0 deletions src/malli/json_schema/parse.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
(ns malli.json-schema.parse
(:require [malli.core :as m]
[malli.util :as mu]
[clojure.set :as set]
[clojure.string :as str]))

(def annotations #{:title :description :default :examples :example})

(defn annotations->properties [js-schema]
(-> js-schema
(select-keys annotations)
(set/rename-keys {:examples :json-schema/examples
:example :json-schema/example
:title :json-schema/title
:description :json-schema/description
:default :json-schema/default})))

;; Utility Functions
(defn- map-values
([-fn] (map (fn [[k v]] [k (-fn v)])))
([-fn coll] (sequence (map-values -fn) coll)))

;; Parsing
(defmulti type->malli :type)

(defn $ref [v]
;; TODO to be improved
(keyword (last (str/split v #"/"))))

(defn schema->malli [js-schema]
(let [-keys (set (keys js-schema))]
(mu/update-properties
(cond
(-keys :type) (type->malli js-schema)

(-keys :enum) (into [:enum]
(:enum js-schema))

(-keys :const) [:= (:const js-schema)]

;; Aggregates
(-keys :oneOf) (into
;; TODO Figure out how to make it exclusively select o schema
;; how about `m/multi`?
[:or]
(map schema->malli)
(:oneOf js-schema))

(-keys :anyOf) (into
[:or]
(map schema->malli)
(:anyOf js-schema))

(-keys :allOf) (into
[:and]
(map schema->malli)
(:allOf js-schema))

(-keys :not) [:not (schema->malli (:not js-schema))]

(-keys :$ref) ($ref (:$ref js-schema))

(empty -keys) :any

:else (throw (ex-info "Not supported" {:json-schema js-schema
:reason ::schema-type})))
merge
(annotations->properties js-schema))))

(defn properties->malli [required [k v]]
(cond-> [k]
(nil? (required k)) (conj {:optional true})
true (conj (schema->malli v))))

(defn- prop-size [pred?] (fn [-map] (pred? (count (keys -map)))))
(defn- min-properties [-min] (prop-size (partial <= -min)))
(defn- max-properties [-max] (prop-size (partial >= -max)))

(defn with-min-max-properties-size [malli v]
(let [predicates [(some->> v
(:minProperties)
(min-properties)
(conj [:fn]))
(some->> v
(:maxProperties)
(max-properties)
(conj [:fn]))]]
(cond->> malli
(some some? predicates)
(conj (into [:and]
(filter some?)
predicates)))))

(defn object->malli [{:keys [additionalProperties] :as v}]
(let [required (into #{}
;; TODO Should use the same fn as $ref
(map keyword)
(:required v))
closed? (false? additionalProperties)]
(m/schema (-> (if (:type additionalProperties)
(let [va (schema->malli additionalProperties)] [:map-of va va])
[:map])
(cond-> closed? (conj {:closed :true}))
(into
(map (partial properties->malli required))
(:properties v))
(with-min-max-properties-size v)))))

(defmethod type->malli "string" [{:keys [pattern minLength maxLength enum format]}]
;; `format` metadata is deliberately not considered.
;; String enums are stricter, so they're also implemented here.
(cond
pattern [:re pattern]
enum (into [:enum] enum)
(= format "uuid") :uuid
:else (let [attrs (cond-> nil
minLength (assoc :min minLength)
maxLength (assoc :max maxLength))]
(if attrs
[:string attrs]
:string))))

(defn- number->malli [{:keys [minimum maximum exclusiveMinimum exclusiveMaximum
multipleOf enum type]
:as schema}]
(let [integer (= type "integer")
implicit-double (or minimum maximum integer enum
(number? exclusiveMaximum) (number? exclusiveMinimum))
maximum (if (number? exclusiveMaximum) exclusiveMaximum maximum)
minimum (if (number? exclusiveMinimum) exclusiveMinimum minimum)]
(cond-> (if integer [:int] [])
(or minimum maximum) identity
enum (into [(into [:enum] enum)])
maximum (into [[(if exclusiveMaximum :< :<=) maximum]])
minimum (into [[(if exclusiveMinimum :> :>=) minimum]])
(not implicit-double) (into [[:double]]))))

(defmethod type->malli "integer" [p]
;; TODO Implement multipleOf support
(let [ranges-logic (number->malli p)]
(if (> (count ranges-logic) 1)
(into [:and] ranges-logic)
(first ranges-logic))))

(defmethod type->malli "number" [{:keys [exclusiveMinimum exclusiveMaximum minimum maximum] :as p}]
(let [ranges-logic (number->malli p)]
(if (> (count ranges-logic) 1)
(into [:and] ranges-logic)
(first ranges-logic))))

(defmethod type->malli "boolean" [p] boolean?)
(defmethod type->malli "null" [p] :nil)
(defmethod type->malli "object" [p] (object->malli p))
(defmethod type->malli "array" [p] (let [items (:items p)]
(cond
(vector? items) (into [:tuple]
(map schema->malli)
items)
(:uniqueItems p) [:set (schema->malli items)]
(map? items) [:vector (schema->malli items)]
:else (throw (ex-info "Not Supported" {:json-schema p
:reason ::array-items})))))

(defmethod type->malli "file" [p]
[:map {:json-schema {:type "file"}} [:file :any]])

(defmethod type->malli :default [{:keys [type] :as p}]
(cond
(vector? type) (into [:or] (map #(type->malli {:type %}) type))
(and type (= 1 (count (keys p)))) {:json-schema/type type}
:else
(throw (ex-info "Not Supported" {:json-schema p
:reason ::unparseable-type}))))

(defn json-schema-document->malli [obj]
[:schema {:registry (into {}
(map-values schema->malli)
(:definitions obj))}
(schema->malli obj)])
117 changes: 117 additions & 0 deletions test/malli/json_schema/parse_test.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
(ns malli.json-schema.parse-test
(:require [clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.core-test]
[malli.json-schema :as json-schema]
[malli.json-schema.parse :as sut]
[malli.util :as mu]))

(def expectations
[ ;; predicates
[[:and :int [:>= 1]] {:type "integer", :minimum 1} :one-way true]
[[:and :int [:>= 1]] {:allOf [{:type "integer"} {:type "number", :minimum 1}]}]
[[:> 0] {:type "number" :exclusiveMinimum 0}]
[:double {:type "number"}]
;; comparators
[[:> 6] {:type "number", :exclusiveMinimum 6}]
[[:>= 6] {:type "number", :minimum 6}]
[[:< 6] {:type "number", :exclusiveMaximum 6}]
[[:<= 6] {:type "number", :maximum 6}]
[[:= "x"] {:const "x"}]
;; base
[[:not :string] {:not {:type "string"}}]
[[:and :int [:and :int [:>= 1]]] {:allOf [{:type "integer"}
{:type "integer", :minimum 1}]} :one-way true]
[[:or :int :string] {:anyOf [{:type "integer"} {:type "string"}]}]
[[:map
[:a :string]
[:b {:optional true} :string]
[:c :string]] {:type "object"
:properties {:a {:type "string"}
:b {:type "string"}
:c {:type "string"}}
:required [:a :c]}]
[[:or [:map [:type :string] [:size :int]] [:map [:type :string] [:name :string] [:address [:map [:country :string]]]] :string]
{:oneOf [{:type "object",
:properties {:type {:type "string"}
:size {:type "integer"}},
:required [:type :size]}
{:type "object",
:properties {:type {:type "string"},
:name {:type "string"},
:address {:type "object"
:properties {:country {:type "string"}}
:required [:country]}},
:required [:type :name :address]}
{:type "string"}]} :one-way true]
[[:map-of :string :string] {:type "object"
:additionalProperties {:type "string"}}]
[[:vector :string] {:type "array", :items {:type "string"}}]
[[:set :string] {:type "array"
:items {:type "string"}
:uniqueItems true}]
[[:enum 1 2 "3"] {:enum [1 2 "3"]}]
[[:and :int [:enum 1 2 3]] {:type "integer" :enum [1 2 3]} :one-way true]
[[:enum 1.1 2.2 3.3] {:type "number" :enum [1.1 2.2 3.3]}]
[[:enum "kikka" "kukka"] {:type "string" :enum ["kikka" "kukka"]}]
[[:enum :kikka :kukka] {:type "string" :enum [:kikka :kukka]}]
[[:enum 'kikka 'kukka] {:type "string" :enum ['kikka 'kukka]}]
[[:or :string :nil] {:oneOf [{:type "string"} {:type "null"}]} :one-way true]
[[:or :string :nil] {:anyOf [{:type "string"} {:type "null"}]}]
[[:tuple :string :string] {:type "array"
:items [{:type "string"} {:type "string"}]
:additionalItems false}]
[[:re "^[a-z]+\\.[a-z]+$"] {:type "string", :pattern "^[a-z]+\\.[a-z]+$"}]
[:any {}]
[:nil {:type "null"}]
[[:string {:min 1, :max 4}] {:type "string", :minLength 1, :maxLength 4}]
[[:and :int [:<= 4] [:>= 1]] {:type "integer", :minimum 1, :maximum 4} :one-way true]
[[:and [:<= 4] [:>= 1]] {:type "number", :minimum 1, :maximum 4} :one-way true]
[:uuid {:type "string", :format "uuid"}]

[:int {:type "integer"}]
;; type-properties
[[:and :int [:>= 6]] {:type "integer", :format "int64", :minimum 6} :one-way true]
[[:and {:json-schema/example 42} :int [:>= 6]] {:type "integer", :format "int64", :minimum 6, :example 42} :one-way true]])

(deftest json-schema-test
(doseq [[schema json-schema & {:keys [one-way]}] expectations]
(testing json-schema
(is (= schema
(m/form (sut/schema->malli json-schema)))))

(when-not one-way
(testing (str "round trip " json-schema "\n" schema)
(is (= json-schema
(-> json-schema sut/schema->malli malli.json-schema/transform))))))

(testing "full override"
(is (= [:map {:json-schema {:type "file"}} [:file :any]]
(m/form (sut/schema->malli {:type "file"})))))

(testing "with properties"
(is (= [:map
[:x1 [:string {:json-schema/title "x"}]]
[:x2 [:any #:json-schema{:default "x" :title "x"}]]
[:x3 [:string #:json-schema{:title "x" :default "x"}]]
[:x4 {:optional true} [:any #:json-schema{:title "x-string" :default "x2"}]]]

(m/form (sut/schema->malli {:type "object",
:properties {:x1 {:title "x", :type "string"}
:x2 {:title "x", :default "x"}
:x3 {:title "x", :type "string", :default "x"}
:x4 {:title "x-string", :default "x2"}},
:required [:x1 :x2 :x3]}))))

#_(testing "custom type"
(is (= [:map
[:x5 {:json-schema/type "x-string"} :string]]
(m/form (sut/schema->malli {:type "object", :properties {:x5 {:type "x-string"}}, :required [:x5]})))))

(is (= [:and {:json-schema/title "age"
:json-schema/description "blabla"
:json-schema/default 42} :int]
(m/form (sut/schema->malli {:allOf [{:type "integer"}]
:title "age"
:description "blabla"
:default 42}))))))