Last active
January 31, 2020 11:10
-
-
Save saikyun/44ac065634b78a7c70cb68b37fd0bd1b to your computer and use it in GitHub Desktop.
basic edn validation for clj/cljs
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
(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