Skip to content

Commit 2c1f1e2

Browse files
committed
wip
1 parent c9819f7 commit 2c1f1e2

File tree

9 files changed

+372
-6
lines changed

9 files changed

+372
-6
lines changed

dev/compojure_api_kondo_hooks/plumbing/core.clj

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
"Utility belt for Clojure in the wild"
33
(:refer-clojure :exclude [update])
44
(:require
5-
[schema.utils :as schema-utils]
6-
[schema.macros :as schema-macros]
7-
[plumbing.fnk.schema :as schema]
5+
[compojure-api-kondo-hooks.schema.macros :as schema-macros]
86
[compojure-api-kondo-hooks.plumbing.fnk.impl :as fnk-impl]))
97

108
(defmacro letk
@@ -30,8 +28,6 @@
3028
3129
Map destructuring bindings can be mixed with ordinary symbol bindings."
3230
[bindings & body]
33-
(schema/assert-iae (vector? bindings) "Letk binding must be a vector")
34-
(schema/assert-iae (even? (count bindings)) "Letk binding must have even number of elements")
3531
(reduce
3632
(fn [cur-body-form [bind-form value-form]]
3733
(if (symbol? bind-form)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
(ns compojure-api-kondo-hooks.plumbing.core
2+
"Utility belt for Clojure in the wild"
3+
(:refer-clojure :exclude [update])
4+
(:require
5+
[compojure-api-kondo-hooks.schema.macros :as schema-macros]
6+
[compojure-api-kondo-hooks.plumbing.fnk.impl :as fnk-impl]))
7+
8+
(defmacro letk
9+
"Keyword let. Accepts an interleaved sequence of binding forms and map forms like:
10+
(letk [[a {b 2} [:f g h] c d {e 4} :as m & more] a-map ...] & body)
11+
a, c, d, and f are required keywords, and letk will barf if not in a-map.
12+
b and e are optional, and will be bound to default values if not present.
13+
g and h are required keys in the map found under :f.
14+
m will be bound to the entire map (a-map).
15+
more will be bound to all the unbound keys (ie (dissoc a-map :a :b :c :d :e)).
16+
:as and & are both optional, but must be at the end in the specified order if present.
17+
The same symbol cannot be bound multiple times within the same destructing level.
18+
19+
Optional values can reference symbols bound earlier within the same binding, i.e.,
20+
(= [2 2] (let [a 1] (letk [[a {b a}] {:a 2}] [a b]))) but
21+
(= [2 1] (let [a 1] (letk [[{b a} a] {:a 2}] [a b])))
22+
23+
If present, :as and :& symbols are bound before other symbols within the binding.
24+
25+
Namespaced keys are supported by specifying fully-qualified key in binding form. The bound
26+
symbol uses the _name_ portion of the namespaced key, i.e,
27+
(= 1 (letk [[a/b] {:a/b 1}] b)).
28+
29+
Map destructuring bindings can be mixed with ordinary symbol bindings."
30+
[bindings & body]
31+
(reduce
32+
(fn [cur-body-form [bind-form value-form]]
33+
(if (symbol? bind-form)
34+
`(let [~bind-form ~value-form] ~cur-body-form)
35+
(let [{:keys [map-sym body-form]} (fnk-impl/letk-input-schema-and-body-form
36+
&env
37+
bind-form ;(fnk-impl/ensure-schema-metadata &env bind-form)
38+
[]
39+
cur-body-form)]
40+
`(let [~map-sym ~value-form] ~body-form))))
41+
`(do ~@body)
42+
(reverse (partition 2 bindings))))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
(ns compojure-api-kondo-hooks.plumbing.fnk.impl
2+
(:require
3+
[clojure.set :as set]
4+
[schema.core :as-alias s]
5+
[compojure-api-kondo-hooks.schema.macros :as schema-macros]))
6+
7+
;;;;; Helpers
8+
9+
(defn name-sym
10+
"Returns symbol of x's name.
11+
Converts a keyword/string to symbol, or removes namespace (if any) of symbol"
12+
[x]
13+
(with-meta (symbol (name x)) (meta x)))
14+
15+
;;; Parsing new fnk binding style
16+
17+
(declare letk-input-schema-and-body-form)
18+
19+
(defn- process-schematized-map
20+
"Take an optional binding map like {a 2} or {a :- Number 2} and convert the schema
21+
information to canonical metadata, if present."
22+
[env binding]
23+
(case (count binding)
24+
1 (let [[sym v] (first binding)]
25+
{sym v})
26+
27+
2 (let [[[[sym _]] [[schema v]]] ((juxt filter remove) #(= (val %) :-) binding)]
28+
{sym v})))
29+
30+
;; TODO: unify this with positional version.
31+
(defn letk-arg-bind-sym-and-body-form
32+
"Given a single element of a single letk binding form and a current body form, return
33+
a map {:schema-entry :body-form} where schema-entry is a tuple
34+
[bound-key schema external-schema?], and body-form wraps body with destructuring
35+
for this binding as necessary."
36+
[env map-sym binding key-path body-form]
37+
(cond (symbol? binding)
38+
{:schema-entry []
39+
:body-form `(let [~(name-sym binding) (get ~map-sym ~(keyword binding) ~key-path)]
40+
~body-form)}
41+
42+
(map? binding)
43+
(let [schema-fixed-binding (process-schematized-map env binding)
44+
[bound-sym opt-val-expr] (first schema-fixed-binding)
45+
bound-key (keyword bound-sym)]
46+
{:schema-entry []
47+
:body-form `(let [~(name-sym bound-sym) (get ~map-sym ~bound-key ~opt-val-expr)]
48+
~body-form)})
49+
50+
(vector? binding)
51+
(let [[bound-key & more] binding
52+
{inner-input-schema :input-schema
53+
inner-external-input-schema :external-input-schema
54+
inner-map-sym :map-sym
55+
inner-body-form :body-form} (letk-input-schema-and-body-form
56+
env
57+
(with-meta (vec more) (meta binding))
58+
(conj key-path bound-key)
59+
body-form)]
60+
{:schema-entry []
61+
:body-form `(let [~inner-map-sym (get ~map-sym ~bound-key ~key-path)]
62+
~inner-body-form)})
63+
64+
:else (throw (ex-info (format "bad binding: %s" binding) {}))))
65+
66+
(defn- extract-special-args
67+
"Extract trailing & sym and :as sym, possibly with schema metadata. Returns
68+
[more-bindings special-args-map] where special-args-map is a map from each
69+
special symbol found to the symbol that was found."
70+
[env special-arg-signifier-set binding-form]
71+
{:pre [(set? special-arg-signifier-set)]}
72+
(let [[more-bindings special-bindings] (split-with (complement special-arg-signifier-set) binding-form)]
73+
(loop [special-args-map {}
74+
special-arg-set special-arg-signifier-set
75+
[arg-signifier & other-bindings :as special-bindings] special-bindings]
76+
(if-not (seq special-bindings)
77+
[more-bindings special-args-map]
78+
(do
79+
(let [[sym remaining-bindings] (schema-macros/extract-arrow-schematized-element env other-bindings)]
80+
(recur (assoc special-args-map arg-signifier sym)
81+
(disj special-arg-set arg-signifier)
82+
remaining-bindings)))))))
83+
84+
(defn letk-input-schema-and-body-form
85+
"Given a single letk binding form, value form, key path, and body
86+
form, return a map {:input-schema :external-input-schema :map-sym :body-form}
87+
where input-schema is the schema imposed by binding-form, external-input-schema
88+
is like input-schema but includes user overrides for binding vectors,
89+
map-sym is the symbol which it expects the bound value to be bound to,
90+
and body-form wraps body in the bindings from binding-form from map-sym."
91+
[env binding-form key-path body-form]
92+
(let [[bindings {more-sym '& as-sym :as}] (extract-special-args env #{'& :as} binding-form)
93+
as-sym (or as-sym (gensym "map"))
94+
[input-schema-elts
95+
external-input-schema-elts
96+
bound-body-form] (reduce
97+
(fn [[input-schema-elts external-input-schema-elts cur-body] binding]
98+
(let [{:keys [schema-entry body-form]}
99+
(letk-arg-bind-sym-and-body-form
100+
env as-sym binding key-path cur-body)
101+
[bound-key input-schema external-input-schema] schema-entry]
102+
[(conj input-schema-elts [bound-key input-schema])
103+
(conj external-input-schema-elts
104+
[bound-key (or external-input-schema input-schema)])
105+
body-form]))
106+
[[] [] body-form]
107+
(reverse
108+
(schema-macros/process-arrow-schematized-args
109+
env bindings)))
110+
explicit-schema-keys []
111+
final-body-form (if more-sym
112+
`(let [~more-sym (dissoc ~as-sym ~@explicit-schema-keys)]
113+
~bound-body-form)
114+
bound-body-form)]
115+
{:map-sym as-sym
116+
:body-form final-body-form}))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
(ns compojure-api-kondo-hooks.schema.macros)
2+
3+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
4+
;;; Helpers for processing and normalizing element/argument schemas in s/defrecord and s/(de)fn
5+
6+
(defn extract-arrow-schematized-element
7+
"Take a nonempty seq, which may start like [a ...] or [a :- schema ...], and return
8+
a list of [first-element-with-schema-attached rest-elements]"
9+
[env s]
10+
(assert (seq s))
11+
(let [[f & more] s]
12+
(if (= :- (first more))
13+
[f (drop 2 more)]
14+
[f more])))
15+
16+
(defn process-arrow-schematized-args
17+
"Take an arg vector, in which each argument is followed by an optional :- schema,
18+
and transform into an ordinary arg vector where the schemas are metadata on the args."
19+
[env args]
20+
(loop [in args out []]
21+
(if (empty? in)
22+
out
23+
(let [[arg more] (extract-arrow-schematized-element env in)]
24+
(recur more (conj out arg))))))
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
(ns compojure-api-kondo-hooks.plumbing.core
2+
"Utility belt for Clojure in the wild"
3+
(:refer-clojure :exclude [update])
4+
(:require
5+
[compojure-api-kondo-hooks.schema.macros :as schema-macros]
6+
[compojure-api-kondo-hooks.plumbing.fnk.impl :as fnk-impl]))
7+
8+
(defmacro letk
9+
"Keyword let. Accepts an interleaved sequence of binding forms and map forms like:
10+
(letk [[a {b 2} [:f g h] c d {e 4} :as m & more] a-map ...] & body)
11+
a, c, d, and f are required keywords, and letk will barf if not in a-map.
12+
b and e are optional, and will be bound to default values if not present.
13+
g and h are required keys in the map found under :f.
14+
m will be bound to the entire map (a-map).
15+
more will be bound to all the unbound keys (ie (dissoc a-map :a :b :c :d :e)).
16+
:as and & are both optional, but must be at the end in the specified order if present.
17+
The same symbol cannot be bound multiple times within the same destructing level.
18+
19+
Optional values can reference symbols bound earlier within the same binding, i.e.,
20+
(= [2 2] (let [a 1] (letk [[a {b a}] {:a 2}] [a b]))) but
21+
(= [2 1] (let [a 1] (letk [[{b a} a] {:a 2}] [a b])))
22+
23+
If present, :as and :& symbols are bound before other symbols within the binding.
24+
25+
Namespaced keys are supported by specifying fully-qualified key in binding form. The bound
26+
symbol uses the _name_ portion of the namespaced key, i.e,
27+
(= 1 (letk [[a/b] {:a/b 1}] b)).
28+
29+
Map destructuring bindings can be mixed with ordinary symbol bindings."
30+
[bindings & body]
31+
(reduce
32+
(fn [cur-body-form [bind-form value-form]]
33+
(if (symbol? bind-form)
34+
`(let [~bind-form ~value-form] ~cur-body-form)
35+
(let [{:keys [map-sym body-form]} (fnk-impl/letk-input-schema-and-body-form
36+
&env
37+
bind-form ;(fnk-impl/ensure-schema-metadata &env bind-form)
38+
[]
39+
cur-body-form)]
40+
`(let [~map-sym ~value-form] ~body-form))))
41+
`(do ~@body)
42+
(reverse (partition 2 bindings))))
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
(ns compojure-api-kondo-hooks.plumbing.fnk.impl
2+
(:require
3+
[clojure.set :as set]
4+
[schema.core :as-alias s]
5+
[compojure-api-kondo-hooks.schema.macros :as schema-macros]))
6+
7+
;;;;; Helpers
8+
9+
(defn name-sym
10+
"Returns symbol of x's name.
11+
Converts a keyword/string to symbol, or removes namespace (if any) of symbol"
12+
[x]
13+
(with-meta (symbol (name x)) (meta x)))
14+
15+
;;; Parsing new fnk binding style
16+
17+
(declare letk-input-schema-and-body-form)
18+
19+
(defn- process-schematized-map
20+
"Take an optional binding map like {a 2} or {a :- Number 2} and convert the schema
21+
information to canonical metadata, if present."
22+
[env binding]
23+
(case (count binding)
24+
1 (let [[sym v] (first binding)]
25+
{sym v})
26+
27+
2 (let [[[[sym _]] [[schema v]]] ((juxt filter remove) #(= (val %) :-) binding)]
28+
{sym v})))
29+
30+
;; TODO: unify this with positional version.
31+
(defn letk-arg-bind-sym-and-body-form
32+
"Given a single element of a single letk binding form and a current body form, return
33+
a map {:schema-entry :body-form} where schema-entry is a tuple
34+
[bound-key schema external-schema?], and body-form wraps body with destructuring
35+
for this binding as necessary."
36+
[env map-sym binding key-path body-form]
37+
(cond (symbol? binding)
38+
{:schema-entry []
39+
:body-form `(let [~(name-sym binding) (get ~map-sym ~(keyword binding) ~key-path)]
40+
~body-form)}
41+
42+
(map? binding)
43+
(let [schema-fixed-binding (process-schematized-map env binding)
44+
[bound-sym opt-val-expr] (first schema-fixed-binding)
45+
bound-key (keyword bound-sym)]
46+
{:schema-entry []
47+
:body-form `(let [~(name-sym bound-sym) (get ~map-sym ~bound-key ~opt-val-expr)]
48+
~body-form)})
49+
50+
(vector? binding)
51+
(let [[bound-key & more] binding
52+
{inner-input-schema :input-schema
53+
inner-external-input-schema :external-input-schema
54+
inner-map-sym :map-sym
55+
inner-body-form :body-form} (letk-input-schema-and-body-form
56+
env
57+
(with-meta (vec more) (meta binding))
58+
(conj key-path bound-key)
59+
body-form)]
60+
{:schema-entry []
61+
:body-form `(let [~inner-map-sym (get ~map-sym ~bound-key ~key-path)]
62+
~inner-body-form)})
63+
64+
:else (throw (ex-info (format "bad binding: %s" binding) {}))))
65+
66+
(defn- extract-special-args
67+
"Extract trailing & sym and :as sym, possibly with schema metadata. Returns
68+
[more-bindings special-args-map] where special-args-map is a map from each
69+
special symbol found to the symbol that was found."
70+
[env special-arg-signifier-set binding-form]
71+
{:pre [(set? special-arg-signifier-set)]}
72+
(let [[more-bindings special-bindings] (split-with (complement special-arg-signifier-set) binding-form)]
73+
(loop [special-args-map {}
74+
special-arg-set special-arg-signifier-set
75+
[arg-signifier & other-bindings :as special-bindings] special-bindings]
76+
(if-not (seq special-bindings)
77+
[more-bindings special-args-map]
78+
(do
79+
(let [[sym remaining-bindings] (schema-macros/extract-arrow-schematized-element env other-bindings)]
80+
(recur (assoc special-args-map arg-signifier sym)
81+
(disj special-arg-set arg-signifier)
82+
remaining-bindings)))))))
83+
84+
(defn letk-input-schema-and-body-form
85+
"Given a single letk binding form, value form, key path, and body
86+
form, return a map {:input-schema :external-input-schema :map-sym :body-form}
87+
where input-schema is the schema imposed by binding-form, external-input-schema
88+
is like input-schema but includes user overrides for binding vectors,
89+
map-sym is the symbol which it expects the bound value to be bound to,
90+
and body-form wraps body in the bindings from binding-form from map-sym."
91+
[env binding-form key-path body-form]
92+
(let [[bindings {more-sym '& as-sym :as}] (extract-special-args env #{'& :as} binding-form)
93+
as-sym (or as-sym (gensym "map"))
94+
[input-schema-elts
95+
external-input-schema-elts
96+
bound-body-form] (reduce
97+
(fn [[input-schema-elts external-input-schema-elts cur-body] binding]
98+
(let [{:keys [schema-entry body-form]}
99+
(letk-arg-bind-sym-and-body-form
100+
env as-sym binding key-path cur-body)
101+
[bound-key input-schema external-input-schema] schema-entry]
102+
[(conj input-schema-elts [bound-key input-schema])
103+
(conj external-input-schema-elts
104+
[bound-key (or external-input-schema input-schema)])
105+
body-form]))
106+
[[] [] body-form]
107+
(reverse
108+
(schema-macros/process-arrow-schematized-args
109+
env bindings)))
110+
explicit-schema-keys []
111+
final-body-form (if more-sym
112+
`(let [~more-sym (dissoc ~as-sym ~@explicit-schema-keys)]
113+
~bound-body-form)
114+
bound-body-form)]
115+
{:map-sym as-sym
116+
:body-form final-body-form}))
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
(ns compojure-api-kondo-hooks.schema.macros)
2+
3+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
4+
;;; Helpers for processing and normalizing element/argument schemas in s/defrecord and s/(de)fn
5+
6+
(defn extract-arrow-schematized-element
7+
"Take a nonempty seq, which may start like [a ...] or [a :- schema ...], and return
8+
a list of [first-element-with-schema-attached rest-elements]"
9+
[env s]
10+
(assert (seq s))
11+
(let [[f & more] s]
12+
(if (= :- (first more))
13+
[f (drop 2 more)]
14+
[f more])))
15+
16+
(defn process-arrow-schematized-args
17+
"Take an arg vector, in which each argument is followed by an optional :- schema,
18+
and transform into an ordinary arg vector where the schemas are metadata on the args."
19+
[env args]
20+
(loop [in args out []]
21+
(if (empty? in)
22+
out
23+
(let [[arg more] (extract-arrow-schematized-element env in)]
24+
(recur more (conj out arg))))))

scripts/regen-kondo.clj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ set -ex
55
rm -r resources/clj-kondo.exports
66
mkdir -p resources/clj-kondo.exports/metosin/compojure-api/compojure/api
77
mkdir -p resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/compojure
8+
mkdir -p resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/fnk
9+
mkdir -p resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/schema
810

911
bb -f ./scripts/regen_kondo_config.clj

0 commit comments

Comments
 (0)