diff --git a/README.md b/README.md index 7dbe315..ca1359e 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ merged with the internal registry at coerce time. ```clj (s/def ::foo keyword?) -(c/coerce ::foo "bar" {::c/idents {::foo (fn [x opts] (str "keyword:" x))}}) -> "keyword:bar" +(c/coerce ::foo "bar" {:idents {::foo (fn [x opts] (str "keyword:" x))}}) -> "keyword:bar" ``` Coercers are functions of 2 args, the value, and the options coerce @@ -60,19 +60,19 @@ The typical example would be : ```clj (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] +(c/coerce ::foo ["a" "b"] {: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 +behavior of coercer generators via :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. ```clj (c/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)))}}) + {:forms {`s/coll-of (fn [[_ spec]] (fn [x opts] (do-something-crazy-with-spec+the-value spec x opts)))}}) ``` ### Closed maps @@ -169,7 +169,7 @@ Learn by example: (c/coerce-structure {::number "42" ::not-defined "bla" :sub {::odd-number "45"}} - {::c/idents {::not-defined `keyword? + {:idents {::not-defined `keyword? ; => {::number 42 ; ::not-defined :bla ; :sub {::odd-number 45}} @@ -182,7 +182,7 @@ Learn by example: ; 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 +(c/coerce ::my-custom-attr "Z") {:idents {::my-custom-attr keyword}}) ; => :Z ``` Examples from predicate to coerced value: @@ -259,10 +259,10 @@ Examples from predicate to coerced value: (c/conform ::number "40") ; 40 ;; Throw on coerce structure -(c/coerce-structure {::number "42"} {::c/op c/coerce!}) +(c/coerce-structure {::number "42"} {:op c/coerce!}) ;; Conform on coerce structure -(c/coerce-structure {::number "42"} {::c/op c/conform}) +(c/coerce-structure {::number "42"} {:op c/conform}) ``` ## Caching @@ -278,11 +278,10 @@ about this, for instance when you define static coercers via 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 {})`. +In any case you can turn off the cache by passing `: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 diff --git a/VERSION b/VERSION index 62da8d8..155b9cb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.3-SNAPSHOT \ No newline at end of file +2.0.0-SNAPSHOT \ No newline at end of file diff --git a/src/exoscale/coax.cljc b/src/exoscale/coax.cljc index c391ad3..c9868bd 100644 --- a/src/exoscale/coax.cljc +++ b/src/exoscale/coax.cljc @@ -127,14 +127,20 @@ (defn gen-coerce-merge [[_ & spec-forms]] - (fn [x {:as opts :keys [closed]}] + (fn [x opts] (if (map? x) - (into (if closed {} x) - (map (fn [spec-form] - (coerce spec-form - x - (assoc opts :closed true)))) - spec-forms) + (reduce (fn [m spec-form] + ;; for every spec-form coerce to new value; + ;; we need to compare key by key what changed so that + ;; defaults do not overwrite coerced values + (into m + (keep (fn [[spec v]] + ;; new-val doesn't match default, keep it + (when-not (= (get x spec) v) + [spec v]))) + (coerce spec-form x (assoc opts :closed true)))) + x + spec-forms) :exoscale.coax/invalid))) (defn gen-coerce-nilable @@ -265,15 +271,11 @@ indicates a spec form likely it will return it's generated coercer from registry :exoscale.coax/forms, otherwise it returns the identity coercer" - [spec {:as opts :exoscale.coax/keys [enums]}] + [spec {:as _opts :keys [idents forms enums]}] (let [spec-exp (si/spec-root spec) {:as reg :exoscale.coax/keys [idents]} (-> @registry-ref - (update :exoscale.coax/idents - merge - (:exoscale.coax/idents opts)) - (update :exoscale.coax/forms - merge - (:exoscale.coax/forms opts)) + (update :exoscale.coax/idents merge idents) + (update :exoscale.coax/forms merge forms) (cond-> enums (assoc :exoscale.coax/enums enums)))] (or (cond (qualified-ident? spec-exp) @@ -292,7 +294,7 @@ "Get the coercing function from a given key. First it tries to lookup the coercion on the registry, otherwise try to infer from the specs. In case nothing is found, identity function is returned." - [spec {:exoscale.coax/keys [idents] :as opts}] + [spec {:keys [idents] :as opts}] (or (when (qualified-keyword? spec) (si/registry-lookup (merge (:exoscale.coax/idents @registry-ref) idents) @@ -315,7 +317,7 @@ (defn coerce-fn [spec opts] - (if (:exoscale.coax/cache opts true) + (if (:cache opts true) (cached-coerce-fn spec opts) (coerce-fn* spec opts))) @@ -415,7 +417,7 @@ (defn coerce-structure "Recursively coerce map values on a structure." ([x] (coerce-structure x {})) - ([x {:exoscale.coax/keys [idents op] + ([x {:keys [idents op] :or {op coerce} :as opts}] (walk/prewalk (fn [x] @@ -427,14 +429,3 @@ [k (op (get idents k k) v opts)] [k v])))))) x))) - -(s/def ::foo string?) -(s/def ::bar string?) -(s/def ::z string?) - -(s/def ::m (s/keys :req-un [::foo ::bar])) - -(coerce ::m {:foo "f" :bar "b" :baz "x"} {:closed true}) ; baz is not on the spec - -(coerce `(s/merge ::m (s/keys :req-un [::z])) - {:foo "f" :bar "b" :baz "x" :z "z"} {:closed true}) ; baz is not on the spec diff --git a/test/exoscale/coax_test.cljc b/test/exoscale/coax_test.cljc index 5c85261..23c3fd6 100644 --- a/test/exoscale/coax_test.cljc +++ b/test/exoscale/coax_test.cljc @@ -86,7 +86,7 @@ (is (= (sc/coerce ::infer-nilable nil) nil)) (is (= (sc/coerce ::infer-nilable "") "")) (is (= (sc/coerce ::nilable-int "10") 10)) - (is (= (sc/coerce ::nilable-int "10" {::sc/idents {`int? (fn [x _] (keyword x))}}) :10)) + (is (= (sc/coerce ::nilable-int "10" {:idents {`int? (fn [x _] (keyword x))}}) :10)) (is (= (sc/coerce ::nilable-pos-int "10") 10)) (is (= (sc/coerce ::nilable-string nil) nil)) @@ -287,13 +287,13 @@ ::not-defined "bla" :unqualified 12 :sub {::infer-int "42"}} - {::sc/idents {::not-defined `keyword?}}) + {:idents {::not-defined `keyword?}}) {::some-coercion 321 ::not-defined :bla :unqualified 12 :sub {::infer-int 42}})) (is (= (sc/coerce-structure {::or-example "321"} - {::sc/op sc/conform}) + {:op sc/conform}) {::or-example [:int 321]})) (defrecord SomeRec [a]) @@ -344,22 +344,22 @@ ::legs ["7" "7"] :foo "bar" :name "john"} - {::sc/idents + {:idents {::head c/to-long ::leg c/to-long ::name c/to-keyword}})) "Coerce with option form") - (is (= 1 (sc/coerce `string? "1" {::sc/idents {`string? c/to-long}})) + (is (= 1 (sc/coerce `string? "1" {:idents {`string? c/to-long}})) "overrides works on qualified-idents") (is (= [1] (sc/coerce `(s/coll-of string?) ["1"] - {::sc/idents {`string? c/to-long}})) + {:idents {`string? c/to-long}})) "overrides works on qualified-idents, also with composites") (is (= ["foo" "bar" "baz"] (sc/coerce `vector? "foo,bar,baz" - {::sc/idents {`vector? (fn [x _] (str/split x #"[,]"))}})) + {:idents {`vector? (fn [x _] (str/split x #"[,]"))}})) "override on real world use case with vector?"))) (s/def ::foo int?) @@ -399,7 +399,7 @@ (s/def ::merge (s/merge (s/keys :req-un [::foo]) ::unqualified ;; TODO: add s/multi-spec test - )) + any?)) (is (= {:foo 1 :bar "1" :c {:a 2}} (sc/coerce ::merge {:foo "1" :bar 1 :c {:a 2}})) "Coerce new vals appropriately") @@ -430,7 +430,7 @@ (is (= {::x :y/quux} (sc/coerce ::mm {::x "quux"} - {::sc/cache? false})))) + {:cache? false})))) (def d :kw) ;; no vars in cljs