Home

Awesome

Muotti<br />

Noun.

  1. mould/mold (hollow form or matrix for shaping a fluid or plastic substance)
  2. cast (mould used to make cast objects)
  3. die
  4. form (thing that gives shape to other things as in a mold)

<sup>(source: https://en.wiktionary.org/wiki/muotti)</sup>

Deploy to Clojars Clojars Project cljdoc badge

Muotti is a graph based value transformer library which aims to solve value transformation by utilizing a digraph of known transformations to produce a transformer chain which is then used to perform the actual transformation.

Archival note

This library is no longer maintained, but remains functional. Its latest published version can be considered feature complete and has proven to be stable. If you need to modify the library in any way, do consider forking.

Usage

Given a map of adjacencies - that is, edges of a graph - with validation and transformer functions:

(require '[muotti.core :as muotti])

(def config {:transformations {[:keyword :string] {:validator   keyword?
                                                   :transformer name}
                               [:string :number]  {:validator   string?
                                                   :transformer parse-long}
                               [:string :boolean] {:validator   string?
                                                   :transformer boolean}
                               [:number :string]  {:validator   number?
                                                   :transformer str}}})

a transformer can be created:

(def t (muotti/->transformer config))

which is then immediately usable for transforming values:

(muotti/transform t :keyword :number :123)
; => 123
(muotti/transform t :number :boolean 123)
; => true  ;; non-empty values are treated as ´true´ by clojure.core/boolean

Unresolvable transformations return a special value:

(muotti/transform t :keyword :double :3.14)
; => ::unknown-path

Transformer chain validation errors also return a special value:

(def broken-adjacency {:transformations {[:a :b] {:validator   keyword?
                                                  :transformer str}}})
(def t2 (muotti/->transformer broken-adjacency))
(muotti/transform t2 :a :b "not a number")
;; => ::invalid-value

All possible paths in the graph will be tested to resolve a result:

(def multiple {:transformations {[:in :num] {:transformer #(Integer/parseInt %)}
                                 [:in :str] {:transformer str}
                                 [:str :out] {:transformer #(= "magic!" %)}
                                 [:num :out] {:transformer #(= 6 %)}}})
(def t3 (muotti/->transformer multiple))
(muotti/transform t3 :in :out "6")
;;=> true
(muotti/transform t3 :in :out "magic!")
;;=> true
(muotti/transform t3 :in :out "0")
;;=> false
(muotti/transform t3 :in :out "anything")
;;=> false

Resolving order of paths is not guaranteed to be stable!

Malli integration

Muotti is made to complement Malli's decoding and encoding capabilities through Malli's Value Transformation capability.

Create a Malli transformer and then use it to call eg. malli.core/decode with the transformer:

(require '[malli.core :as malli])
(require '[muotti.malli :as mm])

(def malli-transformer (mm/transformer (muotti/->transformer mm/malli-config)))

(malli/decode
  [:map
   [:a {:muotti/ignore true} :uuid]
   [:b :int]]
  {:a :invalid
   :b "123"}
  malli-transformer)
;;=> {:a nil, :b 123}

Override source and target types

Use :muotti/source and :muotti/target properties to override transformation types.

See muotti.malli-tests/override-types for examples.

Provide default value for nil inputs

Use :muotti/default to provide a default value.

(malli/decode
  [:string {:muotti/default "hello"}]
  nil
  malli-transformer)
;;=> hello

Supported transformations

Muotti's aim is to support all major Malli types and predicates which are too numerous to list here. Instead see either

  1. muotti.malli-tests namespace or
  2. The DOT graph below

DOT/GraphViz support

It is possible to output the graph contained by the transformer as DOT:

(->> (muotti/->transformer mm/malli-config)
     (muotti/visualize-dot)
     (spit "/tmp/graph.dot"))

The resulting file can be input into GraphViz:

dot -Tpng /tmp/graph.dot > graph.png

which results in DOT example output

Mermaid Flowchart support

For easier embedding the graph can also be converted into a Mermaid Flowchart script:

(->> (muotti/->transformer mm/malli-config)
     (muotti/visualize-mermaid)

This results in a string which can be, for example, put directly into GitHub Markdown file:

flowchart TD
	:qualified-symbol([:qualified-symbol]) --> :string([:string])
	:qualified-symbol([:qualified-symbol]) --> :any([:any])
	:double([:double]) --> float?([float?])
	:double([:double]) --> double?([double?])
	:double([:double]) --> :string([:string])
	:double([:double]) --> number?([number?])
	:double([:double]) --> :any([:any])
	:muotti.malli/big-integer([:muotti.malli/big-integer]) --> integer?([integer?])
	:muotti.malli/big-integer([:muotti.malli/big-integer]) --> nat-int?([nat-int?])
	:muotti.malli/big-integer([:muotti.malli/big-integer]) --> number?([number?])
	:int([:int]) --> :double([:double])
	:int([:int]) --> :muotti.malli/big-integer([:muotti.malli/big-integer])
	:int([:int]) --> int?([int?])
	:int([:int]) --> :muotti.malli/ratio([:muotti.malli/ratio])
	:int([:int]) --> :muotti.malli/float([:muotti.malli/float])
	:int([:int]) --> :string([:string])
	:int([:int]) --> :muotti.malli/big-decimal([:muotti.malli/big-decimal])
	:int([:int]) --> integer?([integer?])
	:int([:int]) --> nat-int?([nat-int?])
	:int([:int]) --> number?([number?])
	:int([:int]) --> :any([:any])
	:symbol([:symbol]) --> :string([:string])
	:symbol([:symbol]) --> :any([:any])
	:qualified-keyword([:qualified-keyword]) --> :string([:string])
	:qualified-keyword([:qualified-keyword]) --> :any([:any])
	int?([int?]) --> neg-int?([neg-int?])
	int?([int?]) --> pos-int?([pos-int?])
	:muotti.malli/ratio([:muotti.malli/ratio]) --> ratio?([ratio?])
	:muotti.malli/ratio([:muotti.malli/ratio]) --> number?([number?])
	:muotti.malli/float([:muotti.malli/float]) --> float?([float?])
	:muotti.malli/float([:muotti.malli/float]) --> number?([number?])
	:string([:string]) --> :double([:double])
	:string([:string]) --> :muotti.malli/big-integer([:muotti.malli/big-integer])
	:string([:string]) --> :int([:int])
	:string([:string]) --> :symbol([:symbol])
	:string([:string]) --> :muotti.malli/ratio([:muotti.malli/ratio])
	:string([:string]) --> :keyword([:keyword])
	:string([:string]) --> :uuid([:uuid])
	:string([:string]) --> :boolean([:boolean])
	:keyword([:keyword]) --> :symbol([:symbol])
	:keyword([:keyword]) --> :string([:string])
	:keyword([:keyword]) --> :any([:any])
	:muotti.malli/big-decimal([:muotti.malli/big-decimal]) --> decimal?([decimal?])
	:muotti.malli/big-decimal([:muotti.malli/big-decimal]) --> float?([float?])
	:muotti.malli/big-decimal([:muotti.malli/big-decimal]) --> number?([number?])
	integer?([integer?]) --> rational?([rational?])
	ratio?([ratio?]) --> rational?([rational?])
	:uuid([:uuid]) --> :string([:string])
	:uuid([:uuid]) --> :any([:any])
	:boolean([:boolean]) --> :string([:string])
	:boolean([:boolean]) --> :any([:any])
	number?([number?]) --> pos?([pos?])
	number?([number?]) --> neg?([neg?])
	number?([number?]) --> zero?([zero?])