-
Notifications
You must be signed in to change notification settings - Fork 28
/
utils_v2.cljc
258 lines (203 loc) · 5.95 KB
/
utils_v2.cljc
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
(ns cli-matic.utils-v2
"
# Utils to work with nested configuration trees (cfg v2)
- Convert commands v1 to v2 (fully nested).
- Accessors for data in a nested structure
"
(:require
[cli-matic.utils :as U]
[cli-matic.specs :as S]
[clojure.spec.alpha :as s]
#?(:clj [cli-matic.platform-macros :refer [try-catch-all]]
:cljs [cli-matic.platform-macros :refer-macros [try-catch-all]])
[clojure.string :as str]))
(defn convert-config-v1->v2
"Converts a command version 1 to v2.
A command v1 has always an outer `::S/climatic-cfg`
while a v2 is fully nested.
Note that even in the case where there is only one
command to be run, we still retain the same invocation
format as it originally was - no surprises.
"
[cmd_v1]
{:command (get-in cmd_v1 [:app :command])
:description (get-in cmd_v1 [:app :description])
:version (get-in cmd_v1 [:app :version])
:opts (get-in cmd_v1 [:global-opts])
:subcommands (get-in cmd_v1 [:commands])})
(s/fdef convert-config-v1->v2
:args (s/cat :cmdv1 ::S/climatic-cfg-classic)
:ret ::S/climatic-cfg)
;
;
;
(def SETUP-DEFAULTS-v1
{:app {; see help-gen/
:global-help nil
:subcmd-help nil}
:global-opts []})
(defn add-setup-defaults-v1
"Adds all elements that need to be in the setup spec
but we allow the caller not specify explicitly."
[supplied-setup]
(U/deep-merge SETUP-DEFAULTS-v1 supplied-setup))
(defn cfg-v2
" Converts a config object into v2, if not already v2.
"
[cfg]
(cond
; in v2, we have no :app
(nil? (:app cfg))
cfg
; else, we need to covert it
:else
(convert-config-v1->v2 (add-setup-defaults-v1 cfg))))
(s/fdef cfg-v2
:args (s/cat :cfg (s/or :v1 ::S/climatic-cfg-classic
:v2 ::S/climatic-cfg))
:ret ::S/climatic-cfg)
(defn isRightCmd?
"Check if this is the right command or not,
by name or alias."
[command-or-short-name cfg]
(or (= (:command cfg) command-or-short-name)
(= (:short cfg) command-or-short-name)))
(defn walk
"
Walks a path through a configuration object,
and returns a list of all elements,
in order from root to leaf, as an
executable path.
Does not assert that the last element is a leaf.
The path:
- A subcommand path.
- If empty, no subcommands and no globals
- Each member is the canonical name of a subcommand.
- Each member but the last is of type ::branch-subcommand
It is possible to convert an executable-path back
into a path with [[as-canonical-path]].
"
[cfg path]
(cond
; No path: return a vector with the only option
(empty? path)
[cfg]
; Follow through the path
:else
(loop [c [cfg]
p path
e []]
(let [pe (first p)
rp (rest p)
my-cmd (first (filter (partial isRightCmd? pe) c))
elems (conj e my-cmd)]
(cond
; not found?
(empty? my-cmd)
(throw (ex-info
(str "Unknown subcommand: " pe " - in path " path)
{:valid-path (mapv :command elems)}))
; no remaining items
(empty? rp)
elems
; go back to work
:else
(recur (:subcommands my-cmd)
rp
elems))))))
(s/fdef walk
:args (s/cat
:cfg ::S/climatic-cfg
:path ::S/subcommand-path)
:ret ::S/subcommand-executable-path)
(defn can-walk?
"Check that you can walk up to a point.
It basically traps the exception."
[cfg path]
(try-catch-all
(do
(walk cfg path)
true)
(fn [_]
false)))
(s/fdef can-walk?
:args (s/cat
:cfg ::S/climatic-cfg
:path ::S/subcommand-path)
:ret boolean?)
(defn as-canonical-path
"
Gets the canonical path from an executable-sequence
of subcommands, as obtained from [[walk]].
"
[subcommands]
(mapv :command subcommands))
(defn is-runnable?
"Checks if the last element if the
executable path is actually runnable, that is,
a command."
[xp]
(let [e (last xp)]
(ifn? (:runs e))))
(s/fdef is-runnable?
:args (s/cat :xp ::S/subcommand-executable-path)
:ret boolean?)
(defn canonical-path-to-string
[path]
(str/join " " path))
(s/fdef canonical-path-to-string
:args (s/cat
:path ::S/subcommand-path)
:ret string?)
(defn get-subcommand
"Given a configuration and a path through it,
reeturns the last subcommand."
[cfg path]
(last (walk cfg path)))
(defn get-options-for
"Given a configuration and a path through it,
returns :opts for the last subcommmand."
[cfg path]
(:opts (get-subcommand cfg path)))
(defn rewrite-opts
"
Out of a cli-matic arg list, generates a set of
options for tools.cli.
It also adds in the -? and --help options
to trigger display of helpness.
"
[climatic-args subcmd]
(U/cm-opts->cli-opts
(get-options-for climatic-args subcmd)))
(s/fdef rewrite-opts
:args (s/cat :args ::S/climatic-cfg
:path ::S/subcommand-path)
:ret some?)
(defn list-positional-parms
"Extracts all positional parameters from the configuration."
[cfg subcmd]
;;(prn "CFG" cfg "Sub" subcmd)
(let [opts (get-options-for cfg subcmd)]
(U/positional-parms-from-opts opts)))
(s/fdef
list-positional-parms
:args (s/cat :cfg ::S/climatic-cfg
:cmd ::S/subcommand-path)
:ret (s/coll-of ::S/climatic-option))
(defn get-most-specific-value
"Given a configuration and a path through it, gets the most
specific value for an option.
For example, the help generator might be defined on each subcommand,
or on the root node, or nowhere. We always take the most specific
value.
If the value is defined nowhere, we return a default.
"
([cfg path a-key default]
(let [path-as-objects (walk cfg path)
values (map a-key path-as-objects)
non-nil-values (filter some? values)]
(if (empty? non-nil-values)
default
(last non-nil-values))))
([cfg path a-key]
(get-most-specific-value cfg path a-key nil)))