malli

https://github.com/metosin/malli :malli:
2020-10-25T10:14:18.307700Z

I did, yes it is more involved 🙂

2020-10-25T10:17:15.307800Z

To be more specific, this project is a (conceptual) port from a clojure.spec project https://github.com/jeroenvandijk/aws.cloudformation.malli

2020-10-25T10:18:43.308200Z

I was generating the clojure.spec definitions from the AWS Cloudformation spec. This generation part was much easier with Malli

2020-10-25T10:19:05.308400Z

There are enough differences that I cannot consider it as a 1 on 1 port

borkdude 2020-10-25T10:40:13.308700Z

Feel free to respond on Reddit as this was not my own question, just forwarded it here

2020-10-25T10:51:34.308900Z

yeah thanks. I’m not using reddit. I hope the reddit user finds it here 😅

👌 1
borkdude 2020-10-25T16:28:42.309400Z

Is there any guidance on how to write your own transformer?

ikitommi 2020-10-25T16:35:43.310100Z

@borkdude there is no guide yet, but lot of tests and some samples in the README.

ikitommi 2020-10-25T16:36:20.310500Z

happy to help if you need any.

borkdude 2020-10-25T16:38:33.311500Z

@ikitommi I think I asked this before, but I can't find any docs on this, nor can I find the conversation on Zulip. So here it goes:

(def Schema
  [:map [:x int?]])

(defrecord Wrapper [obj loc])

parsed 
;;=> {#script.Wrapper{:obj :x, :loc {:row 1, :col 2, :end-row 1, :end-col 4}} #script.Wrapper{:obj 1, :loc {:row 1, :col 5, :end-row 1, :end-col 6}}}

(defn wrapper-transformer []
  (mt/transformer
   {:name :wrapper
    :default-decoder :obj
    :default-encoder (fn [obj]
                       (->Wrapper obj nil))}))

(prn (m/decode Schema parsed wrapper-transformer)) ;;=> nil ... ???

borkdude 2020-10-25T16:40:14.312Z

so I want to validate/transform the wrapped value according to the schema

borkdude 2020-10-25T16:41:28.312400Z

any value that is not Wrapped should just be transformed as is

borkdude 2020-10-25T16:41:42.312700Z

but a value that is wrapped should be unpacked using :obj

borkdude 2020-10-25T16:42:05.313300Z

and I want to give an error message based on :loc

ikitommi 2020-10-25T16:44:11.314200Z

oh, i recall. need to dig in my histories too if there was/is a good answer.

borkdude 2020-10-25T16:45:40.314600Z

I tried this:

(defn unwrap [x]
  (if (instance? Wrapper x)
    (:obj x)
    x))

(defn wrapper-transformer []
  (mt/transformer
   {:name :wrapper
    :default-decoder unwrap
    :default-encoder (fn [obj]
                       (->Wrapper obj nil))}))
but this just returns the entire object itself

borkdude 2020-10-25T16:47:19.315Z

it seems it doesn't handle the value recursively

ikitommi 2020-10-25T16:50:40.315600Z

the problem is that the decoder is first given a map, it calls :obj on it, which return nil.

ikitommi 2020-10-25T16:51:05.316100Z

I would use clojure.walk/pre-walk for first sweep of decoding.

ikitommi 2020-10-25T16:52:26.317400Z

the decoding here walks: 1. the :map 2. the keys (`:x`)

ikitommi 2020-10-25T16:52:58.318100Z

unless the Wrappedis already decoded on 1, there is no :x and the decoding stops.

ikitommi 2020-10-25T16:53:33.318900Z

so, with malli, you would need to decode all the keys & values on :map step also. doable, but extra noise 😞

ikitommi 2020-10-25T16:55:40.319200Z

(ns user
  (:require [malli.core :as m]
            [malli.transform :as mt]))

(def Schema
  [:map [:x int?]])

(defrecord Wrapper [obj loc])

(def parsed {(map->Wrapper {:obj :x, :loc {:row 1, :col 2, :end-row 1, :end-col 4}})
             (map->Wrapper {:obj 1, :loc {:row 1, :col 5, :end-row 1, :end-col 6}})})

(defn unwrap [x]
  (if (instance? Wrapper x) (:obj x) x))

(defn wrapper-transformer []
  (mt/transformer
    {:name :wrapper
     :decoders {:map (fn [x] (reduce-kv (fn [acc k v] (assoc acc (unwrap k) (unwrap v))) {} x))}
     :default-decoder unwrap
     :default-encoder (fn [obj] (->Wrapper obj nil))}))

(m/decode Schema parsed wrapper-transformer) ;;=> nil ... ???
; => {:x 1}

ikitommi 2020-10-25T16:55:49.319500Z

but, this is much simpler:

(clojure.walk/prewalk unwrap parsed)
; => {:x 1}

borkdude 2020-10-25T16:57:51.320100Z

Yes, but how do I get validation errors if I first call prewalk on this thing?

borkdude 2020-10-25T16:58:06.320600Z

based on :loc

ikitommi 2020-10-25T16:58:10.320800Z

I’ll check more of this later.

borkdude 2020-10-25T17:02:46.321Z

Thanks. Me too, cooking dinner :)

borkdude 2020-10-25T17:07:52.321600Z

Hmm, in spec I would maybe have to spec a key as ::wrapped-int and then coerce it after it was checked?

borkdude 2020-10-25T17:13:11.321800Z

which is not ideal

ikitommi 2020-10-25T17:25:40.323800Z

can the wrapped by anywhere? Wrapped map/vector/set? All values are wrapped (any nested edn value)? Or just keys and values in the map?

ikitommi 2020-10-25T17:26:07.324600Z

I have an idea, but would like to understand the case first.

borkdude 2020-10-25T17:54:13.327Z

@ikitommi The use case is preserving location information for non-iobjs and using that for error messages while validating malli schemas

borkdude 2020-10-25T17:54:36.327600Z

e.g. you want to validate an EDN file and you get an error: this should be an int, on line 5, row 12

👌 1
borkdude 2020-10-25T18:16:38.328400Z

@ikitommi This is the complete code: deps.edn:

{:deps {metosin/malli {:mvn/version "0.2.1"}
        borkdude/edamame {:git/url "<https://github.com/borkdude/edamame>"
                          :sha "ba93fcfca1a0fff1f68d5137b98606b82797a17a"}}}
(ns script
  (:require [edamame.core :as e]
            [malli.core :as m]
            [malli.transform :as mt]))

(def Schema
  [:map [:x int?]])

(defrecord Wrapper [obj loc])

(defn iobj? [x]
  (instance? clojure.lang.IObj x))

(def parsed
  (e/parse-string "{:x 1}"
                  {:postprocess
                   (fn [{:keys [:obj :loc]}]
                     (if (iobj? obj)
                       (vary-meta obj merge loc)
                       (-&gt;Wrapper obj loc)))}))

(defn unwrap [x]
  (if (instance? Wrapper x)
    (:obj x)
    x))

(defn wrapper-transformer []
  (mt/transformer
   {:name :wrapper
    :default-decoder unwrap
    :default-encoder (fn [obj]
                       (-&gt;Wrapper obj nil))}))

;; (prn parsed)
(prn (m/decode Schema parsed wrapper-transformer))

borkdude 2020-10-25T19:52:17.329200Z

@ikitommi So the way it can work is:

(defn fail! [schema {:keys [:obj :loc]}]
  (throw (ex-info (str obj " did not satisfy " schema
                       " [at " (str (:col loc)":"(:row loc)) "]") {})))

(def &lt;42 [:and 'int? [:&lt; 42]])

(defn lift-non-iobj-schema [schema]
  [:map {:encode/success :obj,
         :encode/failure (partial fail! schema)} [:obj schema]])

(def Schema [:map [:a (lift-non-iobj-schema &lt;42)]])

(def valid? (m/validator Schema))
(def success (m/encoder Schema (mt/transformer {:name :success})))
(def failure (m/encoder Schema (mt/transformer {:name :failure})))

borkdude 2020-10-25T19:52:33.329400Z

E.g. this will print:

(prn (parse-validate-and-transform "{:a 42}"))

borkdude 2020-10-25T19:52:45.329600Z

Syntax error (ExceptionInfo) compiling at (script.clj:32:1).
42 did not satisfy [:and int? [:&lt; 42]] [at 5:1]

borkdude 2020-10-25T19:53:49.330400Z

The downsize of this is that I have to wrap schemas myself in case I want to check something non-iobj-ish (strings, keywords, numbers)

borkdude 2020-10-25T19:57:25.330800Z

And this doesn't seem to work for keywords for example:

(def &lt;42 [:and 'int? [:&lt; 42]])

(defn lift-non-iobj-schema [schema]
  [:map {:encode/success :obj,
         :encode/failure (partial fail! schema)}
   [:obj schema]])

(def Schema [:map [(lift-non-iobj-schema :a) (lift-non-iobj-schema &lt;42)]])

borkdude 2020-10-25T19:58:09.331200Z

anyway, maybe this is a too niche use case

ikitommi 2020-10-25T20:03:21.331800Z

have an idea, need 30min to test & play

borkdude 2020-10-25T20:05:34.332Z

take yer time

borkdude 2020-10-25T20:06:36.333Z

if this can be fixed, potentially it would also work for rewrite-cljc structures: normal malli specs, but they run over the rewrite-cljc nodes using a simple function that looks at the actual value

borkdude 2020-10-25T20:07:43.333600Z

it's basically a projection from wrapped thing to the essential value while the wrapped thing has more info to produce useful output in case of failure to parse/validate