Fork me on GitHub
#malli
<
2022-02-22
>
rovanion14:02:39

I'm getting data to a Reitit handler with malli coercion from a simple HTML form. One of the fields should either be a number or be left blank when the form is submitted. I want to translate that to either nil or to remove the key entirely from the map during coercion, is that possible?

rovanion14:02:27

Right now coersion fails because the feild holds a string when it should hold an int, but just defining the value of the field to be [:or string? int?] means that all other functions have to deal with the possibility of that field being a non-int.

dvingo15:02:00

you could make your own schema type and then add custom coercion logic there. For example this is a custom schema for local dates:

:local-date (m/-simple-schema
                            {:type            :local-date
                             :pred            tu/date?
                             :type-properties {:gen/gen gen-date
                                               :decode/string (fn [v]
                                                                (log/info "Decoding to local-date: " v)
                                                                (tu/date v))}})
and add that to your malli registry

dvingo15:02:57

and to deal with the logic of conditionally removing the key from a map, my guess is that you could change the malli coercer: https://github.com/metosin/reitit/blob/master/doc/coercion/malli_coercion.md#configuring-coercion by changing the transformers

rovanion15:02:19

Interesting, I like that first idea. Make a type with a name like :nilable-form-int, simple enough. Thank you! Writing a coercer would probably be better, but I don't think I could wrap my head around it fast enough to still please the deadline.

ikitommi15:02:47

@rovanion maybe:

(def NilableInt
  [:maybe {:decode/string (fn [x] (when-not (str/blank? x) (mt/-string->long x)))} :int])

(m/decode
 [:map
  [:x NilableInt]
  [:y NilableInt]]
 {:x ""
  :y "123"}
 (mt/string-transformer))
; => {:x nil, :y 123}

(mg/sample
 [:map
  [:x NilableInt]
  [:y NilableInt]])
;({:x -1, :y nil}
; {:x nil, :y nil}
; {:x 0, :y -1}
; {:x nil, :y nil}
; {:x 2, :y nil}
; {:x -3, :y -1}
; {:x -2, :y nil}
; {:x 3, :y 1}
; {:x nil, :y nil}
; {:x -8, :y -1})

rovanion16:02:51

Hah, was just writing that exact :decode/string-function in my own simple-schema. But yeah, that fits the bill!

rovanion16:02:34

I really like that Clojure has functions like when-not in the standard library so I don't have to make them up in some util library.

rovanion16:02:14

s/functions/functions and macros/

rovanion17:02:37

This is where I'm at now:

(register! :html/nilable-int                                                                                          
  (malli/-simple-schema                                                                                               
   (fn [opts _]                                                                                                       
     {:type            :html/nilable-int                                                                              
      :pred            (some-fn int? nil?)                                                                            
      :property-pred   (malli/-min-max-pred nil)                                                                      
      :description     "An int or nil. Empty string is transformed to nil."                                           
      :type-properties {:decode/string (fn [x] (when-not (string/blank? x)                                            
                                                 (mtransform/-string->long x)))                                       
                        :gen/gen (gen/large-integer* opts)}})))
I just have to write a generator that generates nil sometimes. Sadly :gen/schema [:maybe [int? opts]] doesn't respect :min passed in opts.

ikitommi18:02:30

int? is just a predicate, use :int , which is a real schema

rovanion18:02:32

The practical implications of the distinction is lost on me, I thought both were resolved to the exact same schema. Either way it made no difference, setting :gen/schema [:maybe [:int opts]] still generated negative numbers when opts was {:min 0}.

ikitommi18:02:55

(mg/sample [:any {:gen/schema [:int {:min 100}]}])
; => (101 101 101 101 102 108 105 100 111 148)

ikitommi19:02:24

(mg/sample [:maybe [:any {:gen/schema [:int {:min 100}]}]])
; => (nil nil nil 102 nil nil 102 103 111 159)

rovanion19:02:38

Sorry, I must have failed to press C-c C-c or something after switching from int? to :int because it's working now and I don't know what else could have caused it to fail. Thank you so much for your time.

rovanion19:02:41

This is the schema I ended up with, does both generate, transform and conform as I want it:

(register! :html/nilable-int                                                                                  
  (malli/-simple-schema                                                                                       
   (fn [opts _]                                                                                               
     {:type            :html/nilable-int                                                                      
      :pred            (some-fn int? nil?)                                                                    
      :property-pred   (malli/-min-max-pred nil)                                                              
      :description     "An int or nil. Empty string is transformed to nil."                                   
      :type-properties {:decode/string (fn [x] (when-not (string/blank? x)                                    
                                                 (mtransform/-string->long x)))                               
                        :gen/schema [:maybe [:int opts]]}})))

Oliver Marks20:02:43

So been playing a bit more and built up some snippet code to show my issues, the first works as I expect the second part does not seem to transform in the same way and I can not figure out why, can any one lend some assistance ?

(def my-spec
  [:map {:closed true}
   [:object/type [:keyword {:json-schema/type "keyword" :string-schema/type "keyword"}]]])

(m/decode
 my-spec
 {:object/type "test"}
 mt/json-transformer)

;; => #:object{:type :test}
How ever using this returns an error complaining that objecct/type is not a keyword, I was under the imrpression it should be converted the same as m/decode, so I am obviously missing a trick some where, tried a few things but just can not get it to decode.
(reitit.coercion.malli/create
 {:transformers {:body {:default reitit.coercion.malli/default-transformer-provider
                        :formats {"application/json" reitit.coercion.malli/json-transformer-provider
                                  "application/transit+json; charset=utf-8" reitit.coercion.malli/json-transformer-provider
                                  "application/transit+json" reitit.coercion.malli/json-transformer-provider}}
                 :keyword {:default reitit.coercion.malli/string-transformer-provider}
                 :string {:default reitit.coercion.malli/string-transformer-provider}
                 :response {:default reitit.coercion.malli/default-transformer-provider}}})

1