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

205 lines
6.6 KiB
Clojure
Raw Normal View History

2015-02-26 15:55:20 +00:00
(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)))
2015-02-26 15:55:20 +00:00
;; Selector functions
2015-06-18 04:56:03 +00:00
(def ^{:doc "Version of select that takes in a selector pre-compiled with comp-paths"}
compiled-select compiled-select*)
(defn select
"Navigates to and returns a sequence of all the elements specified by the selector."
[selector structure]
(compiled-select (comp-unoptimal selector)
structure))
(defn compiled-select-one
"Version of select-one that takes in a selector pre-compiled with comp-paths"
2015-02-26 15:55:20 +00:00
[selector structure]
(let [res (compiled-select selector structure)]
2015-02-26 15:55:20 +00:00
(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-unoptimal selector) structure))
(defn compiled-select-one!
"Version of select-one! that takes in a selector pre-compiled with comp-paths"
2015-02-26 15:55:20 +00:00
[selector structure]
(let [res (compiled-select-one selector structure)]
2015-02-26 15:55:20 +00:00
(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-unoptimal 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)))
2015-02-26 15:55:20 +00:00
(defn select-first
2015-04-22 15:46:13 +00:00
"Returns first element found. Not any more efficient than select, just a convenience"
2015-02-26 15:55:20 +00:00
[selector structure]
(compiled-select-first (comp-unoptimal selector) structure))
2015-02-26 15:55:20 +00:00
;; Update functions
2015-06-18 04:56:03 +00:00
(def ^{:doc "Version of update that takes in a selector pre-compiled with comp-paths"}
compiled-update compiled-update*)
(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-unoptimal 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))
2015-05-10 12:09:48 +00:00
2015-04-22 15:46:13 +00:00
(defn setval
"Navigates to each value specified by the selector and replaces it by val"
[selector val structure]
(compiled-setval (comp-unoptimal selector) val structure))
2015-02-26 15:55:20 +00:00
(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}}]
2015-02-26 15:55:20 +00:00
(let [state (mutable-cell nil)]
[(compiled-update selector
2015-02-26 15:55:20 +00:00
(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-unoptimal selector) update-fn structure :merge-fn merge-fn))
2015-02-26 15:55:20 +00:00
;; Built-in pathing and context operations
(def ALL (->AllStructurePath))
(def VAL (->ValCollect))
2015-02-26 15:55:20 +00:00
(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))
2015-02-26 15:55:20 +00:00
(defn walker [afn] (->WalkerStructurePath afn))
(defn codewalker [afn] (->CodeWalkerStructurePath afn))
(defn filterer [afn] (->FilterStructurePath afn))
(defn keypath [akey] (->KeyPath akey))
2015-04-18 16:16:51 +00:00
(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))))
2015-02-26 15:55:20 +00:00
(extend-type clojure.lang.Keyword
StructurePath
2015-05-11 18:02:08 +00:00
(select* [kw structure next-fn]
(next-fn (get structure kw)))
(update* [kw structure next-fn]
(assoc structure kw (next-fn (get structure kw)))
2015-02-26 15:55:20 +00:00
))
(extend-type clojure.lang.AFn
StructurePath
(select* [afn structure next-fn]
2015-02-26 15:55:20 +00:00
(if (afn structure)
(next-fn structure)))
(update* [afn structure next-fn]
2015-02-26 15:55:20 +00:00
(if (afn structure)
(next-fn structure)
2015-02-26 15:55:20 +00:00
structure)))
(defn collect [& selector]
(->SelectCollector select (comp-paths* selector)))
2015-02-26 15:55:20 +00:00
(defn collect-one [& selector]
(->SelectCollector select-one (comp-paths* selector)))
(defn putval
"Adds an external value to the collected vals. Useful when additional arguments
are required to the update function that would otherwise require partial
application or a wrapper function.
e.g., incrementing val at path [:a :b] by 3:
(update [:a :b (putval 3)] + some-map)"
[val]
(->PutValCollector val))
2015-06-18 04:56:03 +00:00
(defn cond-path
"Takes in alternating cond-fn selector cond-fn selector...
Tests the structure on the cond-fn, and if it matches uses the following selector for
the rest of the selector. Otherwise, it moves on to the next selector. If nothing
matches, then the structure is not selected."
[& conds]
(->> conds
(partition 2)
(map (fn [[c p]] [c (comp-paths* p)]))
doall
->ConditionalPath
))
(defn if-path
"Like cond-path, but with if semantics."
([cond-fn if-path] (cond-path cond-fn if-path))
([cond-fn if-path else-path]
(cond-path cond-fn if-path (fn [_] true) else-path)))