Created
November 3, 2022 14:25
-
-
Save jeroenvandijk/080370966fadb7e65601931c3de47ed5 to your computer and use it in GitHub Desktop.
Malli inline validation printing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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