Skip to content

Instantly share code, notes, and snippets.

@ikitommi
Last active July 10, 2019 09:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save ikitommi/17602f0d08f754f89a4c6a029d8dd47e to your computer and use it in GitHub Desktop.
Save ikitommi/17602f0d08f754f89a4c6a029d8dd47e to your computer and use it in GitHub Desktop.
Schema coercion, how to do this with spec?
; (./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))
; (./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)))
@kenrestivo
Copy link

Had to do the following to get it to compile with 1.9.0-alpha14

https://gist.github.com/kenrestivo/75bdee513b1704be1a743b5558774f84#file-with-spec-clj-L54

@kenrestivo
Copy link

I'm told that this has been deprecated anyway in favor of
https://github.com/metosin/spec-tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment