Liking cljdoc? Tell your friends :D

coax

Clojure.spec coercion library for clj(s).

It started as a fork of spec-coerce, which brings a really nice approach to spec based coercion, very much in line with the spec api. Major props to @wilkerlucio for his work on that library it really opened our eyes on how such an api should look like.

As we were patching/changing it internally more and more to fit our needs, it has reached a state where the internals are quite different, it can also do things spec-coerce does not support and change some defaults that didn't match our usage.

Coax is centred around its own registry for coercion rules, when a coercion is not registered it can infer in most cases what to do to coerce a value into something that conforms to a spec. It also supports "coerce time" options to enable custom coercion from any spec type, including spec forms (like s/coll-of & co) or just idents (predicates, registered specs).

What

The typical (infered) example would be :

(s/def ::foo keyword?)
(c/coerce ::foo "bar") -> :bar

registering coercers

You can register a coercer per spec if needed

(s/def ::foo string?)
(c/def ::foo (fn [x opts] (str "from-registry: " x)))
(c/coerce ::foo "bar") -> "from-registry: bar"

Overrides

Overrides allow to change the defaults, essentially all the internal conversion rules are open via the options to coerce, they will be merged with the internal registry at coerce time.

(s/def ::foo keyword?)
(c/coerce ::foo "bar" {::c/idents {::foo (fn [x opts] (str "keyword:" x)})}) -> "keyword:bar"

Coercers are functions of 2 args, the value, and the options coerce received. They return either a coerced value or :exoscale.coax/invalid, which indicates we didn't know how to coerce that value, in which case s/coerce will set the value to the input.

Overrides also works on any qualified-ident (registered specs or symbols/fns), which is something spec-coerce cannot do currently.

The typical example would be :

(s/def ::foo (s/coll-of keyword?))
;; we'll namespace all keywords in that coll-of
(c/coerce ::foo ["a" "b"] {::c/idents {`keyword? (fn [x opts] (keyword "foo" x)})}) -> [foo/a foo/b]

You can specify multiple overrides per coerce call.

Another thing we added is the ability to reach and change the behaviour of coercer generators via ::c/forms, essentially allowing you to support any spec form like inst-in, coll-of, .... You could easily for instance generate open-api definitions using these.

(s/coerce ::foo (s/coll-of keyword?)
          {::c/forms {`s/coll-of (fn [[_ spec]] (fn [x opts] (do-something-crazy-with-spec+the-value spec x opts)))}})

Documentation

cljdocbadge

Installation

coax is available on Clojars.

Add this to your dependencies:

Clojars Project

More Usage examples (taken directly from spec-coerce)

Learn by example:

(ns exoscale.coax.example
  (:require
    [clojure.spec.alpha :as s]
    [exoscale.coax:as c]))

; Define a spec as usual
(s/def ::number int?)

; Call the coerce method passing the spec and the value to be coerced
(c/coerce ::number "42") ; => 42

; Like spec generators, when using `and` it will use the first item as the inference source
(s/def ::odd-number (s/and int? odd?))
(c/coerce ::odd-number "5") ; => 5

; When inferring the coercion, it tries to resolve the upmost spec in the definition
(s/def ::extended (s/and ::odd-number #(> % 10)))
(c/coerce ::extended "11") ; => 11

; Nilables are considered
(s/def ::nilable (s/nilable ::number))
(c/coerce ::nilable "42") ; => 42
(c/coerce ::nilable "foo") ; => "foo"

; The coercion can even be automatically inferred from specs given explicitly as sets of a homogeneous type
(s/def ::enum #{:a :b :c})
(c/coerce ::enum ":a") ; => :a

; If you wanna play around or use a specific coercion, you can pass the predicate symbol directly
(c/coerce `int? "40") ; => 40

; Parsers are written to be safe to call, when unable to coerce they will return the original value
(c/coerce `int? "40.2") ; => "40.2"
(c/coerce `inst? "date") ; => "date"

; To leverage map keys and coerce a composed structure, use coerce-structure
(c/coerce-structure {::number      "42"
                      ::not-defined "bla"
                      :sub          {::odd-number "45"}})
; => {::number      42
;     ::not-defined "bla"
;     :sub          {::odd-number 45}}

; coerce-structure supports overrides, so you can set a custom coercer for a specific context
(c/coerce-structure {::number      "42"
                      ::not-defined "bla"
                      :sub          {::odd-number "45"}}
                     {::c/idents {::not-defined `keyword?
; => {::number      42
;     ::not-defined :bla
;     :sub          {::odd-number 45}}

; If you want to set a custom coercer for a given spec, use the exoscale.coax registry
(defrecord SomeClass [x])
(s/def ::my-custom-attr #(instance? SomeClass %))
(c/def ::my-custom-attr #(map->SomeClass {:x %}))

; Custom registered keywords always takes precedence over inference
(c/coerce ::my-custom-attr "Z") ; => #user.SomeClass{:x "Z"}

(c/coerce ::my-custom-attr "Z") {::c/idents {::my-custom-attr keyword}}) ; => :Z

Examples from predicate to coerced value:

; Numbers
(c/coerce `number? "42")                                   ; => 42.0
(c/coerce `integer? "42")                                  ; => 42
(c/coerce `int? "42")                                      ; => 42
(c/coerce `pos-int? "42")                                  ; => 42
(c/coerce `neg-int? "-42")                                 ; => -42
(c/coerce `nat-int? "10")                                  ; => 10
(c/coerce `even? "10")                                     ; => 10
(c/coerce `odd? "9")                                       ; => 9
(c/coerce `float? "42.42")                                 ; => 42.42
(c/coerce `double? "42.42")                                ; => 42.42
(c/coerce `zero? "0")                                      ; => 0

; Numbers on CLJS
(c/coerce `int? "NaN")                                     ; => js/NaN
(c/coerce `double? "NaN")                                  ; => js/NaN

; Booleans
(c/coerce `boolean? "true")                                ; => true
(c/coerce `boolean? "false")                               ; => false
(c/coerce `true? "true")                                   ; => true
(c/coerce `false? "false")                                 ; => false

; Idents
(c/coerce `ident? ":foo/bar")                              ; => :foo/bar
(c/coerce `ident? "foo/bar")                               ; => 'foo/bar
(c/coerce `simple-ident? ":foo")                           ; => :foo
(c/coerce `qualified-ident? ":foo/baz")                    ; => :foo/baz
(c/coerce `keyword? "keyword")                             ; => :keyword
(c/coerce `keyword? ":keyword")                            ; => :keyword
(c/coerce `simple-keyword? ":simple-keyword")              ; => :simple-keyword
(c/coerce `qualified-keyword? ":qualified/keyword")        ; => :qualified/keyword
(c/coerce `symbol? "sym")                                  ; => 'sym
(c/coerce `simple-symbol? "simple-sym")                    ; => 'simple-sym
(c/coerce `qualified-symbol? "qualified/sym")              ; => 'qualified/sym

; Collections
(c/coerce `(s/coll-of int?) ["5" "11" "42"])               ; => [5 11 42]
(c/coerce `(s/coll-of int?) ["5" "11.3" "42"])             ; => [5 "11.3" 42]
(c/coerce `(s/map-of keyword? int?) {"foo" "42" "bar" "31"})
; => {:foo 42 :bar 31}

; Branching
; tests are realized in order
(c/coerce `(s/or :int int? :bool boolean?) "40")           ; 40
(c/coerce `(s/or :int int? :bool boolean?) "true")         ; true
; returns original value when no options can handle
(c/coerce `(s/or :int int? :bool boolean?) "foo")          ; "foo"

; Tuple
(c/coerce `(s/tuple int? string?) ["0" 1])                 ; => [0 "1"]

; Others
(c/coerce `uuid? "d6e73cc5-95bc-496a-951c-87f11af0d839")   ; => #uuid "d6e73cc5-95bc-496a-951c-87f11af0d839"
(c/coerce `inst? "2017-07-21")                             ; => #inst "2017-07-21T00:00:00.000000000-00:00"
(c/coerce `nil? "foo")                                     ; => "foo"
(c/coerce `nil? nil)                                       ; => nil

;; Clojure only:
(c/coerce `uri? "http://site.com") ; => (URI. "http://site.com")
(c/coerce `decimal? "42.42") ; => 42.42M
(c/coerce `decimal? "42.42M") ; => 42.42M

;; Throw exception when coercipon fails
(c/coerce! ::number "abc") ; => throws (ex-info "Failed to coerce value" {:spec ::number :val "abc" ...})
(c/coerce! :simple-keyword "abc") ; => "abc", coerce! doesn't do anything on simple keywords

;; Conform the result after coerce
(c/conform ::number "40")          ; 40

;; Throw on coerce structure
(c/coerce-structure {::number "42"} {::c/op c/coerce!})

;; Conform on coerce structure
(c/coerce-structure {::number "42"} {::c/op c/conform})

Caching

Coax applies caching of coercers function to cut the cost of walking specs and generating coercers per call, it makes the coercion process orders of magnitude faster once cache (depends on what the coercion does of course). It is on by default. The cache is under exoscale.coax/coercer-cache, it's just an atom holding a map of [spec, options] -> coercer. In most case you should have to care about this, for instance when you define static coercers via coax/def we'll make sure the cache is updated accordingly, but during development you might need to be aware of the existence of that cache (ex if you defined a bugged coercer, or while doing repl dev).

In any case you can turn off the cache by passing :exoscale.coax/cache? false to the options of coerce/conform/coerce-structure, alternatively you can manually fiddle with the cache under exoscale.coax/coercer-cache, for instance via (reset! exoscale.coax/coercer-cache {}).

License

  • License Copyright © 2020 Exoscale - Distributed under ISC License

  • spec-coerce original license Copyright © 2017 Wilker Lúcio - Distributed under the MIT License.

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close