Skip to content

Instantly share code, notes, and snippets.

@nha
Created October 14, 2017 07:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nha/689016c1cf8c47de3c24a1e1d07ded12 to your computer and use it in GitHub Desktop.
Save nha/689016c1cf8c47de3c24a1e1d07ded12 to your computer and use it in GitHub Desktop.
(:require [clojure.string :as str]
[com.rpl.specter :refer [ALL FIRST LAST STAY AFTER-ELEM MAP-KEYS MAP-VALS recursive-path if-path collect-one traverse cond-path multi-path transform setval before-index index-nav]]
[schema-tools.core :as st]
[schema.core :as s]
[schema.spec.leaf])
(defn primitive-schema?
"do we have a better way?"
[v]
;;possible other approaches
;; - schema.spec.leaf.LeafSpec
;; - check that is has no sub-schemas and that is implements a schema.core
;; protocol (which one?)
;; - ikitommi
;; https://github.com/metosin/schema-tools/blob/master/src/schema_tools/walk.cljc
;; and https://github.com/metosin/schema-viz/blob/master/src/schema_viz/core.clj
(boolean
(and ;; (satisfies? s/Schema v)
(or
(some #(= v %) [s/Any s/Int s/Num s/Bool s/Str s/Symbol s/Uuid])
(some #(instance? % v)
[(type (s/enum)) ;; schema.core.EnumSchema
s/Regex
schema.core.Maybe
schema.core.One ;; s/optional
schema.core.Predicate])))))
(def ^:private PATH-SCHEMA-WALKER
(recursive-path [] p
(cond-path
(fn [v]
(instance? schema.core.ConditionalSchema v))
[(multi-path ALL (collect-one FIRST) LAST p)]
(fn [v]
(cond
(primitive-schema? v) false
(map? v) true ;; this is a real path
:default false))
[ALL (collect-one FIRST) LAST p]
(fn [v] true) STAY)))
(defn- reduce-schema-flat* [f m]
(reduce
f
{}
(traverse PATH-SCHEMA-WALKER m)))
(def ^:private schema-separator "_")
(def ^:private schema-separator-regex (re-pattern separator))
(defn key->str
"same as s/explicit-schema-key but does not error"
[k]
(cond (keyword? k) (name k)
(instance? schema.core.RequiredKey k) (name (.-k ^schema.core.RequiredKey k))
(s/optional-key? k) (name (.-k ^schema.core.OptionalKey k))
:default k))
(defn- make-optional [k]
(if (instance? schema.core.OptionalKey k)
k
(s/optional-key k)))
(defn flatten-schema*
"Transform a nested map into a seq of [keyseq leaf-val] pairs
adapted from plumatic.plumbing"
[m]
;; TODO see if specter can do it better (like above)
;; TODO see ic can remove all these concat
(when m
((fn flatten-helper [keyseq m]
(when m
(cond
(instance? schema.core.ConditionalSchema m) (let [[p1+s1 p2+s2] (:preds-and-schemas m)
;; undocumented internals,
;; we care about schemas, not predicates
left (second p1+s1)
right (second p2+s2)
;; make last of the keyseq optional,
;; since either condition could be true
opt-keyseq (transform [LAST] make-optional keyseq)]
(into (flatten-helper opt-keyseq left)
(flatten-helper opt-keyseq right)))
(primitive-schema? m) [[keyseq m]]
(map? m) (mapcat (fn [[k v]] (flatten-helper (conj keyseq k) v)) m)
:default [[keyseq m]])))
[] m)))
(defn flatten-schema
"note: this is probably rather slow, so use it once during boot/generation time"
[m]
;; NOTE: assumes m is a map TODO handle other cases
(let [flat-seq (->> (flatten-schema* m)
(map (fn [[path v]]
(let [str-path (apply str (interpose schema-separator (map key->str path)))
has-optional? (some s/optional-key? path)
final-path (if has-optional? (s/optional-key str-path) str-path)]
[final-path v]))))]
;; print a warning when there are duplicate keys
(doall (map (fn [[k v]] (when (< 1 v)(timbre/warn v "duplicate key " k)))
(frequencies (map first flat-seq))))
(into {} flat-seq)))
(defn unflatten-data
"reverse operation of flatten-schema, intended for data only as schemas have too many edge cases"
[m]
;; NOTE assumes a map for now TODO handle others
(reduce (fn [acc [path v]]
(let [s path
#_(if (instance? schema.core.OptionalKey path)
(.-k path)
path)]
(assoc-in acc (map keyword (str/split s separator-regex)) v))) {} m))
(defn map->querystring
"m: a flat map
returns a querystring fragment"
[m]
(->> m
(transform [MAP-VALS] codec/form-encode)
(map #(str/join "=" %))
(str/join "&")))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment