malli

https://github.com/metosin/malli :malli:
borkdude 2020-08-07T08:46:51.311800Z

Hoe does one combine validate and transform? E.g. this doesn't crash:

(prn (m/decode int? :foo mt/string-transformer))
I'm not saying it should, just wondering how to do it. Not clear from the README

borkdude 2020-08-07T08:48:10.312400Z

Should I first call m/valid, if not valid, then m/explain and else m/decode, effectively traversing the structure twice?

ikitommi 2020-08-07T09:21:51.313600Z

If you need decoding, the flow should be: 1. decode 2. validate 3. explain on error

ikitommi 2020-08-07T09:22:05.314100Z

there is 2-3 walks in the error case.

ikitommi 2020-08-07T09:22:23.314600Z

for happy case, 1-2

ikitommi 2020-08-07T09:24:20.317200Z

having a seoarate optimized validate makes the happy case fast

ikitommi 2020-08-07T09:24:51.318Z

the docs could have examples on this...

ikitommi 2020-08-07T09:26:51.321300Z

the m/decoder doesn't have to walk the structure, it returns an function to transform just the parts that need to be decoded. In case there is nothing to do, it returns identity

borkdude 2020-08-07T09:35:08.321900Z

@ikitommi The concrete example I was going to try:

$ cat deps.edn
{:deps {metosin/malli {:git/url "<https://github.com/metosin/malli>" :sha "2bd749f7148e28a379f1e628a32188e7f6cf0bc4"}
        borkdude/edamame {:git/url "<https://github.com/borkdude/edamame>" :sha "64c7eb43950eb500ba7429dded48257cd15355ae"}}}
$ cat src/edamalli/core.clj
(ns edamalli.core
  (:require [edamame.core :as e]
            [malli.core :as m]
            [malli.transform  :as mt]))

(defrecord WrappedNum [obj loc])

(defn postprocess [{:keys [:obj :loc]}]
  (if (number? obj) (-&gt;WrappedNum obj loc) obj))

(defn -main [&amp; args]
  (prn (e/parse-string "[:foo 42]" {:postprocess postprocess}))
  ;; TODO:
  ;; - validate that the WrappedNum contains value &lt; 42
  ;; - then transform it to only that number
  ;; - else raise error, printing the location metadata of that number
  )
$ clojure -m edamalli.core
[:foo #edamalli.core.WrappedNum{:obj 42, :loc {:row 1, :col 7, :end-row 1, :end-col 9}}]

zclj 2020-08-07T11:06:30.324900Z

@ikitommi I am parsing a schema into a malli-schema. The original might contain recursive references to other "entities" in the schema. I do not know this up front. Are there any trade-offs in putting all my potential recursive entity references in a [:ref ], even if they turn out not to be?

ikitommi 2020-08-07T11:44:50.326900Z

@borkdude would [:foo 42] be transformed to [:foo 42] , as would [:foo :bar #{42}] to itself and {:a 41} would fail on the fact that there was a number that was not 42?

ikitommi 2020-08-07T11:45:18.327700Z

happy to help, sample inputs -> outputs would help.

borkdude 2020-08-07T11:45:54.328600Z

@ikitommi No, [:foo (WrappedNum. x y)] would be transformed to [:foo x] only if x < 42, else error with explain using y

ikitommi 2020-08-07T11:46:46.329300Z

could that validation happen already in the :postprocess?

borkdude 2020-08-07T11:47:45.330800Z

I just want to feed this data to malli and not intertwine parsing data from text to sexprs with malli validation

ikitommi 2020-08-07T11:48:23.331700Z

ok. can the input be anything, e.g [:foo {:bar (WrappedNum. x y)}]?

borkdude 2020-08-07T11:48:23.331800Z

[:foo y] could also be {:foo y} if that's easier for you

borkdude 2020-08-07T11:49:00.332300Z

No, it's more like person: {:name "foo" :age 42}, let's say

borkdude 2020-08-07T11:49:06.332500Z

so attribute+value

borkdude 2020-08-07T11:50:01.333100Z

so unwrapping would just be (:obj wrapped), that would be the transform step

borkdude 2020-08-07T11:50:46.333700Z

The use case for this is: normally edamame doesn't let you have location metadata for numbers and strings, but using a wrapped value you can have that

borkdude 2020-08-07T11:51:15.334300Z

so I want to use malli like normally, but just use the location metadata in the wrapped value for reporting errors and discard it if the value is valid

borkdude 2020-08-07T11:59:35.334800Z

I believe in spec you would write a conformer for this

ikitommi 2020-08-07T12:12:42.335800Z

yes, this is kinda tricky with current malli, as there is not yet a parsing api, like conform.

ikitommi 2020-08-07T12:13:42.336900Z

also, there is no custom overridable validator, so one needs to describe the given data structure (here, a tuple with keyword and a record).

borkdude 2020-08-07T12:15:19.338300Z

ok, so one would write a schema using the records and if everything's ok, then postwalk yourself, unwrapping them?

ikitommi 2020-08-07T12:15:19.338400Z

but, something like this:

;; schemas
(def &lt;42 [:and int? [:&lt; 42]])
(def Schema [:tuple keyword? [:map {:encode/success :obj, :encode/failure :loc} [:obj &lt;42]]])

;; validator and encoders for both success &amp; failure
(def valid? (m/validator Schema))
(def success (m/encoder Schema (mt/transformer {:name :success})))
(def failure (m/encoder Schema (mt/transformer {:name :failure})))

;; in action
(defn parse-validate-and-transform [s]
  (let [x (e/parse-string s {:postprocess postprocess})]
    (if (valid? x) (success x) (failure x))))
=>
(parse-validate-and-transform "[:foo 41]")
; =&gt; [:foo 41]

(parse-validate-and-transform "[:foo 42]")
; =&gt; [:foo {:row 1, :col 7, :end-row 1, :end-col 9}]

ikitommi 2020-08-07T12:16:03.339200Z

yes, postwalk would do. or a recursive schema definition. if the wrapped records can be anywhere

borkdude 2020-08-07T12:16:12.339400Z

let me try your snippet

borkdude 2020-08-07T12:17:52.339700Z

$ clojure -m edamalli.core
Execution error (ExceptionInfo) at malli.core/-fail! (core.cljc:76).
:sci-not-available {:code ":obj"}
That was unexpected, I don't need sci in this example?

ikitommi 2020-08-07T12:18:24.339900Z

oh, should not.

ikitommi 2020-08-07T12:19:22.340200Z

(def Schema [:tuple keyword? [:map {:encode/success (fn [x] (:obj x)), :encode/failure (fn [x] (:loc x)) [:obj &lt;42]}]])

ikitommi 2020-08-07T12:21:29.341300Z

pushed e19872273c3660fbc482dcff4c2d8439dbcbb2a6, which should allow naked keywords as functions.

ikitommi 2020-08-07T12:21:53.341600Z

(def Schema [:tuple keyword? [:map {:encode/success :obj, :encode/failure :loc} [:obj &lt;42]]])

borkdude 2020-08-07T12:21:54.341700Z

borkdude 2020-08-07T12:22:42.342200Z

I'm still getting the sci-not-available error

borkdude 2020-08-07T12:23:56.342600Z

When I do include sci, I get:

[:foo {:row 1, :col 7, :end-row 1, :end-col 9}]

borkdude 2020-08-07T12:24:24.342900Z

I guess I should throw my own error in :encode/failure?

ikitommi 2020-08-07T12:26:21.343400Z

yes, that’s one place to do it. will check why sci is needed. just a sec.

borkdude 2020-08-07T12:27:59.343700Z

(defn parse-validate-and-transform [s]
  (let [x (e/parse-string s {:postprocess postprocess})]
    (if (valid? x)
      (prn "SUCCESS" (success x))
      (prn "ERROR" (failure x)))))
$ clojure -m edamalli.core
"ERROR" [:foo {:row 1, :col 7, :end-row 1, :end-col 9}]

borkdude 2020-08-07T12:29:14.344Z

haha, when I do this:

:encode/failure {:message "should be lower than 42"}
I get:
$ clojure -m edamalli.core
Execution error (ExceptionInfo) at sci.impl.utils/throw-error-with-location (utils.cljc:54).
Could not resolve symbol: should [at line 1, column 1]

borkdude 2020-08-07T12:29:38.344500Z

I have no idea what I'm doing, since I don't know these APIs well. I'll take a look after work again some time

ikitommi 2020-08-07T12:36:40.344900Z

8e067b3d004d1692cbfc695bc73d7e032ecb6e7f

ikitommi 2020-08-07T12:37:14.345500Z

the code used sci for all non fn?s, no to all non ifn?s. need to add tests.

borkdude 2020-08-07T12:38:45.345900Z

@ikitommi I now have this:

(def Schema [:tuple keyword? [:map {:encode/success :obj, :encode/failure {:message "should be lower than 42"}} [:obj &lt;42]]])
Output:
"eval!" "should be lower than 42"
Execution error (ExceptionInfo) at malli.core/-fail! (core.cljc:76).
:sci-not-available {:code "should be lower than 42"}

borkdude 2020-08-07T12:39:14.346300Z

I might be doing something wrong, but it seems there's a debug println in there?

ikitommi 2020-08-07T12:43:05.347200Z

:picard-facepalm: my bad. but this kinda works (but is bad, should be better when we have the parsing api):

ikitommi 2020-08-07T12:43:31.347800Z

➜  ~ clojure -Sdeps '{:deps {metosin/malli {:sha "230b1767729aad3e02568f1320855e2b45d2d9b5", :git/url "<https://github.com/metosin/malli>"}, borkdude/edamame {:sha "64c7eb43950eb500ba7429dded48257cd15355ae", :git/url "<https://github.com/borkdude/edamame>"}}}'
Checking out: <https://github.com/metosin/malli> at 230b1767729aad3e02568f1320855e2b45d2d9b5

Clojure 1.10.1
user=&gt; (ns edamalli.core
  (:require [edamame.core :as e]
            [malli.core :as m]
            [malli.transform :as mt]))

(defrecord WrappedNum [obj loc])

(defn postprocess [{:keys [:obj :loc]}]
  (if (number? obj) (-&gt;WrappedNum obj loc) obj))

(defn fail! [{:keys [:obj :loc]}]
  (throw (ex-info (str "so bad " obj "/" loc) {})))

(def &lt;42 [:and int? [:&lt; 42]])
(def Schema [:tuple keyword? [:map {:encode/success :obj,
                                    :encode/failure fail!} [:obj &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})))

(defn parse-validate-and-transform [s]
  (let [x (e/parse-string s {:postprocess postprocess})]
    (if (valid? x) (success x) (failure x))))

edamalli.core=&gt; (parse-validate-and-transform "[:foo 41]")
[:foo 41]

edamalli.core=&gt; (parse-validate-and-transform "[:foo 42]")
Execution error (ExceptionInfo) at edamalli.core/fail! (REPL:2).
so bad 42/{:row 1, :col 7, :end-row 1, :end-col 9}

ikitommi 2020-08-07T12:45:48.348800Z

@zclj there is a small (have not measured) penalty for using ref-schemas, one function hop basically as the values are memoized.

zclj 2020-08-08T09:45:41.350200Z

thanks for the update! In my use-case I will also do generation from the schema, where I will blow the stack if I don't use :ref for recursive references. Are there any implications for using :ref for potentially non-recursive entities in that case?

ikitommi 2020-08-10T12:05:11.355500Z

I don’t think so.

borkdude 2020-08-07T12:45:53.349100Z

works, thanks

👍 1
zclj 2020-08-07T13:03:56.349700Z

ok, that's fine since I have to do something to solve it anyway, by post-walking or such. Doing it up-front with malli ref considerable make the design simpler. Thanks for the info!