Created
November 21, 2018 20:20
-
-
Save aphyr/8c75635d57fbb80da4f9a69f19ff9eb1 to your computer and use it in GitHub Desktop.
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
; We use EDN as a serialization format in Clojure | |
user=> (require '[clojure.string :as str] '[clojure.edn :as edn]) | |
nil | |
; Defrecords are a common way to represent data | |
user=> (defrecord Star [hair body face]) | |
#<Class@28e8ba16 user.Star> | |
; Serializing a defrecord to EDN is easy | |
user=> (def s (pr-str (Star. "ga" "ga" "ooh la la"))) | |
#'user/s | |
user=> s | |
"#user.Star{:hair \"ga\", :body \"ga\", :face \"ooh la la\"}" | |
; But you can't read that string back; it's write-only. | |
user=> (edn/read-string s) | |
java.lang.RuntimeException: No reader function for tag user.Star | |
; You could write a custom deserializer for each tag (e.g. user.Star) | |
; but what if you don't know the datatypes you'll be asked to serialize with in advance? | |
(defn class-name->ns-str | |
"Turns a class string into a namespace string (by translating _ to -)" | |
[class-name] | |
(str/replace class-name #"_" "-")) | |
; Welll... let's introspect the tag at runtime and see if we have a corresponding defrecord | |
(defn edn-tag->constructor | |
"Takes an edn tag and returns a constructor fn taking that tag's value and | |
building an object from it." | |
[tag] | |
(let [c (resolve tag)] | |
(when (nil? c) | |
(throw (RuntimeException. (str "EDN tag " (pr-str tag) " isn't resolvable to a class") | |
(pr-str tag)))) | |
(when-not ((supers c) clojure.lang.IRecord) | |
(throw (RuntimeException. | |
(str "EDN tag " (pr-str tag) | |
" looks like a class, but it's not a record," | |
" so we don't know how to deserialize it.")))) | |
(let [; Translate from class name "foo.Bar" to namespaced constructor fn | |
; "foo/map->Bar" | |
constructor-name (-> (name tag) | |
class-name->ns-str | |
(str/replace #"\.([^\.]+$)" "/map->$1")) | |
constructor (resolve (symbol constructor-name))] | |
(when (nil? constructor) | |
(throw (RuntimeException. | |
(str "EDN tag " (pr-str tag) " looks like a record, but we don't" | |
" have a map constructor " constructor-name " for it")))) | |
constructor))) | |
; This is expensive, so we'll avoid doing it more than once | |
(def memoized-edn-tag->constructor (memoize edn-tag->constructor)) | |
; Now we can provide a default reader for unknown tags... | |
(defn default-edn-reader | |
"We use defrecords heavily and it's nice to be able to deserialize them." | |
[tag value] | |
(if-let [c (memoized-edn-tag->constructor tag)] | |
(c value) | |
(throw (RuntimeException. | |
(str "Don't know how to read edn tag " (pr-str tag)))))) | |
; Now you can round-trip any defrecord: | |
user=> (edn/read-string {:default default-edn-reader} s) | |
#user.Star {:body "ga" :face "ooh la la" :hair "ga"} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment