Skip to content

Instantly share code, notes, and snippets.

@jeroenvandijk
Created November 3, 2022 14:25
Show Gist options
  • Save jeroenvandijk/080370966fadb7e65601931c3de47ed5 to your computer and use it in GitHub Desktop.
Save jeroenvandijk/080370966fadb7e65601931c3de47ed5 to your computer and use it in GitHub Desktop.
Malli inline validation printing
(ns malli.inline-output
(:require
[edamame.core :refer [parse-string]]
[malli.core :as m]
[malli.error :as me]))
;; -- helper code to find location data in an edn file
(defprotocol IUnwrap
(unwrap [_]))
(extend-protocol IUnwrap
Object
(unwrap [x]
x)
nil
(unwrap [_]
nil))
(defrecord Wrapper [obj]
IUnwrap
(unwrap [_] obj))
(defn iobj? [x]
(instance? clojure.lang.IObj x)
#_? #_(:clj (instance? clojure.lang.IObj x)
:cljs (satisfies? IWithMeta x)))
;; We need to add location data to everything, see https://github.com/borkdude/edamame#postprocess
(defn parse-string-with-detailed-location [s]
(parse-string s {:postprocess
(fn [{:keys [obj loc]}]
(vary-meta (if (iobj? obj)
obj
(->Wrapper obj))
merge loc))}))
(let [sentinel ::not-found]
(defn get-in+ [wrapped-m ks]
(reduce
(fn [m k]
(let [v (get m k sentinel)]
(if (identical? v sentinel)
(if-let [[_ v] (and (map? m)
(some (fn [[k0 v]]
(when (identical? k (unwrap k0))
[:found v]))
m))]
v
(throw (ex-info "could not find entry" {:m wrapped-m
:ks ks})))
v)))
wrapped-m
ks)))
(comment
;; Found
(get-in+ {:a 1} [:a :b])
(get-in+ {nil 1} [nil])
(get-in+ {(->Wrapper :a nil) 1} [:a])
)
;; -- formatting helpers
(defn update-row-at [rows row-id f]
(let [[head [line & left]] (split-at (dec row-id) rows)]
(concat head [(f line)] left)))
(defn with-text-indent [ident text]
(str (clojure.string/join "" (repeat ident " ")) text))
(defn indexed-text [index-format rows]
(clojure.string/join "\n" (map-indexed (fn [i line]
(str (format index-format (inc i)) line))
rows)))
;; -- Combine everything
(defn print-errors-inline [schema input]
(let [value (parse-string input)]
(if-let [{:keys [errors] :as explain-data} (m/explain schema value)]
(let [explainations (me/humanize explain-data)
parsed (parse-string-with-detailed-location input)
left-padding 3
index-format (str "%" left-padding "d: ")
indent (count (format index-format 0))
rows (clojure.string/split-lines input)
errors (group-by :in errors) ;; Deduplify
rows (reduce-kv (fn [rows0 error-path error]
(let [{:keys [location-selector location-path explaination]}
(if (= :malli.core/missing-key (:type (first error)))
{:location-selector (juxt :end-row (fn [location] (dec (:end-col location))))
:location-path (butlast error-path)
:explaination (str (first (get-in explainations error-path)) " " (pr-str (last error-path)))}
{:location-selector (juxt :row :col)
:location-path error-path
:explaination (clojure.string/join ", " (get-in explainations error-path))})
location (meta (get-in+ parsed location-path))
[row col] (location-selector location)]
(update-row-at rows0
row (fn [row]
(str row "\n"
(with-text-indent (+ indent (dec col))
(str "^------- " explaination)))))))
rows errors)]
(println (indexed-text index-format rows))
:invalid-see-console)
:valid)))
(comment
(print-errors-inline
[:map
[:a any?]
[:b integer?]
[:c
[:map [:x string?]]]
]
"{:b :z
:c {:c0 2}
}")
;=>
; 1: {:b :z
; ^------- should be an integer
; 2:
; 3: :c {:c0 2}
; ^------- missing required key :x
; 4:
; 5: }
; ^------- missing required key :a
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment