Skip to content

Commit 23ab9a0

Browse files
thhellerswannodette
authored andcommitted
Add support for protocols via metadata
1 parent b967499 commit 23ab9a0

File tree

2 files changed

+83
-22
lines changed

2 files changed

+83
-22
lines changed

src/main/clojure/cljs/core.cljc

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2022,14 +2022,29 @@
20222022
=> 17"
20232023
[psym & doc+methods]
20242024
(core/let [p (:name (cljs.analyzer/resolve-var (dissoc &env :locals) psym))
2025-
[doc methods] (if (core/string? (first doc+methods))
2026-
[(first doc+methods) (next doc+methods)]
2027-
[nil doc+methods])
2028-
psym (vary-meta psym assoc
2029-
:doc doc
2030-
:protocol-symbol true)
2025+
[opts methods]
2026+
(core/loop [opts {:protocol-symbol true}
2027+
methods []
2028+
sigs doc+methods]
2029+
(core/if-not (seq sigs)
2030+
[opts methods]
2031+
(core/let [[head & tail] sigs]
2032+
(core/cond
2033+
(core/string? head)
2034+
(recur (assoc opts :doc head) methods tail)
2035+
(core/keyword? head)
2036+
(recur (assoc opts head (first tail)) methods (rest tail))
2037+
(core/list? head)
2038+
(recur opts (conj methods head) tail)
2039+
:else
2040+
(throw #?(:clj (Exception.
2041+
(core/str "Invalid protocol, " psym " received unexpected argument"))
2042+
:cljs (js/Error.
2043+
(core/str "Invalid protocol, " psym " received unexpected argument"))))
2044+
))))
2045+
psym (vary-meta psym merge opts)
20312046
ns-name (core/-> &env :ns :name)
2032-
fqn (core/fn [n] (symbol (core/str ns-name "." n)))
2047+
fqn (core/fn [n] (symbol (core/str ns-name) (core/str n)))
20332048
prefix (protocol-prefix p)
20342049
_ (core/doseq [[mname & arities] methods]
20352050
(core/when (some #{0} (map count (filter vector? arities)))
@@ -2047,21 +2062,44 @@
20472062
(core/symbol? arg) arg
20482063
(core/and (map? arg) (core/some? (:as arg))) (:as arg)
20492064
:else (gensym))) sig)
2050-
sig)]
2051-
`(~sig
2052-
(if (and (not (nil? ~(first sig)))
2053-
(not (nil? (. ~(first sig) ~(symbol (core/str "-" slot)))))) ;; Property access needed here.
2054-
(. ~(first sig) ~slot ~@sig)
2055-
(let [x# (if (nil? ~(first sig)) nil ~(first sig))
2056-
m# (unchecked-get ~(fqn fname) (goog/typeOf x#))]
2057-
(if-not (nil? m#)
2058-
(m# ~@sig)
2059-
(let [m# (unchecked-get ~(fqn fname) "_")]
2060-
(if-not (nil? m#)
2061-
(m# ~@sig)
2062-
(throw
2063-
(missing-protocol
2064-
~(core/str psym "." fname) ~(first sig)))))))))))
2065+
sig)
2066+
2067+
fqn-fname (fqn fname)
2068+
fsig (first sig)
2069+
2070+
;; construct protocol checks in reverse order
2071+
;; check the.protocol/fn["_"] for default impl last
2072+
check
2073+
`(let [m# (unchecked-get ~fqn-fname "_")]
2074+
(if-not (nil? m#)
2075+
(m# ~@sig)
2076+
(throw
2077+
(missing-protocol
2078+
~(core/str psym "." fname) ~fsig))))
2079+
2080+
;; then check protocol fn in metadata (only when protocol is marked with :extend-via-metadata true)
2081+
check
2082+
(core/if-not (:extend-via-metadata opts)
2083+
check
2084+
`(if-let [meta-impl# (-> ~fsig (core/meta) (core/get '~fqn-fname))]
2085+
(meta-impl# ~@sig)
2086+
~check))
2087+
2088+
;; then check protocol on js string,function,array,object
2089+
check
2090+
`(let [x# (if (nil? ~fsig) nil ~fsig)
2091+
m# (unchecked-get ~fqn-fname (goog/typeOf x#))]
2092+
(if-not (nil? m#)
2093+
(m# ~@sig)
2094+
~check))
2095+
2096+
;; then check protocol property on object (first check actually executed)
2097+
check
2098+
`(if (and (not (nil? ~fsig))
2099+
(not (nil? (. ~fsig ~(symbol (core/str "-" slot)))))) ;; Property access needed here.
2100+
(. ~fsig ~slot ~@sig)
2101+
~check)]
2102+
`(~sig ~check)))
20652103
psym (core/-> psym
20662104
(vary-meta update-in [:jsdoc] conj
20672105
"@interface")

src/test/cljs/cljs/core_test.cljs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,3 +1739,26 @@
17391739
(is (== 2 (count (js-keys o))))
17401740
(is (== 17 (gobject/get o "a")))
17411741
(is (== 27 (gobject/get o "b")))))
1742+
1743+
(defprotocol ExtMetaProtocol
1744+
:extend-via-metadata true
1745+
(ext-meta-protocol [x]))
1746+
1747+
(defprotocol NonMetaProtocol
1748+
(non-meta-protocol [x]))
1749+
1750+
(defrecord SomeMetaImpl [x]
1751+
ExtMetaProtocol
1752+
(ext-meta-protocol [_] x)
1753+
NonMetaProtocol
1754+
(non-meta-protocol [_] x))
1755+
1756+
(deftest test-cljs-2960
1757+
;; protocol impl via metadata
1758+
(is (= 1 (ext-meta-protocol (with-meta {} {`ext-meta-protocol (fn [_] 1)}))))
1759+
;; actual impl before metadata
1760+
(is (= 2 (ext-meta-protocol (with-meta (SomeMetaImpl. 2) {`ext-meta-protocol (fn [_] 1)}))))
1761+
;; protocol not marked as :extend-via-metadata so fallthrough to no impl
1762+
(is (thrown? js/Error (non-meta-protocol (with-meta {} {`non-meta-protocol (fn [_] 1)}))))
1763+
;; normal impl call just in case
1764+
(is (= 2 (non-meta-protocol (with-meta (SomeMetaImpl. 2) {`non-meta-protocol (fn [_] 1)})))))

0 commit comments

Comments
 (0)