Giter Club home page Giter Club logo

spec-coerce's Introduction

spec-coerce Clojars Project Test cljdoc badge

A Clojure(script) library designed to leverage your specs to coerce your information into correct types.

Spec Coerce will remain in alpha while clojure.spec still in alpha.

Usage

Learn by example:

(ns spec-coerce.example
  (:require
    [clojure.spec.alpha :as s]
    [spec-coerce.core :as sc]))
    
; Define a spec as usual
(s/def ::number int?)

; Call the coerce method passing the spec and the value to be coerced
(sc/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?))
(sc/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)))
(sc/coerce ::extended "11") ; => 11

; Nilables are considered
(s/def ::nilable (s/nilable ::number))
(sc/coerce ::nilable "42") ; => 42
(sc/coerce ::nilable "nil") ; => nil
(sc/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})
(sc/coerce ::enum ":a") ; => :a

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

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

; To leverage map keys and coerce a composed structure, use coerce-structure
(sc/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, and can be also a point
; to set coercer for unqualified keys
(sc/coerce-structure {::number      "42"
                      ::not-defined "bla"
                      :unqualified  "12"
                      :sub          {::odd-number "45"}}
                     {::sc/overrides {::not-defined `keyword?
                                      :unqualified  ::number}})
; => {::number      42
;     ::not-defined :bla
;     :unqualified  12
;     :sub          {::odd-number 45}}

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

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

; Coercers in the registry can be overriden within a specific context
(binding [sc/*overrides* {::my-custom-attr keyword}]
  (sc/coerce ::my-custom-attr "Z")) ; => :Z

Examples from predicate to coerced value:

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

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

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

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

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

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

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

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

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

;; Throw exception when coercion fails
(sc/coerce! `int? "abc") ; => throws (ex-info "Failed to coerce value" {:spec `int? :value "abc"})
(sc/coerce! :simple-keyword "abc") ; => "abc", coerce! doesn't do anything on simple keywords

;; Conform the result after coerce
(sc/conform `(s/or :int int? :bool boolean?) "40")          ; [:int 40]

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

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

License

Copyright © 2017 Wilker Lúcio

Distributed under the MIT License.

spec-coerce's People

Contributors

gfredericks avatar joodie avatar lukasrychtecky avatar mpenet avatar nogden avatar seancorfield avatar vemv avatar whittlesjr avatar wilkerlucio avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

spec-coerce's Issues

Throws error for a spec with an `or` form in the `:req-un` set of a `keys` form.

In spec it is perfectly valid to logically or keys in the :req(-un) section of a keys form. E.g.

;; A map that must have either a bar or a baz
(s/def ::foo (s/keys :req [(or ::bar ::baz)]))

Coerce throws an error when trying to coerce using a spec that uses or in the :req-un section of a keys form. E.g.

(spec/def ::foo int?)
(spec/def ::bar string?)
(spec/def ::baz (spec/keys :req-un [(or ::foo ::bar)]))

(coerce/coerce ::baz {:foo "1" :bar "hi"}) ;=> ClassCastException

The same example using fully qualified keys (:req) seems to work correctly.

Bad coercions for pos/neg/nat specs

"-42" coerced to pos?, pos-int?, nat?, nat-int? appears to result in -42.

Conversely, "42" coerced to neg?, neg-int? results in 42.

The problem with this is that it's inconsistent with the general behavior that if x was coerced to pred, then f is a valid pred.

You can reproduce this adding some test cases:

image

Those cases will fail, and IMO they shouldn't.

WDYT?

Thanks - Victor

Coerce doesn't work in cljs

Hello,
I have problems with this lib:

(ns test.foo
  (:require [spec-coerce.core :as sc]))

(sc/coerce `int? "10") => "10"

my dependencies

 :deps
 {reagent        {:mvn/version "0.8.1"}
  re-frame       {:mvn/version "0.10.6"}
  metosin/reitit {:mvn/version "0.2.2"}
  spec-coerce    {:mvn/version "1.0.0-alpha6"}}

how I can debug problem?

Coercing a sequence changes its order

Hi,

when coercing a sequence its order is changed depending by its type (list/vector).

(require '[clojure.spec.alpha :as s])
(require '[spec-coerce.core :as sc])

(s/def ::num int?)
(s/def ::obj (s/keys :req-un [::num]))
(s/def ::seq (s/coll-of ::obj))

(sc/coerce ::seq [{:a 1} {:a 2}]) ;; => [{:a 1} {:a 2}]
(sc/coerce ::seq (list {:a 1} {:a 2})) ;; => ({:a 2} {:a 1})

The reason is that into uses (reduce conj ... https://github.com/wilkerlucio/spec-coerce/blob/master/src/spec_coerce/core.cljc#L150

I'd fix it with something like this:

(defn- map-seq
  [fun x]
  (if (vector? x)
    (mapv fun x)
    (map fun x)))

(defn parse-coll-of [[_ pred & _]]
  (fn [x]
    (if (sequential? x)
      (map-seq (partial coerce pred) x)
      x)))

What do you think?

Nilables don't work when specs are used

Hej,
thanks for creating this great library.

I've found inconsistent behaviour when using nilables.

(s/def ::nilable-int (s/nilable int?))
(sc/coerce ::nilable-int "10") ;; -> 10 works as expected

Unfortunately this doesn't work:

(s/def ::int int?)
(s/def ::nilable-int (s/nilable ::int))
(sc/coerce ::nilable-int "10") ;; -> "10" eeek

I'm attaching a patch that fixes this bug.

Support for multi-specs?

Perhaps I'm missing something, but it seems from my experimentation that this library doesn't support coercion of multi-specs.

For example given a multi-spec such as:

(spec/def ::event (spec/multi-spec event ::type)

It'd be really awesome if I could do the following:

(sc/coerce ::event {:type :some-event-type :some-attribute-to-be-coerced "123"}

and have the event data be coerced based on the multi-methods I have defined for the multi-spec.

Is there anything that prevents this or would it be possible to implement?

parse-long and parse-double should cast numeric types

Right now, if given an int, parse-double will return the integer unchanged. This causes coerce! to throw. It's easy enough to check for number? and just cast to the desired type.

PR incoming with my proposed solution.

Regex cat spec doesn't seem to be coerced at all

Suppose I have a spec registry like this:

(s/def ::arg (s/or :int int? :boolean boolean? :keyword keyword? :string string?))
(s/def ::evt-kw keyword?)
(s/def ::arg-list (s/cat :evt-kw ::evt-kw :evt-args (s/* ::arg)))

Then (sc/coerce ::evt-kw ":test") returns :test (a keyword) as expected, but (sc/coerce ::arg-list [":test"]) returns a vector with a string in it, coerce! throws, and conform returns ::s/invalid. Is there any way to make use of the spec regex ops?

coerce-structure overrides nested unqualified keys support

I was wondering if we should add support for nested keys path in coerce-structure to allow fine grained targeting of keys? What do you think? (it should be quite easy to add)

(coerce-structure {:a {:b [{:a "1"}]}} {::sc/overrides {[:a :b :a] ::number})` -> {:a {:b {:a [1]}}}

or something more pull expression like:

(coerce-structure {:a {:b [{:a "1"}]}} {::sc/overrides {{:a {:b [:a]}} ::number})` -> {:a {:b {:a [1]}}}

inst? coerce seems very strict

We currently use conformers to provide API -> data coercion as part of a ::date spec and we accept several string formats:

           ["yyyy/M/d" "M/d/yyyy"
            "yyyy-M-d" "M-d-yyyy"
            "EEE MMM dd HH:mm:ss zzz yyyy"]

I was hoping that (sc/coerce inst? input)would be equally forgiving :) I was a bit surprised that even"2018-7-23"wasn't accepted (needs to be"2018-07-23"`).

I know I could write a custom coercion but I wondered whether you've considered supporting more input formats for date coercion?

Coercion for Decimal doesn't work as expected

Hi,
thank you for this library.

When I get a number from e.g. Ring request I'd like to coerce it to Big Decimal.

(s/def ::amount (s/and decimal? pos?))
(sc/coerce ::amount "") ;; throws java.lang.ArrayIndexOutOfBoundsException
;; but I expect to get "", because the value is not possible to coerce

(sc/coerce ::amount 1) ;; it returns 1
;; but I expect to get 1M, because the value is Long 

So I suggest to:

  1. parse-decimal function should not care if x is string or not.
  2. parse-decimal should be wrapped in try-catch.

What do you think about it?

Coercion to string has no effect

If a predicate string? is used for a spec, coerce does not coerce to a string.

(s/def ::foo string?)
(sc/coerce ::foo 1)  ;=> 1

'coerce-structure' should accept a spec as an argument, like 'coerce'

I'm just starting with spec, so I might be missing something,..

I think coerce-structure should accept a spec as an argument in order to be able to handle unqualified maps.

(s/keys :req-un` [::first-name ::last-name])
{:first-name "Bob" :last-name "Smith"} ;; <---- can't be coerced

1.0.0-alpha15 doesn't compile in CLJS

Could have been caught with #13

------ ERROR -------------------------------------------------------------------
 File: jar:file:/Users/kszabo/.m2/repository/spec-coerce/spec-coerce/1.0.0-alpha15/spec-coerce-1.0.0-alpha15.jar!/spec_coerce/core.cljc:179:11
--------------------------------------------------------------------------------
 176 |
 177 | (defn parse-multi-spec
 178 |   [[_ f retag & _]]
 179 |   (let [f (resolve f)]
-----------------^--------------------------------------------------------------
Encountered error when macroexpanding cljs.core/resolve.
AssertionError: Assert failed: Argument to resolve must be a quoted symbol
(core/and (seq? quoted-sym) (= (quote quote) (first quoted-sym)))

add support for s/merge

Some implementation notes:

  • if we follow the generative model, s/merge should merge all the resolved s/keys arguments, unlike say s/and
  • s/merge can take s/keys, but they can be also sourced from s/multi-spec (#31)
  • we cannot just do a reduce + coerce on arguments as coerce will return coerced elements + the rest, we need to resolve the leaf s/keys (the same way the s/keys parser does now) and merge the :req/:opts, essentially generating an equivalent of a single s/keys (which could be an impl. possibility actually) and then call coerce on it.

`spec/and` precludes coercion

Hi,

I found out that spec/and appears to preclude coercion:

(spec/def ::foo (spec/and any? integer?))

(spec-coerce/coerce `integer? "42") ;; does its job correctly: returns the number 42

(spec-coerce/coerce ::foo "42")     ;; returns the string "42"

WDYT?

Thanks - Victor

is there a function that raise an exception when coerce fail?

Personally I think it's a common usage to do coerce and assert together. what I'm looking for is a function do the following but won't walk the data structure twice:

(s/def ::number int?)

(defn the-fn [k x]
  (->> (sc/coerce k x)
       (s/assert* k)))

(the-fn ::number 10)     ;; => 10
(the-fn ::number "10")   ;; => 10
(the-fn ::number "10.1") ;; => exception

Is support for s/nilable on the roadmap?

I tried (s/def ::maybe-int (s/nilable int?)) and (sc/coerce ::maybe-int "42") and got "42" back. I realized that I could use a long form spec with s/or but then I need to unconfirm the result.

(s/def ::maybe-int (s/or :nil nil? :int int?))
(sc/coerce ::maybe-int "42") => 42 ;; yay!

Behavior for specs defined as set literals

Hi, thanks for the library!

Given this spec:

(spec/def ::orientation #{:north :south :east :west})

...spec-coerce cannot coerce "north" into :north.

Is that expected behavior?

One trickier case would be (spec/def ::foo #{:north 1 "s"}). OTOH such an heterogeneous set would be probably universally bad - spec/or should be used instead.

Cheers - Victor

First class coercion contexts

Hi,

It's not really an issue, more a question.
First of all thanks for the library, it's quite useful.

Would you see an interest in having reifiable coercion contexts, basically an extra/optional argument to coercion fns that would allow to specify per call coercers. Something similar to Schema coercion-matchers?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.