Skip to content

Commit 56a94ac

Browse files
authored
add :excess & :missing
* port destructuring & spec changes * port tests
1 parent 6563339 commit 56a94ac

3 files changed

Lines changed: 194 additions & 16 deletions

File tree

‎src/main/cljs/cljs/core/specs/alpha.cljc‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,12 @@
4545
(s/def ::as ::local-name)
4646
(s/def ::defaults ::local-name)
4747
(s/def ::select ::local-name)
48+
(s/def ::excess ::local-name)
49+
(s/def ::missing ::local-name)
4850
(s/def ::all ::local-name)
4951

5052
(s/def ::map-special-binding
51-
(s/keys :opt-un [::as ::or ::keys ::syms ::strs ::keys! ::syms! ::strs! ::select ::defaults ::all]))
53+
(s/keys :opt-un [::as ::or ::keys ::syms ::strs ::keys! ::syms! ::strs! ::select ::excess ::missing ::defaults ::all]))
5254

5355
(s/def ::map-binding (s/tuple ::binding-form any?))
5456

@@ -60,7 +62,7 @@
6062
(s/def ::map-bindings
6163
(s/every (s/or :map-binding ::map-binding
6264
:qualified-keys-or-syms ::ns-keys
63-
:special-binding (s/tuple #{:as :or :keys :syms :strs :keys! :syms! :strs! :select :defaults :all} any?))
65+
:special-binding (s/tuple #{:as :or :keys :syms :strs :keys! :syms! :strs! :select :excess :missing :defaults :all} any?))
6466
:kind map?))
6567

6668
(s/def ::map-binding-form (s/merge ::map-bindings ::map-special-binding))

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

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,7 @@
667667
(core/defn ^:private destmap*
668668
[pb bvec b v]
669669
(core/let [gmap (gensym "map__")
670-
gignore (gensym "ignore__")
670+
gtemp (gensym "temp__")
671671
defaults (:or b)
672672
defaults-as (:defaults b)
673673
_ (core/when (core/and defaults-as (core/not defaults))
@@ -677,6 +677,10 @@
677677
gdefaults (core/when defaults (zipmap (keys defaults) (repeatedly #(gensym "default__"))))
678678
select (:select b)
679679
all (:all b)
680+
excess (:excess b)
681+
missing (:missing b)
682+
gnotfound (core/when missing (gensym "notfound__"))
683+
gnotfound? (core/when missing (gensym "notfound?__"))
680684
xf (core/fn [mk]
681685
(core/let [mkns (namespace mk)
682686
mkn (name mk)]
@@ -696,7 +700,12 @@
696700
(if (:as b)
697701
(conj ret (:as b) gmap)
698702
ret))))
699-
bes (dissoc b :as :or :select :all)
703+
ret (if missing
704+
(conj ret
705+
missing nil
706+
gnotfound (core/list 'new 'js/Object))
707+
ret)
708+
bes (dissoc b :as :or :select :all :excess :missing)
700709
localize (core/fn [bb]
701710
(if #?(:clj (core/instance? clojure.lang.Named bb)
702711
:cljs (cljs.core/implements? INamed bb))
@@ -718,12 +727,22 @@
718727
:cljs (throw (new js/Error
719728
(core/str "Can't supply default value for required key: " bk))))
720729
(core/list `cljs.core/get gmap bk (if local-default? (gdefaults local) (gdefaults bk)))))
721-
(core/list getter gmap bk))]
730+
(if req?
731+
(if missing
732+
(core/list `cljs.core/get gmap bk gnotfound)
733+
(core/list `cljs.core/req! gmap bk))
734+
(core/list `cljs.core/get gmap bk)))]
722735
(if (ident? bb)
723-
(core/-> ret (conj local bv))
736+
(if (core/and req? missing)
737+
(conj ret
738+
gtemp bv
739+
gnotfound? `(identical? ~gtemp ~gnotfound)
740+
missing `(if ~gnotfound? (assoc ~missing ~bk nil) ~missing)
741+
local `(when-not ~gnotfound? ~gtemp))
742+
(core/-> ret (conj local bv)))
724743
(pb ret bb bv))))
725744
retsel
726-
(core/loop [ret ret, sel #{}, bes bes, b->k {}, subs nil, suba nil]
745+
(core/loop [ret ret, sel #{}, bes bes, b->k {}, subs nil, suba nil, subexcess nil submissing nil]
727746
(if (seq bes)
728747
(core/let [be (first bes), bb (key be), bk (val be)]
729748
(if (core/keyword? bb)
@@ -740,7 +759,7 @@
740759
#?(:clj (throw (new IllegalArgumentException (core/str "& can only appear once in " dir)))
741760
:cljs (throw (new js/Error (core/str "& can only appear once in " dir)))))
742761
(core/let [_
743-
(core/when (core/and (not preamp?) (core/symbol? bb))
762+
(core/when (core/and (core/not preamp?) (core/symbol? bb))
744763
#?(:clj (throw
745764
(new IllegalArgumentException
746765
(core/str "'" bb
@@ -751,13 +770,13 @@
751770
"' - binding symbols can only appear before '&', use keys after")))))
752771
bk (if preamp? (tr bb) bb)]
753772
(recur (if (core/or preamp? req?)
754-
(push1 ret (if preamp? bb gignore) bk req?)
773+
(push1 ret (if preamp? bb gtemp) bk req?)
755774
ret)
756775
(conj sel bk)
757776
(next bbs) preamp?
758777
(if preamp? (assoc b->k (localize bb) bk) b->k)))))
759778
{:ret ret, :sel sel, :b->k b->k}))]
760-
(recur (:ret retsel) (:sel retsel) (next bes) (:b->k retsel) subs suba))
779+
(recur (:ret retsel) (:sel retsel) (next bes) (:b->k retsel) subs suba subexcess submissing))
761780
(core/let [subsel? (core/and select (map? bb))
762781
bb (if (core/or (core/not subsel?) (:select bb))
763782
bb
@@ -768,10 +787,23 @@
768787
bb
769788
(assoc bb :all (gensym "all__")))
770789
suba (if suball? (assoc suba bk (:all bb)) suba)
790+
791+
subexcess? (core/and excess (map? bb))
792+
bb (if (core/or (core/not subexcess?) (:excess bb))
793+
bb
794+
(assoc bb :excess (gensym "excess__")))
795+
subexcess (if subexcess? (assoc subexcess bk (:excess bb)) subexcess)
796+
797+
submissing? (core/and missing (map? bb))
798+
bb (if (core/or (core/not submissing?) (:missing bb))
799+
bb
800+
(assoc bb :missing (gensym "missing__")))
801+
submissing (if submissing? (assoc submissing bk (:missing bb)) submissing)
802+
771803
b->k (if (core/symbol? bb) (assoc b->k bb bk) b->k)]
772-
(recur (push1 ret bb bk false) (conj sel bk) (next bes) b->k subs suba))))
773-
{:ret ret, :sel sel, :b->k b->k :subs subs :suba suba}))
774-
ret (:ret retsel), sel (:sel retsel), b->k (:b->k retsel)
804+
(recur (push1 ret bb bk false) (conj sel bk) (next bes) b->k subs suba subexcess submissing))))
805+
{:ret ret, :sel sel, :b->k b->k :subs subs :suba suba :subexcess subexcess :submissing submissing}))
806+
ret (:ret retsel), sel (vec (:sel retsel)), b->k (:b->k retsel)
775807
new-or-code (core/and defaults (core/or defaults-as select all))
776808
bk #(if (core/symbol? %)
777809
(core/let [bk (b->k %)]
@@ -780,21 +812,31 @@
780812
:cljs (throw (new js/Error (core/str "symbol " % " in :or does not refer to a binding")))))
781813
bk)
782814
%)
783-
dm (core/when defaults (dissoc (zipmap (map bk (keys gdefaults)) (vals gdefaults)) nil))
815+
dm (core/when defaults (zipmap (map bk (keys gdefaults)) (vals gdefaults)))
784816
_ (core/and new-or-code (core/not= (count (select-keys dm sel)) (count defaults))
785817
#?(:clj (throw (new IllegalArgumentException (core/str "keys "
786818
(apply disj (set (keys dm)) sel)
787819
" appear only in :or")))
788820
:cljs (throw (new js/Error (core/str "keys "
789821
(apply disj (set (keys dm)) sel)
790822
" appear only in :or")))))
823+
dm (if (empty? dm) nil dm)
791824
ret (if select
792-
(conj ret select `(when-let [mm# (merge (some-vals ~dm) ~gmap (some-vals ~(:subs retsel)))]
825+
(conj ret select `(when-let [mm# (merge ~dm ~gmap (some-vals ~(:subs retsel)))]
793826
(select-keys mm# ~sel)))
794827
ret)
795828
ret (if all
796-
(conj ret all `(merge (some-vals ~dm) ~gmap (some-vals ~(:suba retsel))))
829+
(conj ret all `(merge ~dm ~gmap (some-vals ~(:suba retsel))))
830+
ret)
831+
832+
ret (if excess
833+
(conj ret excess `(merge (not-empty (apply dissoc ~gmap ~sel)) (some-vals ~(:subexcess retsel))))
834+
ret)
835+
836+
ret (if missing
837+
(conj ret missing `(merge ~missing (some-vals ~(:submissing retsel))))
797838
ret)
839+
798840
ret (if defaults-as (conj ret defaults-as dm) ret)]
799841
ret))
800842

‎src/test/cljs/cljs/destructuring_test.cljs‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,140 @@
424424
(is (= 1 (let [{:keys [a & 'b]} {:a 1}] a)))
425425
(is (= 1 (let [{:keys! [a & 'b "c"]} {:a 1, 'b 2, "c" 3}] a)))))))
426426

427+
(deftest excess
428+
(let [sample-map {:a 1, :b 2, :c {:aa 10},
429+
'd 4 'e 5 'f {'dd 40 'ee 50},
430+
"g" 6 "h" 7 "i" {"gg" 60 "hh" 70}}]
431+
(testing "happy path"
432+
(let [{:keys [a] :excess exa} (select-keys sample-map [:a :b :c])
433+
{:keys [a b c] :excess exnil} (select-keys sample-map [:a :b :c])
434+
{{:excess exnest} :c} sample-map
435+
{:keys [a c] :excess exkws} (select-keys sample-map [:a :b :c])
436+
{:syms [d f] :excess exsyms} (select-keys sample-map '[d e f])
437+
{:strs [g i] :excess exstrs} (select-keys sample-map ["g" "h" "i"])]
438+
(is (= {:b 2 :c {:aa 10}} exa))
439+
(is (= {:aa 10} exnest))
440+
(is (= {:b 2} exkws))
441+
(is (= '{e 5} exsyms))
442+
(is (= {"h" 7} exstrs))
443+
444+
(testing ":excess predicative use"
445+
(is (nil? exnil))
446+
(is (nil? (let [{:excess ex} {}] ex)))
447+
(is (nil? (let [{:keys [a] :excess ex} nil] ex)))
448+
449+
(let [{:keys [a] :excess ex-some} (select-keys sample-map [:a :b :c])
450+
{:keys [a b c] :excess ex-none} (assoc (select-keys sample-map [:a :b :c]) :b nil)]
451+
(is (some-vals ex-some))
452+
(is (not (some-vals ex-none)))))))
453+
454+
(testing ":excess retains nil values"
455+
(let [{:keys [a] :excess ex-nil1} (assoc (select-keys sample-map [:a :b :c]) :b nil)
456+
{{:keys [aa] :excess ex-nil2} :c} (assoc-in sample-map [:c :bb] nil)]
457+
(is (= {:b nil :c {:aa 10}} ex-nil1))
458+
(is (= {:bb nil} ex-nil2))))
459+
460+
(testing ":excess and :or to ensure that defaults do not show up"
461+
(let [{:keys [a z] :or {z 99} :excess exor} (select-keys sample-map [:a :b :c])]
462+
(is (= {:b 2 :c {:aa 10}} exor))))
463+
464+
(testing "nested :excess, also with &"
465+
(let [{:keys [a] {:keys [aa] :excess exc} :c} sample-map
466+
{:keys! [a & :b :c]
467+
:syms! [& 'd 'e 'f]
468+
:strs! [& "g" "h" "i"]
469+
{:keys [zz] :or {:zz 999} :excess exinner1} :c
470+
{:syms [yy] :or {'yy 999} :excess exinner2} 'f
471+
{:strs [xx] :or {"xx" 999} :excess exinner3} "i"
472+
:excess exouter} sample-map]
473+
(is (nil? exc))
474+
(is (= {:aa 10} exinner1))
475+
(is (= {'dd 40 'ee 50} exinner2))
476+
(is (= {"gg" 60 "hh" 70} exinner3))
477+
(is (= '{:c {:aa 10}, f {dd 40, ee 50}, "i" {"gg" 60, "hh" 70}} exouter))))
478+
479+
(testing ":excess with namespace-qualification"
480+
(let [nsmap {:foo/x 1000, :foo/y 2000, ::z 3000}
481+
{:foo/keys [x] :excess exfoo} nsmap
482+
{::keys [z] :excess exauto} nsmap
483+
{:foo/keys [x] ::keys [z] :excess exmix} nsmap
484+
{:foo/keys [x y] ::keys [z] :excess exall} nsmap]
485+
(is (= {:foo/y 2000 ::z 3000} exfoo))
486+
(is (= {:foo/x 1000 :foo/y 2000} exauto))
487+
(is (= {:foo/y 2000} exmix))
488+
(is (nil? exall))))))
489+
490+
(deftest missing-directive
491+
(let [keys-map {:a 1 :b 2 :nest {:aa 10}}
492+
{:keys! [a b], :missing mkeys-nil} keys-map
493+
{:keys! [a b c], :missing mkeys-c} keys-map
494+
{:keys! [a b & :c], :missing mkeys-c&} keys-map
495+
496+
syms-map '{a 1 b 2 nest {aa 10}}
497+
{:syms! [a b], :missing msyms-nil} syms-map
498+
{:syms! [a b c], :missing msyms-c} syms-map
499+
{:syms! [a b & 'c], :missing msyms-c&} syms-map
500+
501+
strs-map {"a" 1 "b" 2 "nest" {"aa" 10}}
502+
{:strs! [a b], :missing mstrs-nil} strs-map
503+
{:strs! [a b c], :missing mstrs-c} strs-map
504+
{:strs! [a b & "c"], :missing mstrs-c&} strs-map
505+
506+
q-map {:foo/a 1 :b 2 :foo/c 3 ::d 4}
507+
{:keys! [foo/a :foo/c] :missing mq-nil} q-map
508+
{:keys! [b & :foo/d] :missing mq-d} q-map
509+
{:keys! [foo/a & :foo/d] :missing mq-d&} q-map
510+
{:foo/keys! [a c] :missing q-nil} q-map
511+
{:foo/keys! [a & :foo/d] :missing q-d} q-map
512+
{:foo/keys! [a & :foo/d] :missing q-d&} q-map
513+
{::keys! [d] :missing aq-nil} q-map]
514+
515+
(testing "1-level :missing keys for keys/qkeys/syms/strs"
516+
(is (nil? mkeys-nil))
517+
(is (= mkeys-c {:c nil}))
518+
(is (= mkeys-c& {:c nil}))
519+
520+
(is (nil? msyms-nil))
521+
(is (= msyms-c '{c nil}))
522+
(is (= msyms-c& '{c nil}))
523+
524+
(is (nil? mstrs-nil))
525+
(is (= mstrs-c {"c" nil}))
526+
(is (= mstrs-c& {"c" nil}))
527+
528+
(is (nil? mq-nil))
529+
(is (= mq-d #:foo{:d nil}))
530+
(is (= mq-d& #:foo{:d nil}))
531+
(is (nil? q-nil))
532+
(is (= q-d #:foo{:d nil}))
533+
(is (= q-d& #:foo{:d nil}))
534+
(is (nil? aq-nil)))
535+
536+
(testing "required nested map that also has required keys, covering the following cases:
537+
- :nest is missing
538+
- :nest is nil
539+
- :nest is a map missing :x and/or :y
540+
- :nest is a map having everything that's required"
541+
(let [sample-nest {:a 0 :b 0}]
542+
(are [input-map expected] (= expected
543+
(let [{:keys! [a b & :nest]
544+
{:keys! [x y]} :nest
545+
:missing mkeys-missing}
546+
input-map]
547+
mkeys-missing))
548+
549+
sample-nest {:nest {:x nil, :y nil}}
550+
551+
(assoc sample-nest :nest nil) {:nest {:x nil, :y nil}}
552+
553+
(assoc sample-nest :nest {:x 1}) {:nest {:y nil}}
554+
555+
(assoc sample-nest :nest {:x 1 :y 2}) nil)))
556+
557+
(testing "that a nested map required keys is captured with the outer :missing"
558+
(let [{:keys! [a b], {:keys! [bb]} :nest :missing mkeys-outer} keys-map]
559+
(is (= mkeys-outer {:nest {:bb nil}}))))))
560+
427561
(comment
428562

429563
(cljs.test/run-tests)

0 commit comments

Comments
 (0)