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

Initial implementation of json-schema import #211

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
143 changes: 143 additions & 0 deletions src/malli/json_schema/parse.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
(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})

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

;; 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) [:enum (:const js-schema)]

;; Aggregates
(-keys :oneOf) (into
Copy link

@RutledgePaulV RutledgePaulV Mar 6, 2023

Choose a reason for hiding this comment

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

You can do this by combining and, or, and not.

(is (equivalent? (xor X Y) (or (and X (not Y)) (and Y (not X))))))

It would be nice if malli just added a xor schema though (which could do the same expansion under the hood).

@ikitommi have you considered adding :xor ?

Copy link

@PavlosMelissinos PavlosMelissinos Mar 7, 2023

Choose a reason for hiding this comment

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

xor != oneOf

https://en.wikipedia.org/wiki/Exclusive_or

Since it is associative, it may be considered to be an n-ary operator which is true if and only if an odd number of arguments are true. That is, a XOR b XOR ... may be treated as XOR(a,b,...).

;; TODO Figure out how to make it exclusively select o schema
[: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))

: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-poperties-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 [v]
(let [required (into #{}
;; TODO Should use the same fn as $ref
(map keyword)
(:required v))
closed? (false? (:additionalProperties v))]
(m/schema (-> [:map]
(cond-> closed? (conj {:closed :true}))
(into
(map (partial properties->malli required))
(:properties v))
(with-min-max-poperties-size v)))))

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

(defmethod type->malli "integer" [{:keys [minimum maximum exclusiveMinimum exclusiveMaximum multipleOf]
:or {minimum Integer/MIN_VALUE
maximum Integer/MAX_VALUE}}]
;; On draft 4, exclusive{Minimum,Maximum} is a boolean.
;; TODO Decide on whether draft 4 will be supported
;; TODO Implement exclusive{Minimum,Maximum} support
;; TODO Implement multipleOf support
;; TODO Wrap, when it makes sense, the values below with range checkers, i.e. [:< maximum]
;; TODO extract ranges logic and reuse with number
(cond
(pos? minimum) pos-int?
(neg? maximum) neg-int?
:else int?))

(defmethod type->malli "number" [p] number?)
(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)
(map? items) [:vector (schema->malli items)]
:else (throw (ex-info "Not Supported" {:json-schema p
:reason ::array-items})))))

(defn json-schema-document->malli [obj]
[:schema {:registry (into {}
(map-values schema->malli)
(:definitions obj))}
(schema->malli obj)])