Skip to content

Instantly share code, notes, and snippets.

@favila
Created April 5, 2016 00:24
Show Gist options
  • Save favila/c8bd1eddb8263e53ba93ae3a60fd1bb7 to your computer and use it in GitHub Desktop.
Save favila/c8bd1eddb8263e53ba93ae3a60fd1bb7 to your computer and use it in GitHub Desktop.
Utilities for making it easier to read edn files. Also includes tag readers that datomic uses.
(ns favila.read-edn.tag-readers
"Common tag-reader maps for edn."
(:require datomic.db
datomic.function
datomic.codec)
(:import java.net.URI))
(defmethod print-method URI [^URI x ^Writer w]
(doto w
(.write "#uri ")
(.write "\"")
;; java.net.URI does not appear to allow unescaped
;; double-quotes, so this should be safe.
(.write (.toString x))
(.write "\"")))
;; Discovered by examining *data-readers* with datomic in the classpath.
(def datomic
"Read edn or dtm files with datomic tag literals in them. Has the same set
of tags (plus #uri) as understood by datomic.Util/readAll."
{'db/id datomic.db/id-literal
'db/fn datomic.function/construct
'base64 datomic.codec/base-64-literal
'uri #(URI. ^String %)})
(ns favila.read-edn
"A namespace that makes reading edn a little easier.
Takes care of PushbackReader coersion, supplies a `TaggedValue` record for
unknown tags, `edn-form-reader` for reading successive forms from edn using
the same handler options, and easy-to-use functions to lazily seq edn objects
out of anything `clojure.java.io/reader` understands.
Example use:
(def results (read-all-edn-string
\"#uri \"\"http://example.org\"\"
#unknown [:a :b]\"
{'uri #(URI. ^String %)
:default tagged-value}))
=> #'favila.read-edn/results
(first results)
=> #object[java.net.URI 0x1ee736ad \"http://example.org\"]
(second results)
=> #unknown[:a :b]
(type (second results))
=> favila.read_edn.TaggedValue
(:tag (second results))
=> unknown
(:value (second results))
=> [:a :b]"
(:require clojure.edn
[clojure.java.io :as io])
(:import [java.io Writer Reader PushbackReader StringReader]))
(defn pushback-reader
"Return x coerced to a PushbackReader. Understands anything
clojure.java.io/reader understands plus PushbackReader itself (which is
returned unchanged)."
^PushbackReader [x]
(cond (instance? PushbackReader x) x
(instance? Reader x) (PushbackReader. x)
:else (PushbackReader. (io/reader x :encoding "UTF-8"))))
(defn pushback-reader-string ^PushbackReader [s]
"Return a PushbackReader wrapping the contents of string s."
(PushbackReader. (StringReader. s)))
(let [eof (reify Object (toString [_] "edn EOF"))]
(defn edn-form-reader [^PushbackReader pbr tag-readers]
"Return a function which returns successive edn objects from pbr.
When EOF is reached, returns a sentinal which can be tested for with
`edn-eof?`."
(let [opts {:eof eof
:readers (dissoc tag-readers :default)
:default (get tag-readers :default)}]
#(clojure.edn/read opts pbr)))
(defn- edn-eof?
"Return true if x is an edn EOF marker, else false."
[x]
(identical? eof x)))
(defrecord TaggedValue [tag value])
(defmethod print-method TaggedValue [this ^Writer w]
(.write w "#")
(print-method (:tag this) w)
(.write w " ")
(print-method (:value this) w))
(defn tagged-value
"Given a tag symbol and a value, return a TaggedValue record which
holds the tag under the :tag key and the unmodified value under :value.
This is useful as a catch-all default tag reader because it allows unknown
tags to be roundtripped."
[tag value]
{:pre [(symbol? tag)]}
(->TaggedValue tag value))
(defn read-all-edn
"Given something clojure.java.io/reader understands and which contains edn,
return a lazy sequence of the objects read from the edn.
`tag-readers` is an optional map of tag symbols to tag readers
(1-arg functions which take the object read after the tag and return an
object). It may also have the key `:default` for a default tag handler: a
2-arg function taking tag-symbol and form and returning an object.
Note: this function always closes the underlying reader automatically when
the edn is exhausted. If you need more control, use `edn-form-reader` and
`edn-eof?` directly."
([readable] (read-all-edn readable {}))
([readable tag-readers]
(let [pbr (pushback-reader readable)
rff (edn-form-reader pbr tag-readers)]
(take-while #(if (edn-eof? %)
(do (.close pbr) false)
true)
(repeatedly rff)))))
(defn read-all-edn-string
"Like read-all-edn except first argument must be a string containing edn.
Accepts the same tag-readers map as read-all-edn."
([s] (read-all-edn-string s {}))
([s tag-readers] (read-all-edn (pushback-reader-string s) tag-readers)))
(defn read-one-edn
"Like read-all-edn, but returns only the next form out of the reader. Returns
`nil` if there are no forms to read. Always closes the readable."
([readable] (read-one-edn readable {}))
([readable tag-readers]
(with-open [pbr (pushback-reader readable)]
(let [form ((edn-form-reader pbr tag-readers))]
(if (edn-eof? form)
nil
form)))))
(defn read-one-edn-string
"Like read-one-edn except first argument must be a string containing edn."
([s] (read-one-edn-string s {}))
([s tag-readers] (read-one-edn (pushback-reader-string s) tag-readers)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment