Last active
July 10, 2019 09:05
-
-
Save ikitommi/17602f0d08f754f89a4c6a029d8dd47e to your computer and use it in GitHub Desktop.
Schema coercion, how to do this with spec?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; (./pull '[prismatic/schema "1.1.3"]) | |
(require '[schema.core :as schema]) | |
(require '[schema.coerce :as coerce]) | |
;; let's define some matchers | |
(def matchers | |
{:string coerce/string-coercion-matcher ;; used with ring query-, path- & form-params | |
:json coerce/json-coercion-matcher ;; used with body/response for "application/json" | |
:edn (constantly nil)}) ;; used with body/response for "application/edn" | |
;; naive conform with schema, conformers/matchers are | |
;; based on type, not instances | |
(defn conform [schema type value] | |
(let [matcher (matchers type (::default matchers)) | |
coercer (coerce/coercer schema matcher)] | |
(coercer value))) | |
;; a sample schema | |
(def Cake {:eggs Long | |
:toppings #{(schema/enum :candy :cream :blueberry)}}) | |
;; eggs=12&toppings=candy&toppings=cream | |
(def string-model {:eggs "12", :toppings ["candy" "cream"]}) | |
;; "{\"eggs\":12,\"toppings\":[\"candy\",\"cream\"]}" | |
(def json-model {:eggs 12, :toppings ["candy" "cream"]}) | |
;; "{:eggs 12, :toppings #{:candy :cream}}" | |
(def edn-model {:eggs 12, :toppings #{:candy :cream}}) | |
;; We can conform Cakes, cool! | |
(conform Cake :string string-model) | |
; => {:eggs 12, :toppings #{:candy :cream}} | |
;; with string-based formats, everything is converted | |
(assert (= (conform Cake :string edn-model) edn-model)) | |
(assert (= (conform Cake :string json-model) edn-model)) | |
(assert (= (conform Cake :string string-model) edn-model)) | |
;; json-matcher doesn't do string->number conversion (json has numbers) | |
(assert (= (conform Cake :json edn-model) edn-model)) | |
(assert (= (conform Cake :json json-model) edn-model)) | |
(assert (not= (conform Cake :json string-model) edn-model)) | |
;; edn-matcher requires everything as-is ( | |
(assert (= (conform Cake :edn edn-model) edn-model)) | |
(assert (not= (conform Cake :edn json-model) edn-model)) | |
(assert (not= (conform Cake :edn string-model) edn-model)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; (./pull '[org.clojure/clojure "1.9.0-alpha10"]) | |
(require '[clojure.spec :as s]) | |
(def ^:dynamic *conform-mode* nil) | |
(defn string->int [x] | |
(if (string? x) | |
(try | |
(Integer/parseInt x) | |
(catch Exception _ | |
:clojure.spec/invalid)))) | |
(defn string->long [x] | |
(if (string? x) | |
(try | |
(Long/parseLong x) | |
(catch Exception _ | |
:clojure.spec/invalid)))) | |
(defn string->double [x] | |
(if (string? x) | |
(try | |
(Double/parseDouble x) | |
(catch Exception _ | |
:clojure.spec/invalid)))) | |
(defn string->keyword [x] | |
(if (string? x) | |
(keyword x))) | |
(defn string->boolean [x] | |
(if (string? x) | |
(cond | |
(= "true" x) true | |
(= "false" x) false | |
:else :clojure.spec/invalid))) | |
(def +string-conformers+ | |
{::int string->int | |
::long string->long | |
::double string->double | |
::keyword string->keyword | |
::boolean string->boolean}) | |
(def +conform-modes+ | |
{::string [string? +string-conformers+]}) | |
(defn dynamic-conformer [accept? type] | |
(with-meta | |
(s/conformer | |
(fn [x] | |
(if (accept? x) | |
x | |
(if-let [[accept? conformers] (+conform-modes+ *conform-mode*)] | |
(if (accept? x) | |
((type conformers) x) | |
:clojure.spec/invalid) | |
:clojure.spec/invalid)))) | |
{::type type})) | |
;; Type'ish | |
(def aInt (dynamic-conformer integer? ::int)) | |
(def aBool (dynamic-conformer boolean? ::boolean)) | |
(def aLong (dynamic-conformer boolean? ::long)) | |
(def aKeyword (dynamic-conformer boolean? ::keyword)) | |
;; Schema | |
(s/def ::age (s/and aInt #(> % 10))) | |
(s/def ::truth aBool) | |
(s/def ::over-million (s/and aLong #(> % 1000000))) | |
(s/def ::language (s/and aKeyword #{:clojure :clojurescript})) | |
;; Default mode | |
(assert (= (s/conform ::age "12") :clojure.spec/invalid)) | |
(assert (= (s/conform ::truth "false") :clojure.spec/invalid)) | |
(assert (= (s/conform ::over-million "1234567") :clojure.spec/invalid)) | |
(assert (= (s/conform ::language "clojure") :clojure.spec/invalid)) | |
;; With string-coercions | |
(binding [*conform-mode* ::string] | |
(assert (= (s/conform ::truth "false") false)) | |
(assert (= (s/conform ::over-million "1234567") 1234567)) | |
(assert (= (s/conform ::language "clojure") :clojure)) | |
(assert (= (s/conform ::age "12") 12))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I'm told that this has been deprecated anyway in favor of
https://github.com/metosin/spec-tools