/
bind.clj
148 lines (128 loc) · 5.26 KB
/
bind.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
(ns workflo.macros.bind
(:require [clojure.core.specs.alpha :as core-specs]
[clojure.test :refer [is]]
[clojure.spec.alpha :as s]
[clojure.spec.gen.alpha :as gen]
[clojure.spec.test.alpha :as st]
[clojure.string :as string]
[workflo.macros.query :as q]
[workflo.macros.query.util :as util]
[workflo.macros.specs.bind :as sb]
[workflo.macros.specs.parsed-query :as spq]
[workflo.macros.specs.query :as sq]))
(s/fdef property-binding-paths
:args (s/cat :property ::spq/typed-property
:path (s/? (s/nilable ::sb/path)))
:ret ::sb/paths)
(defn property-binding-paths
"Takes a property and an optional path of parents. Returns
a flat vector of all paths inside the property and its
parents. Each path is again a sequence of properties,
starting from the leaf (e.g. a regular property) and
ending at the root of the query.
As an example, the query [{user [user [name email]]]}]
would result in the query AST
[{:name user :type :join
:join-source {:name user :type :property}
:join-target [{:name user/name :type :property}
{:name user/email :type :property}]}].
Calling property-binding-paths with the join property
would result in two paths, one for user -> user/name and
one for user -> user/email:
[({:name user/name :type :property}
{:name user :type :join :join-source ... :join-target ...})
({:name user/email :type :property}
{:name user :type :join :join-source ... :join-target ...})]."
([property]
(property-binding-paths property nil))
([property path]
(case (:type property)
:property [(cons property path)]
:link [(cons property path)]
:join (let [new-path (cons property path)]
(if (vector? (:join-target property))
(->> (:join-target property)
(map #(property-binding-paths % new-path))
(apply concat)
(into [new-path]))
[new-path])))))
(s/fdef binding-paths
:args (s/cat :query (s/or :query ::sq/query
:parsed-query ::spq/query))
:ret ::sb/paths)
(defn binding-paths
"Returns a vector of all property binding paths for a query
or parsed query."
[query]
(transduce (map property-binding-paths) concat []
(cond-> query
(s/valid? ::sq/query query) q/conform-and-parse)))
(s/fdef simplified-name
:args (s/cat :sym-or-kw (s/or :sym (s/and symbol? #(not= "." (name %)))
:kw (s/and keyword? #(not= "." (name %)))))
:ret string?)
(defn simplified-name
[sym-or-kw]
(last (string/split (name sym-or-kw) #"\.")))
(s/fdef path-bindings
:args (s/cat :path ::sb/path)
:ret ::core-specs/map-bindings)
(defn path-bindings
"Takes a property binding path and returns destructuring map
that can be used in combination with e.g. let to pluck the
value of the corresponding property from a query result
and bind it to the name of the property.
E.g. for a property path (a b/c d) (simplified notation with
only the property names), it would return {{{a :a} :b/c} :d},
allowing to destructure a map like {:d {:b/c {:a <val>}}}
and bind a to <val>.
It treats backref properties like _b/c in joins special by
binding to b instead of c (the regular case) or _b."
[[leaf & path]]
(loop [form {(or (some-> leaf :alias simplified-name symbol)
(if (util/backref-attr? (:name leaf))
(some-> leaf :name namespace simplified-name symbol)
(some-> leaf :name simplified-name symbol)))
(keyword (:name leaf))}
path path]
(if (empty? path)
form
(recur {form (keyword (:name (first path)))}
(rest path)))))
(s/fdef query-bindings
:args (s/cat :query (s/or :query ::sq/query
:parsed-query ::spq/query))
:ret ::core-specs/map-bindings)
(defn query-bindings
"Takes a query or parsed query and returns map bindings to
be applied to the corresponding query result in order to
destructure and bind all possible properties in the query
result to their names.
E.g. for a query [a :as b c [d e {f [g :as h]}]] it
would return {b :a, d :c/d, e :c/e, f :f, {{h :g} :f}}."
[query]
(let [paths (binding-paths query)
bindings (mapv path-bindings paths)
merge-fn (fn [a b]
(if (and (map? a) (map? b))
(merge a b)
b))
combined (apply (partial merge-with merge-fn) bindings)]
combined))
(s/fdef with-query-bindings*
:args (s/cat :query (s/or :query ::sq/query
:parsed-query ::spq/query)
:result map?
:body any?)
:ret any?)
(defn with-query-bindings*
[query result body]
(let [bindings (query-bindings query)]
`(let [~bindings ~result]
~@body)))
(defmacro with-query-bindings
"Takes a query, a query result and an arbitrary code block.
Wraps the code block so that all possible bindings derived
from the query are bound to the values in the query result."
[query result & body]
(with-query-bindings* query result body))