Skip to content

Instantly share code, notes, and snippets.

@saikyun
Last active January 31, 2020 11:10
Show Gist options
  • Save saikyun/44ac065634b78a7c70cb68b37fd0bd1b to your computer and use it in GitHub Desktop.
Save saikyun/44ac065634b78a7c70cb68b37fd0bd1b to your computer and use it in GitHub Desktop.
basic edn validation for clj/cljs
(ns example.edn-validator)
;; Validator to be used when getting edn-data from a stream
;; if you straigt up run `edn/parse` you get exceptions on
;; data that isn't well formed.
;; Validate tries to parse whatever data you've got right now
;; and provides information about wether the data is well formed
;; or not. Check the bottom for example code.
;; The kinds of structures in our edn
;; Can easily be extended by adding more structures
;; `:state` changes the validation meta, so when
;; inside a string `:in-str` will be true in validation meta
;; and since `:ignore` is true, it will ignore other structures
;; while it's in that state
(def kinds
#{{:open "\"", :close "\"", :state {:kind :in-str
:ignore true}}
{:open "{", :close "}"}
{:open "(", :close ")"}
{:open "[", :close "]"}})
(def open (into {} (map (fn [k] [(:open k) k]) kinds)))
(def close (into {} (map (fn [k] [(:close k) k]) kinds)))
(def ignore (into {} (filter (comp :ignore first) (map (fn [k] [(:state k) k]) kinds))))
(defn opened-structure?
"Returns true, if the validation meta has opened at least one structure."
[vs]
(seq (map (into #{} (map :close kinds)) vs)))
(defn ignore?
"Returns true if the validation meta currently ignoring or not.
E.g. when inside a string this will return true."
[vs]
(seq (map #(ignore (key %)) (filter second (:states vs)))))
(defn unclosed-pair?
"Returns true if the validation meta contains an unclosed pair."
[vs]
(letfn [(unclosed? [[kind amount]]
(and ((into #{} (map :close kinds)) kind)
(< 0 amount)))]
(seq (filter unclosed? vs))))
(defn validate
"Validates partial edn-strings.
Returns a structure looking like so:
{:well-formed \"...\" ;; validated edn
:remaining \"...\" ;; wasn't validated, because the `s`tring
;; contained more than one edn value
;; can be sent to `validate` to keep validating
:bad \"...\" ;; data that didn't validate, check `:state` to
;; get more information about why
:meta {} ;; contains information about unclosed openers (e.g. \"#{1\")
;; and if a structure `:state` is active, e.g. when inside a string,
;; this will contain information about ignoring other structures
;; most relevant when you get `:bad` data
;; check out the examples at the bottom to get a better understanding of this
}"
[s]
(loop [[c & cs] s
validation-meta {}
validated nil]
(let [c (str c)
ignore (ignore? validation-meta)
opened (and (or (not ignore)
((into #{} (map :open ignore)) c))
(open c))
closed (and (or (not ignore)
((into #{} (map :close ignore)) c))
(close c))
[opened closed] (if (and opened closed)
(if (and (validation-meta (:close opened))
(< 0 (validation-meta (:close opened))))
[nil closed]
[opened nil])
[opened closed])]
(cond (and closed
(or (nil? (validation-meta (:close closed)))
(>= 0 (validation-meta (:close closed)))))
{:bad s
:meta (update validation-meta (:close closed) (fnil dec 0))}
(and (opened-structure? validation-meta)
(not (unclosed-pair? validation-meta)))
(if (= c "")
{:well-formed validated}
{:well-formed validated
:remaining (apply str c cs)})
(= c "")
(if (not (unclosed-pair? validation-meta))
{:well-formed validated
:meta validation-meta}
{:bad validated
:meta validation-meta})
:else
(cond
opened (recur cs (-> validation-meta
(update (:close opened) (fnil inc 0))
(#(if-let [s (:state opened)]
(update % :states assoc s true)
%)))
(str validated c))
closed (recur cs (-> validation-meta
(update (:close closed) (fnil dec 0))
(#(if-let [s (:state closed)]
(update % :states assoc s false)
%)))
(str validated c))
:else (recur cs validation-meta (str validated c)))))))
(comment
;; sanity checks
(validate "{")
(validate nil)
(validate "\n:hej")
(validate "\n{}")
;; should work as expected
(validate "{:tag :tap, :val \":yo}\"}")
;; same as above, but with an unfinished form afterwards
(validate "{:tag :tap, :val \":yo}\"}\n{:a")
;; should not work (no closing })
;; positive values in `:meta` reflects too many openers
(validate "{:tag :tap, :val \":yo}\"")
;; throws exception, too many closing }
;; negative values in `:meta` reflects too many closers
(validate "{:tag :tap, :val \":yo}\"}}}")
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment