Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transformers #116

Merged
merged 21 commits into from
May 8, 2018
Merged

Transformers #116

merged 21 commits into from
May 8, 2018

Conversation

ikitommi
Copy link
Member

@ikitommi ikitommi commented May 6, 2018

  • new st/IntoSpec protocol to convert non-recursively vanilla clojure.spec Specs into st/Spec:s. Used in st/encode, st/decode, st/explain, st/explain-data, st/conform and st/conform!.
  • BREAKING: Bye bye conforming, welcome transformers!
    • Guide: https://github.com/metosin/spec-tools#spec-driven-transformations
    • removed: st/type-conforming, st/json-conforming, st/string-conforming
    • new st/Transformer protocol to drive spec-driven value transformations
    • spec values can be both encoded (st/encode) & decoded (st/decode) using a transformer, fixes #96.
    • renamed ns spec-tools.conform into spec-tools.transform, covering both encoding & decoding of values
    • st/type-transformer, supporting both :type and Spec level transformations
    • Spec-driven transformations via keys in encode and decode namespaces.
    • st/encode, st/decode, st/explain, st/explain-data, st/conform and st/conform! take the transformer instance an optional third argument
      • st/json-transformer and st/string-transformer are shipped out-of-the-box

Transformer

(defprotocol Transformer
  (-name [this])
  (-encoder [this spec value])
  (-decoder [this spec value]))

Spec-driven transformations

  • use :encode/* and :decode/* keys from Spec instances to declare how the values should be transformed
(require '[clojure.spec.alpha :as s])
(require '[clojure.string :as str])

(s/def ::spec
  (st/spec
    {:spec #(and (simple-keyword? %) (-> % name str/lower-case keyword (= %)))
     :description "a lowercase keyword, encoded in uppercase in string-mode"
     :decode/string #(-> %2 name str/lower-case keyword)
     :encode/string #(-> %2 name str/upper-case)}))

(st/decode ::spec :kikka)
; :kikka

(as-> "KiKka" $
      (st/decode ::spec $))
; :clojure.spec.alpha/invalid

(as-> "KiKka" $
      (st/decode ::spec $ st/string-transformer))
; :kikka

(as-> "KiKka" $
      (st/decode ::spec $ st/string-transformer)
      (st/encode ::spec $))
; :clojure.spec.alpha/invalid

(as-> "KiKka" $
      (st/decode ::spec $ st/string-transformer)
      (st/encode ::spec $ st/string-transformer))
; "KIKKA"

Spec Bijections?

no, as there can be multiple valid representations for a encoded value. But it's quaranteed that a decoded values X is always encoded into Y, which can be decoded back into X, y -> X -> Y -> X

(as-> "KikKa" $
      (doto $ prn)
      (st/encode ::spec $ st/string-transformer)
      (doto $ prn)
      (st/decode ::spec $ st/string-transformer)
      (doto $ prn)
      (st/encode ::spec $ st/string-transformer)
      (prn $))
; "KikKa"
; "KIKKA"
; :kikka

Type-driven transformations

  • use :type information from Specs (mostly resolved automatically)
(require '[spec-tools.core :as st])

(as-> "2014-02-18T18:25:37Z" $
      (st/decode inst? $))
; :clojure.spec.alpha/invalid

;; decode using string-transformer
(as-> "2014-02-18T18:25:37Z" $
      (st/decode inst? $ st/string-transformer))
; #inst"2014-02-18T18:25:37.000-00:00"

(as-> "2014-02-18T18:25:37Z" $
      (st/decode inst? $ st/string-transformer)
      (st/encode inst? $))
; :clojure.spec.alpha/invalid

;; encode using string-transformer
(as-> "2014-02-18T18:25:37Z" $
      (st/decode inst? $ st/string-transformer)
      (st/encode inst? $ st/string-transformer))
; "2014-02-18T18:25:37.000+0000"

:type gives you encoders & decoders (and docs) for free, like Data.Unjson:

(s/def ::kw
  (st/spec
    {:spec #(keyword %) ;; anonymous function
     :type :keyword}))  ;; encode & decode like a keyword

(st/decode ::kw "kikka" st/string-transformer)
;; :kikka

(st/decode ::kw "kikka" st/json-transformer)
;; :kikka

@coveralls
Copy link

coveralls commented May 6, 2018

Coverage Status

Coverage increased (+0.1%) to 97.476% when pulling 2ee54e7 on transformers into 311a0e6 on master.

Copy link
Contributor

@f-f f-f left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks very good, I really like the idea of defining an explicit interface for the encoding/decoding (the Transformer protocol), makes the whole process clearer 👍

CHANGELOG.md Outdated
(st/encode ::spec $))
; :clojure.spec.alpha/invalid

(as-> "KiKka" $
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep here the comment about bijections? (first time I read it I thought it was a nice disclaimer)

(catch #?(:clj Exception, :cljs js/Error) _ x))
x))

(defn date->string [_ x]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function and keyword->string above are missing tests. Also, it would be very nice to have tests that do the roundtrip, that would help showing/uncover cases in which the "bijection" does not hold.

@ikitommi
Copy link
Member Author

ikitommi commented May 8, 2018

new tests, fixes and docs

@arichiardi
Copy link
Contributor

I also like it, I was wondering if I could address the json schema to spec round trip with this new addition

Copy link
Contributor

@miikka miikka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it in general. Glad to move away from calling it "conforming".

CHANGELOG.md Outdated

(as-> "KiKka" $
(st/decode ::spec $ st/string-transformer)
(st/encode ::spec $))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what is supposed to happen here. If (st/encode ::spec $) won't ever do anything useful, should it have a 3-ary version at all?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true. 2-arity encode doesn't do anything.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I meant 2, not 3

@mgrbyte
Copy link

mgrbyte commented May 8, 2018

I also like the look of this in general, although I've not used the underlying "confiorming" libraries directly, outside of debugging stuff.

a (very minor) naming nit-pick:
Initially I was 💯 on the changing the name of the concept to "transformer", but the protocol has encode and decode "methods", implying of course, that all transforms should be bidirectional.
Since a "transform" (in my head anyway) can be uni-directional (lossy), perhaps "codec" is a better name?

@ikitommi
Copy link
Member Author

ikitommi commented May 8, 2018

codec vs transformer?

hmm.

trans

@ikitommi
Copy link
Member Author

ikitommi commented May 8, 2018

I'll merge this anyway. Will not freeze before name is resolved.

@ikitommi ikitommi merged commit affef27 into master May 8, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Easier way to add custom conforming
6 participants