Skip to content

Commit 75dae0f

Browse files
swannodettefogus
andcommitted
Port CLJ-2979: Implement the selector macro in terms of destructuring and its directives
Co-authored-by: Fogus <mefogus@gmail.com>
1 parent 0142b46 commit 75dae0f

2 files changed

Lines changed: 145 additions & 0 deletions

File tree

‎src/main/clojure/cljs/core.cljc‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,45 @@
855855
bindings
856856
(reduce process-entry [] bents))))
857857

858+
(defn- selector-impl [m]
859+
(core/let [dirs [:select :excess :missing :all]
860+
names (zipmap (filter m dirs) (repeatedly gensym))]
861+
(if (empty? names)
862+
#?(:clj (throw (IllegalArgumentException.
863+
"form must contain at least one of :select :excess :missing :all"))
864+
:cljs (throw (js/Error. "form must contain at least one of :select :excess :missing :all")))
865+
`(fn ~(gensym "selector")
866+
[map#]
867+
(let [~(merge m names) map#]
868+
~(if (= 1 (count names))
869+
(-> names first val)
870+
(core/list `some-vals names)))))))
871+
872+
(defmacro selector
873+
"Builds a selecting-fn from m, a map destructuring form that must
874+
include one or more of the :select, :all, :missing, and :excess
875+
directives. The return function takes a collection, destructures it
876+
per m, and returns a map of the result(s).
877+
878+
If m has exactly one directive, the result is the value that
879+
directive would yield. If m has more than one directive, then it
880+
returns a map of directives to values.
881+
882+
As in destructuring, :missing controls whether missing required keys
883+
throw or are collected.
884+
885+
While a map destructuring form may and sometimes must include
886+
bindings, selector doesn't produce bindings, thus ignoring the
887+
associated directive names.
888+
889+
Throws an exception if the argument is not a map."
890+
{:added "1.13"}
891+
[m]
892+
(when (not (map? m))
893+
#?(:clj (throw (IllegalArgumentException. "expected a map"))
894+
:cljs (throw (js/Error. "expected a map"))))
895+
(selector-impl m))
896+
858897
(core/defmacro ^:private return-first
859898
[& body]
860899
`(let [ret# ~(first body)]

‎src/test/cljs/cljs/core_test.cljs‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2028,3 +2028,109 @@
20282028
(select-keys {:a 1 :b 2 :c nil} [:a :c :d]) {:a 1 :c nil}
20292029
(select-keys nil [:a]) {}
20302030
(select-keys #{:a :b} [:a :c]) {:a :a}))
2031+
2032+
(deftest selector-test
2033+
(let [sample-map {:a 1 :b 2 :c 3 :d 4
2034+
:e 5
2035+
::x 10000
2036+
:nested {:aa 1 'saa 10}}]
2037+
(testing "error cases"
2038+
(is (thrown? Exception (eval '(selector {:keys [a b]}))))
2039+
(is (thrown? Exception (eval '(selector sample-map))))
2040+
(is (thrown? Exception (eval '(selector nil))))
2041+
(is (thrown? Exception (eval '(selector {})))))
2042+
2043+
(testing "single directives return their values directly"
2044+
(let [ex1 (selector {:keys [a b & :c :z]
2045+
:keys! [d]
2046+
:select keys-sel})]
2047+
(is (= {:a 1 :b 2 :c 3 :d 4}
2048+
(ex1 sample-map)))
2049+
2050+
(testing "checked keys without :missing should throw"
2051+
(is (thrown? Exception (ex1 (dissoc sample-map :d)))))
2052+
2053+
(testing ":select with :or"
2054+
(let [ex1 (selector {:keys [a b & :c :z]
2055+
:keys! [d]
2056+
:select keys-sel
2057+
:or {:z 42}})]
2058+
(is (= {:a 1 :b 2 :c 3 :d 4 :z 42}
2059+
(ex1 sample-map))))))
2060+
2061+
(let [ex1 (selector {:keys [a b & :c :z]
2062+
:keys! [d]
2063+
:all keys-all})]
2064+
(is (= sample-map (ex1 sample-map))))
2065+
2066+
(let [ex1 (selector {:keys [a b & :c :z]
2067+
:keys! [d]
2068+
:missing keys-missing})]
2069+
(is (nil? (ex1 sample-map)))
2070+
(is (= {:d nil} (ex1 (dissoc sample-map :d)))))
2071+
2072+
(let [ex1 (selector {:keys [a b & :c :z]
2073+
:keys! [d]
2074+
:excess keys-excess})]
2075+
(is (= (dissoc sample-map :a :b :c :d) (ex1 sample-map)))))
2076+
2077+
(testing ":select plus :missing, but nothing missing"
2078+
(let [ex2 (selector {:keys [a b & :c :z]
2079+
:keys! [d]
2080+
:select keys-sel
2081+
:missing keys-missing})]
2082+
(is (= {:select {:a 1 :b 2 :c 3 :d 4}}
2083+
(ex2 sample-map)))))
2084+
2085+
(testing ":select plus :all"
2086+
(let [ex3 (selector {:keys [a b & :c :z]
2087+
:keys! [d]
2088+
:select keys-sel
2089+
:missing keys-missing
2090+
:all keys-all})]
2091+
(is (= {:select {:a 1 :b 2 :c 3 :d 4}
2092+
:all sample-map}
2093+
(ex3 sample-map)))))
2094+
2095+
(testing ":select, :all, and :excess"
2096+
(let [ex4 (selector {:keys [a b & :c :z]
2097+
:keys! [d]
2098+
:select keys-sel
2099+
:missing keys-missing
2100+
:all keys-all
2101+
:excess keys-excess})]
2102+
(is (= {:select {:a 1 :b 2 :c 3 :d 4}
2103+
:excess (dissoc sample-map :a :b :c :d)
2104+
:all sample-map}
2105+
(ex4 sample-map)))
2106+
(testing "plus :missing"
2107+
(is (= {:d nil}
2108+
(:missing (ex4 (dissoc sample-map :d))))))))
2109+
2110+
(testing "directive names bound to _"
2111+
(let [ex_ (selector {:keys [a b & :c :z]
2112+
:keys! [d]
2113+
:select _
2114+
:missing _
2115+
:all _
2116+
:excess _})]
2117+
(is (= {:select {:a 1 :b 2 :c 3 :d 4}
2118+
:excess (dissoc sample-map :a :b :c :d)
2119+
:all sample-map}
2120+
(ex_ sample-map)))))
2121+
2122+
(testing "nested :select"
2123+
(let [exnest (selector {{aa :aa saa 'saa
2124+
:select nest-sel} :nested
2125+
aqx ::x
2126+
:select tl-sel})]
2127+
(is (= {::x 10000 :nested {:aa 1 'saa 10}}
2128+
(exnest sample-map)))))
2129+
2130+
(testing "nested selection behavior with _ bindings"
2131+
(let [exnest_ (selector {{aa :aa saa 'saa
2132+
:select _} :nested
2133+
aqx ::x
2134+
:select _})]
2135+
(is (= {::x 10000 :nested {:aa 1 'saa 10}}
2136+
(exnest_ sample-map)))))))

0 commit comments

Comments
 (0)