Thanks for sharing! Malli (and spec-tools) lead here. Agree that data-specs are handy when used with libs like reitit, as long as the needs are simple.
Suggestions:
- It might be better not to support automatically interpreted vectors or map-of’s to avoid ambiguity. simple syntax should be simple
- by default, meta-data is not visible when printing the form, so debugging the syntax is harder as one can’t know which form has the
^:m
- like data-specs, adding functional helpers for optional keys, maybes, and ors might be a good idea.
Here is an alternative implementation:
(ns malli.experimental.lite
(:refer-clojure :exclude [set vector and or])
(:require [malli.core :as m]))
(declare schema)
(defrecord -Optional [value])
(defn -schema [t & xs] (schema (into [t] (map schema xs))))
(defn -entry [[k v]]
(let [[v optional] (if (instance? -Optional v) [(:value v) true] [v])]
(cond-> [k] optional (conj {:optional true}) :always (conj (schema v)))))
(defn schema [x] (m/schema (if (map? x) (into [:map] (map -entry x)) x)))
(defn optional [x] (->-Optional x))
(defn maybe [x] (-schema :maybe x))
(defn set [x] (-schema :set x))
(defn vector [x] (-schema :vector x))
(defn map-of [k v] (-schema :map-of k v))
(defn tuple [& xs] (apply -schema :tuple xs))
(defn and [& xs] (apply -schema :and xs))
(defn or [& xs] (apply -schema :or xs))
defining schemas:
(require '[malli.experimental.lite :as l])
(def Schema
(l/schema
{:map1 {:x int?
:y [:maybe string?]
:z (l/maybe keyword?)}
:map2 {:min-max [:int {:min 0 :max 10}]
:tuples (l/vector (l/tuple int? string?))
:optional (l/optional (l/maybe :boolean))
:set-of-maps (l/set {:e int?
:f string?})
:map-of-int (l/map-of int? {:s string?})}}))
;[:map
; [:map1
; [:map
; [:x int?]
; [:y [:maybe string?]]
; [:z [:maybe keyword?]]]]
; [:map2
; [:map
; [:min-max [:int {:min 0, :max 10}]]
; [:tuples [:vector [:tuple int? string?]]]
; [:optional {:optional true} [:maybe :boolean]]
; [:set-of-maps [:set [:map [:e int?] [:f string?]]]]
; [:map-of-int [:map-of int? [:map [:s string?]]]]]]
works normally with rest o malli:
(require '[malli.core :as m])
(require '[malli.error :as me])
(-> Schema
(m/explain
{:map1 {:x 1
:z "kikka"}
:map2 {:min-max 12
:tuples [[1 "a"] [2 "b"]]
:set-of-maps #{{:e 1, :f "f"}}
:map-of-int {1 {:s "seppo"}
2 {:s :sanni}}}})
(me/humanize))
;{:map1 {:y ["missing required key"]
; :z ["should be a keyword"]}
; :map2 {:min-max ["should be between 0 and 10"]
; :map-of-int {2 {:s ["should be a string"]}}}}
What do you think?
EDIT: I pushed the key optionality into value, instead of special wrapper for key (l/opt
, l/req
like in data-specs), I think it’s simpler that way, yup does it like this too,