Skip to content

Instantly share code, notes, and snippets.

@aphyr
Created November 21, 2018 20:20
Show Gist options
  • Save aphyr/8c75635d57fbb80da4f9a69f19ff9eb1 to your computer and use it in GitHub Desktop.
Save aphyr/8c75635d57fbb80da4f9a69f19ff9eb1 to your computer and use it in GitHub Desktop.
; 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