Skip to content

Instantly share code, notes, and snippets.

@ikitommi
Last active November 9, 2020 14:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ikitommi/e3229a0bcef532d1fa032321713227d3 to your computer and use it in GitHub Desktop.
Save ikitommi/e3229a0bcef532d1fa032321713227d3 to your computer and use it in GitHub Desktop.
Demo of validating and coercing EDN data with edamame and malli
{:id "Lillan"
:tags #{:artesan :coffee ":hotel"}
:address {:street "Ahlmanintie 29"
:city "Tampere"
:zip "33100"
:lonlat [61.4858322, 23.7854658]}}
(require '[malli.core :as m])
(require '[malli.transform :as mt])
(require '[edamame.core :as e])
(require '[clojure.walk :refer [prewalk]])
(defrecord Wrapped [])
(defn parse
"Parses an EDN String with edamame and returns a tuple2 of edn-value & path-vec->loc"
[s]
(let [data (e/parse-string s {:postprocess map->Wrapped})
unwrap-1 (fn [v] (if (instance? Wrapped v) (:obj v) v))
unwrap (fn [v] (prewalk unwrap-1 v))
collect (fn collect [acc path {:keys [loc obj]}]
(let [acc (assoc acc path loc)]
(cond
(map? obj) (reduce (fn [acc kv]
(let [k (unwrap (key kv))
acc (collect acc (conj path [:key k]) (key kv))]
(collect acc (conj path k) (val kv)))) acc obj)
(set? obj) (reduce (fn [acc v] (collect acc (conj path (unwrap v)) v)) acc obj)
(vector? obj) (reduce-kv (fn [acc i child] (collect acc (conj path i) child)) acc obj)
(sequential? obj) (transduce (map-indexed vector)
(completing (fn [acc [i child]]
(collect acc (conj path i) child))) acc obj)
:else acc)))]
[(unwrap data) (collect {} [] data)]))
(defn coercer
"Takes a schema and optionally transformers and returns a coercer fn of string -> decoded|error,
with error containing the edamame loc info under :loc for each actual error"
[schema & transformers]
(let [decode (m/decoder schema (apply mt/transformer {:name :edamame} transformers))
explain (m/explainer schema)]
(fn [s]
(let [[data in->loc] (parse s)
decoded (decode data)]
(or (some-> (explain decoded)
(update :errors (partial map #(assoc % :loc (in->loc (:in %)))))
(assoc :type ::error)
(assoc :string s))
decoded)))))
(defn error? [x] (= ::error (:type x)))
;;
;;
;;
(def Address
[:map
[:id string?]
[:tags [:set keyword?]]
[:address
[:map
[:street string?]
[:city string?]
[:zip int?]
[:lonlat [:tuple double? double?]]]]])
(def data (slurp "schema.edn"))
((coercer Address) data)
;{:type :user/error
; :schema [:map
; [:id string?]
; [:tags [:set keyword?]]
; [:address
; :map
; [:street string?]
; [:city string?]
; [:zip int?]
; [:lonlat [:tuple double? double?]]]],
; :value {:tags #{":hotel" :coffee :artesan},
; :address {:lonlat [61.4858322 23.7854658]
; :city "Tampere"
; :street "Ahlmanintie 29"
; :zip "33100"},
; :id "Lillan"},
; :errors (#Error{:path [:tags 0],
; :in [:tags ":hotel"],
; :schema keyword?,
; :value ":hotel",
; :loc {:row 2, :col 27, :end-row 2, :end-col 35}}
; #Error{:path [:address :zip],
; :in [:address :zip],
; :schema int?,
; :value "33100",
; :loc {:row 5, :col 17, :end-row 5, :end-col 24}}),
; :string "{:id \"Lillan\"
; :tags #{:artesan :coffee \":hotel\"}
; :address {:street \"Ahlmanintie 29\"
; :city \"Tampere\"
; :zip \"33100\"
; :lonlat [61.4858322, 23.7854658]}}
; "}
((coercer Address (mt/string-transformer)) data)
{:id "Lillan"
:tags #{:coffee :artesan ::hotel},
:address {:street "Ahlmanintie 29"
:zip 33100
:city "Tampere"
:lonlat [61.4858322 23.7854658]}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment