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

183 lines
6.1 KiB
Clojure

(ns com.rpl.specter
(:use [com.rpl.specter impl protocols])
)
;;TODO: can make usage of vals much more efficient by determining during composition how many vals
;;there are going to be. this should make it much easier to allocate space for vals without doing concats
;;all over the place. The apply to the vals + structure can also be avoided since the number of vals is known
;;beforehand
(defn comp-paths [& paths]
(comp-paths* (vec paths)))
;; Selector functions
(defn compiled-select
"Version of select that takes in a selector pre-compiled with comp-paths"
[^com.rpl.specter.impl.TransformFunctions tfns structure]
(let [^com.rpl.specter.impl.ExecutorFunctions ex (.executors tfns)]
((.select-executor ex) (.selector tfns) structure)
))
(defn select
"Navigates to and returns a sequence of all the elements specified by the selector."
[selector structure]
(compiled-select (comp-paths* selector)
structure))
(defn compiled-select-one
"Version of select-one that takes in a selector pre-compiled with comp-paths"
[selector structure]
(let [res (compiled-select selector structure)]
(when (> (count res) 1)
(throw-illegal "More than one element found for params: " selector structure))
(first res)
))
(defn select-one
"Like select, but returns either one element or nil. Throws exception if multiple elements found"
[selector structure]
(compiled-select-one (comp-paths* selector) structure))
(defn compiled-select-one!
"Version of select-one! that takes in a selector pre-compiled with comp-paths"
[selector structure]
(let [res (compiled-select-one selector structure)]
(when (nil? res) (throw-illegal "No elements found for params: " selector structure))
res
))
(defn select-one!
"Returns exactly one element, throws exception if zero or multiple elements found"
[selector structure]
(compiled-select-one! (comp-paths* selector) structure))
(defn compiled-select-first
"Version of select-first that takes in a selector pre-compiled with comp-paths"
[selector structure]
(first (compiled-select selector structure)))
(defn select-first
"Returns first element found. Not any more efficient than select, just a convenience"
[selector structure]
(compiled-select-first (comp-paths* selector) structure))
;; Update functions
(defn compiled-update
"Version of update that takes in a selector pre-compiled with comp-paths"
[^com.rpl.specter.impl.TransformFunctions tfns update-fn structure]
(let [^com.rpl.specter.impl.ExecutorFunctions ex (.executors tfns)]
((.update-executor ex) (.updater tfns) update-fn structure)
))
(defn update
"Navigates to each value specified by the selector and replaces it by the result of running
the update-fn on it"
[selector update-fn structure]
(compiled-update (comp-paths* selector) update-fn structure))
(defn compiled-setval
"Version of setval that takes in a selector pre-compiled with comp-paths"
[selector val structure]
(compiled-update selector (fn [_] val) structure))
(defn setval
"Navigates to each value specified by the selector and replaces it by val"
[selector val structure]
(compiled-setval (comp-paths* selector) val structure))
(defn compiled-replace-in
"Version of replace-in that takes in a selector pre-compiled with comp-paths"
[selector update-fn structure & {:keys [merge-fn] :or {merge-fn concat}}]
(let [state (mutable-cell nil)]
[(compiled-update selector
(fn [e]
(let [res (update-fn e)]
(if res
(let [[ret user-ret] res]
(->> user-ret
(merge-fn (get-cell state))
(set-cell! state))
ret)
e
)))
structure)
(get-cell state)]
))
(defn replace-in
"Similar to update, except returns a pair of [updated-structure sequence-of-user-ret].
The update-fn in this case is expected to return [ret user-ret]. ret is
what's used to update 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 updated in the data structure."
[selector update-fn structure & {:keys [merge-fn] :or {merge-fn concat}}]
(compiled-replace-in (comp-paths* selector) update-fn structure :merge-fn merge-fn))
;; Built-in pathing and context operations
(def ALL (->AllStructurePath))
(def VAL (->ValCollect))
(def LAST (->LastStructurePath))
(def FIRST (->FirstStructurePath))
(defn srange-dynamic [start-fn end-fn] (->SRangePath start-fn end-fn))
(defn srange [start end] (srange-dynamic (fn [_] start) (fn [_] end)))
(def START (srange 0 0))
(def END (srange-dynamic count count))
(defn walker [afn] (->WalkerStructurePath afn))
(defn codewalker [afn] (->CodeWalkerStructurePath afn))
(defn filterer [afn] (->FilterStructurePath afn))
(defn keypath [akey] (->KeyPath akey))
(defn view [afn] (->ViewPath afn))
(defmacro viewfn [& args]
`(view (fn ~@args)))
(defn selected?
"Filters the current value based on whether a selector finds anything.
e.g. (selected? :vals ALL even?) keeps the current element only if an
even number exists for the :vals key"
[& selectors]
(let [s (comp-paths* selectors)]
(fn [structure]
(->> structure
(select s)
empty?
not))))
(extend-type clojure.lang.Keyword
StructurePath
;; faster to invoke keyword directly on structure rather than reuse key-select and key-update functions
(select* [^clojure.lang.Keyword kw structure next-fn]
(next-fn (kw structure)))
(update* [^clojure.lang.Keyword kw structure next-fn]
(assoc structure kw (next-fn (kw structure)))
))
(extend-type clojure.lang.AFn
StructurePath
(select* [afn structure next-fn]
(if (afn structure)
(next-fn structure)))
(update* [afn structure next-fn]
(if (afn structure)
(next-fn structure)
structure)))
(defn collect [& selector]
(->SelectCollector select (comp-paths* selector)))
(defn collect-one [& selector]
(->SelectCollector select-one (comp-paths* selector)))