specter/src/clj/com/rpl/specter.cljc

681 lines
22 KiB
Clojure

(ns com.rpl.specter
#?(:cljs (:require-macros
[com.rpl.specter.macros
:refer
[late-bound-nav
late-bound-richnav
late-bound-collector
defcollector
defnav
defdynamicnav
richnav
defrichnav]]
[com.rpl.specter.util-macros :refer
[doseqres]]))
(:use [com.rpl.specter.protocols :only [ImplicitNav]]
#?(:clj [com.rpl.specter.macros :only
[late-bound-nav
late-bound-richnav
late-bound-collector
defcollector
defnav
defdynamicnav
richnav
defrichnav]])
#?(:clj [com.rpl.specter.util-macros :only [doseqres]]))
(:require [com.rpl.specter.impl :as i]
[com.rpl.specter.navs :as n]
[clojure.set :as set]))
(defn comp-paths
"Returns a compiled version of the given path for use with
compiled-{select/transform/setval/etc.} functions. This can compile navigators
(defined with `defnav`) without their parameters, and the resulting compiled
path will require parameters for all such navigators in the order in which
they were declared."
[& apath]
(i/comp-paths* (vec apath)))
;; Selection functions
(def ^{:doc "Version of select that takes in a path precompiled with comp-paths"}
compiled-select i/compiled-select*)
(defn select*
"Navigates to and returns a sequence of all the elements specified by the path."
[path structure]
(compiled-select (i/comp-paths* path)
structure))
(def ^{:doc "Version of select-one that takes in a path precompiled with comp-paths"}
compiled-select-one i/compiled-select-one*)
(defn select-one*
"Like select, but returns either one element or nil. Throws exception if multiple elements found"
[path structure]
(compiled-select-one (i/comp-paths* path) structure))
(def ^{:doc "Version of select-one! that takes in a path precompiled with comp-paths"}
compiled-select-one! i/compiled-select-one!*)
(defn select-one!*
"Returns exactly one element, throws exception if zero or multiple elements found"
[path structure]
(compiled-select-one! (i/comp-paths* path) structure))
(def ^{:doc "Version of select-first that takes in a path precompiled with comp-paths"}
compiled-select-first i/compiled-select-first*)
(defn select-first*
"Returns first element found."
[path structure]
(compiled-select-first (i/comp-paths* path) structure))
(def ^{:doc "Version of select-any that takes in a path precompiled with comp-paths"}
compiled-select-any i/compiled-select-any*)
(def ^{:doc "Global value used to indicate no elements selected during
[[select-any]]."}
NONE i/NONE)
(defn select-any*
"Returns any element found or [[NONE]] if nothing selected. This is the most
efficient of the various selection operations."
[path structure]
(compiled-select-any (i/comp-paths* path) structure))
(def ^{:doc "Version of selected-any? that takes in a path precompiled with comp-paths"}
compiled-selected-any? i/compiled-selected-any?*)
(defn selected-any?*
"Returns true if any element was selected, false otherwise."
[path structure]
(compiled-selected-any? (i/comp-paths* path) structure))
;; Reducible traverse functions
(def ^{:doc "Version of traverse that takes in a path precompiled with comp-paths"}
compiled-traverse i/do-compiled-traverse)
(defn traverse*
"Return a reducible object that traverses over `structure` to every element
specified by the path"
[apath structure]
(compiled-traverse (i/comp-paths* apath) structure))
;; Transformation functions
(def ^{:doc "Version of transform that takes in a path precompiled with comp-paths"}
compiled-transform i/compiled-transform*)
(defn transform*
"Navigates to each value specified by the path and replaces it by the result of running
the transform-fn on it"
[path transform-fn structure]
(compiled-transform (i/comp-paths* path) transform-fn structure))
(def ^{:doc "Version of `multi-transform` that takes in a path precompiled with `comp-paths`"}
compiled-multi-transform i/compiled-multi-transform*)
(defn multi-transform*
"Just like `transform` but expects transform functions to be specified
inline in the path using `terminal`. Error is thrown if navigation finishes
at a non-`terminal` navigator. `terminal-val` is a wrapper around `terminal` and is
the `multi-transform` equivalent of `setval`."
[path structure]
(compiled-multi-transform (i/comp-paths* path) structure))
(def ^{:doc "Version of setval that takes in a path precompiled with comp-paths"}
compiled-setval i/compiled-setval*)
(defn setval*
"Navigates to each value specified by the path and replaces it by val"
[path val structure]
(compiled-setval (i/comp-paths* path) val structure))
(def ^{:doc "Version of replace-in that takes in a path precompiled with comp-paths"}
compiled-replace-in i/compiled-replace-in*)
(defn replace-in*
"Similar to transform, except returns a pair of [transformed-structure sequence-of-user-ret].
The transform-fn in this case is expected to return [ret user-ret]. ret is
what's used to transform the data structure, while user-ret will be added to the user-ret sequence
in the final return. replace-in is useful for situations where you need to know the specific values
of what was transformed in the data structure."
[path transform-fn structure & {:keys [merge-fn] :or {merge-fn concat}}]
(compiled-replace-in (i/comp-paths* path) transform-fn structure :merge-fn merge-fn))
;; Helper for making late-bound navs
(def late-path i/late-path)
(def dynamic-param? i/dynamic-param?)
;; Helpers for making recursive or mutually recursive navs
(def local-declarepath i/local-declarepath)
;; Built-in pathing and context operations
(defnav
^{:doc "Stops navigation at this point. For selection returns nothing and for
transformation returns the structure unchanged"}
STOP
[]
(select* [this structure next-fn]
NONE)
(transform* [this structure next-fn]
structure))
(def
^{:doc "Stays navigated at the current point. Essentially a no-op navigator."}
STAY
i/STAY*)
(def
^{:doc "For usage with `multi-transform`, defines an endpoint in the navigation
that will have the parameterized transform function run. The transform
function works just like it does in `transform`, with collected values
given as the first arguments"}
terminal
(richnav [afn]
(select* [this vals structure next-fn]
(i/throw-illegal "'terminal' should only be used in multi-transform"))
(transform* [this vals structure next-fn]
(i/terminal* afn vals structure))))
(defn ^:direct-nav terminal-val
"Like `terminal` but specifies a val to set at the location regardless of
the collected values or the value at the location."
[v]
(terminal (i/fast-constantly v)))
(defnav
^{:doc "Navigate to every element of the collection. For maps navigates to
a vector of `[key value]`."}
ALL
[]
(select* [this structure next-fn]
(n/all-select structure next-fn))
(transform* [this structure next-fn]
(n/all-transform structure next-fn)))
(defnav
^{:doc "Navigate to each value of the map. This is more efficient than
navigating via [ALL LAST]"}
MAP-VALS
[]
(select* [this structure next-fn]
(doseqres NONE [v (vals structure)]
(next-fn v)))
(transform* [this structure next-fn]
(n/map-vals-transform structure next-fn)))
(defcollector VAL []
(collect-val [this structure]
structure))
(def
^{:doc "Navigate to the last element of the collection. If the collection is
empty navigation is stopped at this point."}
LAST
(n/PosNavigator n/get-last n/update-last))
(def
^{:doc "Navigate to the first element of the collection. If the collection is
empty navigation is stopped at this point."}
FIRST
(n/PosNavigator n/get-first n/update-first))
(defnav
^{:doc "Uses start-fn and end-fn to determine the bounds of the subsequence
to select when navigating. Each function takes in the structure as input."}
srange-dynamic
[start-fn end-fn]
(select* [this structure next-fn]
(n/srange-select structure (start-fn structure) (end-fn structure) next-fn))
(transform* [this structure next-fn]
(n/srange-transform structure (start-fn structure) (end-fn structure) next-fn)))
(defnav
^{:doc "Navigates to the subsequence bound by the indexes start (inclusive)
and end (exclusive)"}
srange
[start end]
(select* [this structure next-fn]
(n/srange-select structure start end next-fn))
(transform* [this structure next-fn]
(n/srange-transform structure start end next-fn)))
(defnav
^{:doc "Navigates to every continuous subsequence of elements matching `pred`"}
continuous-subseqs
[pred]
(select* [this structure next-fn]
(doseqres NONE [[s e] (i/matching-ranges structure pred)]
(n/srange-select structure s e next-fn)))
(transform* [this structure next-fn]
(i/continuous-subseqs-transform* pred structure next-fn)))
(defnav
^{:doc "Navigate to the empty subsequence before the first element of the collection."}
BEGINNING
[]
(select* [this structure next-fn]
(next-fn []))
(transform* [this structure next-fn]
(let [to-prepend (next-fn [])]
(n/prepend-all structure to-prepend))))
(defnav
^{:doc "Navigate to the empty subsequence after the last element of the collection."}
END
[]
(select* [this structure next-fn]
(next-fn []))
(transform* [this structure next-fn]
(let [to-append (next-fn [])]
(n/append-all structure to-append))))
(defnav
^{:doc "Navigates to the specified subset (by taking an intersection).
In a transform, that subset in the original set is changed to the
new value of the subset."}
subset
[aset]
(select* [this structure next-fn]
(next-fn (set/intersection structure aset)))
(transform* [this structure next-fn]
(let [subset (set/intersection structure aset)
newset (next-fn subset)]
(-> structure
(set/difference subset)
(set/union newset)))))
(defnav
^{:doc "Navigates to the specified submap (using select-keys).
In a transform, that submap in the original map is changed to the new
value of the submap."}
submap
[m-keys]
(select* [this structure next-fn]
(next-fn (select-keys structure m-keys)))
(transform* [this structure next-fn]
(let [submap (select-keys structure m-keys)
newmap (next-fn submap)]
(merge (reduce dissoc structure m-keys)
newmap))))
(defnav
^{:doc "Using clojure.walk, navigate the data structure until reaching
a value for which `afn` returns truthy."}
walker
[afn]
(select* [this structure next-fn]
(n/walk-select afn next-fn structure))
(transform* [this structure next-fn]
(n/walk-until afn next-fn structure)))
(defnav
^{:doc "Like `walker` but maintains metadata of any forms traversed."}
codewalker
[afn]
(select* [this structure next-fn]
(n/walk-select afn next-fn structure))
(transform* [this structure next-fn]
(i/codewalk-until afn next-fn structure)))
(defdynamicnav subselect
"Navigates to a sequence that contains the results of (select ...),
but is a view to the original structure that can be transformed.
Requires that the input navigators will walk the structure's
children in the same order when executed on \"select\" and then
\"transform\"."
[& path]
(late-bound-nav [late (late-path path)]
(select* [this structure next-fn]
(next-fn (compiled-select late structure)))
(transform* [this structure next-fn]
(let [select-result (compiled-select late structure)
transformed (next-fn select-result)
values-to-insert (i/mutable-cell transformed)]
(compiled-transform late
(fn [_] (let [next-val (first (i/get-cell values-to-insert))]
(i/update-cell! values-to-insert rest)
next-val))
structure)))))
(defrichnav
^{:doc "Navigates to the specified key, navigating to nil if it does not exist."}
keypath
[key]
(select* [this vals structure next-fn]
(next-fn vals (get structure key)))
(transform* [this vals structure next-fn]
(assoc structure key (next-fn vals (get structure key)))))
(defrichnav
^{:doc "Navigates to the key only if it exists in the map."}
must
[k]
(select* [this vals structure next-fn]
(if (contains? structure k)
(next-fn vals (get structure k))
NONE))
(transform* [this vals structure next-fn]
(if (contains? structure k)
(assoc structure k (next-fn vals (get structure k)))
structure)))
(defrichnav
^{:doc "Navigates to result of running `afn` on the currently navigated value."}
view
[afn]
(select* [this vals structure next-fn]
(next-fn vals (afn structure)))
(transform* [this vals structure next-fn]
(next-fn vals (afn structure))))
(defnav
^{:doc "Navigate to the result of running `parse-fn` on the value. For
transforms, the transformed value then has `unparse-fn` run on
it to get the final value at this point."}
parser
[parse-fn unparse-fn]
(select* [this structure next-fn]
(next-fn (parse-fn structure)))
(transform* [this structure next-fn]
(unparse-fn (next-fn (parse-fn structure)))))
(defnav
^{:doc "Navigates to atom value."}
ATOM
[]
(select* [this structure next-fn]
(next-fn @structure))
(transform* [this structure next-fn]
(do
(swap! structure next-fn)
structure)))
(defdynamicnav selected?
"Filters the current value based on whether a path finds anything.
e.g. (selected? :vals ALL even?) keeps the current element only if an
even number exists for the :vals key."
[& path]
(if-let [afn (n/extract-basic-filter-fn path)]
afn
(late-bound-nav [late (late-path path)]
(select* [this structure next-fn]
(i/filter-select
#(n/selected?* late %)
structure
next-fn))
(transform* [this structure next-fn]
(i/filter-transform
#(n/selected?* late %)
structure
next-fn)))))
(defdynamicnav not-selected? [& path]
(if-let [afn (n/extract-basic-filter-fn path)]
(fn [s] (not (afn s)))
(late-bound-nav [late (late-path path)]
(select* [this structure next-fn]
(i/filter-select
#(n/not-selected?* late %)
structure
next-fn))
(transform* [this structure next-fn]
(i/filter-transform
#(n/not-selected?* late %)
structure
next-fn)))))
(defdynamicnav filterer
"Navigates to a view of the current sequence that only contains elements that
match the given path. An element matches the selector path if calling select
on that element with the path yields anything other than an empty sequence.
The input path may be parameterized, in which case the result of filterer
will be parameterized in the order of which the parameterized selectors
were declared."
[& path]
(subselect ALL (selected? path)))
(defdynamicnav transformed
"Navigates to a view of the current value by transforming it with the
specified path and update-fn.
The input path may be parameterized, in which case the result of transformed
will be parameterized in the order of which the parameterized navigators
were declared."
[path update-fn]
(late-bound-nav [late (late-path path)
late-fn update-fn]
(select* [this structure next-fn]
(next-fn (compiled-transform late late-fn structure)))
(transform* [this structure next-fn]
(next-fn (compiled-transform late late-fn structure)))))
(def
^{:doc "Keeps the element only if it matches the supplied predicate. This is the
late-bound parameterized version of using a function directly in a path."}
pred
i/pred*)
(extend-type nil
ImplicitNav
(implicit-nav [this] STAY))
(extend-type #?(:clj clojure.lang.Keyword :cljs cljs.core/Keyword)
ImplicitNav
(implicit-nav [this] (keypath this)))
(extend-type #?(:clj clojure.lang.AFn :cljs function)
ImplicitNav
(implicit-nav [this] (pred this)))
(extend-type #?(:clj clojure.lang.PersistentHashSet :cljs cljs.core/PersistentHashSet)
ImplicitNav
(implicit-nav [this] (pred this)))
(defnav
^{:doc "Navigates to the provided val if the structure is nil. Otherwise it stays
navigated at the structure."}
nil->val
[v]
(select* [this structure next-fn]
(next-fn (if (nil? structure) v structure)))
(transform* [this structure next-fn]
(next-fn (if (nil? structure) v structure))))
(def
^{:doc "Navigates to #{} if the value is nil. Otherwise it stays
navigated at the current value."}
NIL->SET
(nil->val #{}))
(def
^{:doc "Navigates to '() if the value is nil. Otherwise it stays
navigated at the current value."}
NIL->LIST
(nil->val '()))
(def
^{:doc "Navigates to [] if the value is nil. Otherwise it stays
navigated at the current value."}
NIL->VECTOR
(nil->val []))
(defnav ^{:doc "Navigates to the metadata of the structure, or nil if
the structure has no metadata or may not contain metadata."}
META
[]
(select* [this structure next-fn]
(next-fn (meta structure)))
(transform* [this structure next-fn]
(with-meta structure (next-fn (meta structure)))))
(defdynamicnav
^{:doc "Adds the result of running select with the given path on the
current value to the collected vals."}
collect
[& path]
(late-bound-collector [late (late-path path)]
(collect-val [this structure]
(compiled-select late structure))))
(defdynamicnav
^{:doc "Adds the result of running select-one with the given path on the
current value to the collected vals."}
collect-one
[& path]
(late-bound-collector [late (late-path path)]
(collect-val [this structure]
(compiled-select-one late structure))))
(defcollector
^{:doc
"Adds an external value to the collected vals. Useful when additional arguments
are required to the transform function that would otherwise require partial
application or a wrapper function.
e.g., incrementing val at path [:a :b] by 3:
(transform [:a :b (putval 3)] + some-map)"}
putval
[val]
(collect-val [this structure]
val))
(defrichnav
^{:doc "Drops all collected values for subsequent navigation."}
DISPENSE
[]
(select* [this vals structure next-fn]
(next-fn [] structure))
(transform* [this vals structure next-fn]
(next-fn [] structure)))
(defdynamicnav if-path
"Like cond-path, but with if semantics."
([cond-p then-path]
(if-path cond-p then-path STOP))
([cond-p then-path else-path]
(if-let [afn (n/extract-basic-filter-fn cond-p)]
(late-bound-richnav [late-then (late-path then-path)
late-else (late-path else-path)]
(select* [this vals structure next-fn]
(n/if-select
vals
structure
next-fn
afn
late-then
late-else))
(transform* [this vals structure next-fn]
(n/if-transform
vals
structure
next-fn
afn
late-then
late-else)))
(late-bound-richnav [late-cond (late-path cond-p)
late-then (late-path then-path)
late-else (late-path else-path)]
(select* [this vals structure next-fn]
(n/if-select
vals
structure
next-fn
#(n/selected?* late-cond %)
late-then
late-else))
(transform* [this vals structure next-fn]
(n/if-transform
vals
structure
next-fn
#(n/selected?* late-cond %)
late-then
late-else))))))
(defdynamicnav cond-path
"Takes in alternating cond-path path cond-path path...
Tests the structure if selecting with cond-path returns anything.
If so, it uses the following path for this portion of the navigation.
Otherwise, it tries the next cond-path. If nothing matches, then the structure
is not selected.
The input paths may be parameterized, in which case the result of cond-path
will be parameterized in the order of which the parameterized navigators
were declared."
[& conds]
(let [pairs (reverse (partition 2 conds))]
(reduce
(fn [p [tester apath]]
(if-path tester apath p))
STOP
pairs)))
(defdynamicnav multi-path
"A path that branches on multiple paths. For updates,
applies updates to the paths in order."
([] STAY)
([path] path)
([path1 path2]
(late-bound-richnav [late1 (late-path path1)
late2 (late-path path2)]
(select* [this vals structure next-fn]
(let [res1 (i/exec-select* late1 vals structure next-fn)
res2 (i/exec-select* late2 vals structure next-fn)]
(if (identical? NONE res2)
res1
res2)))
(transform* [this vals structure next-fn]
(let [s1 (i/exec-transform* late1 vals structure next-fn)]
(i/exec-transform* late2 vals s1 next-fn)))))
([path1 path2 & paths]
(reduce multi-path (multi-path path1 path2) paths)))
(defdynamicnav stay-then-continue
"Navigates to the current element and then navigates via the provided path.
This can be used to implement pre-order traversal."
[& path]
(multi-path STAY path))
(defdynamicnav continue-then-stay
"Navigates to the provided path and then to the current element. This can be used
to implement post-order traversal."
[& path]
(multi-path path STAY))