setcmd.bend
cswap-bend-cli@0.1.0.0 · 296 lines · 45 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/setload.bend as Setload
3import cswap-bend-cli@0.1.0.0/setwrite.bend as Setwrite
4import cswap-bend-cli@0.1.0.0/fs/write.bend as FWrite
5import cswap-bend-cli@0.1.0.0/json.bend as Json
6import cswap-bend-cli@0.1.0.0/fs.bend as Fs
7import cswap-bend-cli@0.1.0.0/act.bend as Act
8import cswap-bend-cli@0.1.0.0/cmd.bend as Cmd
9import cswap-bend-cli@0.1.0.0/settings.bend as Settings
10
11def ActM(-A: Type) -> Type:
12 Act.of(A)
13
14def ActM.pure(-A: Type, x: A) -> Act.of(A):
15 Act.pure(A, x)
16
17def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B):
18 Act.bind(A, B, m, f)
19
20def err.msg(e: U32 & String) -> String:
21 (c, m) = e
22 m
23
24
25def fin(r: Fs.Res(Cmd.Out)) -> Cmd.Out:
26 match r:
27 case Fail{e}:
28 Cmd.fail(err.msg(e))
29 case Done{o}:
30 o
31
32def read_w.obj(+p: String, j: Json.Json) -> Act.of(Json.Json):
33 match j:
34 case Json.JObj{f}:
35 Act.pure(Json.Json, Json.JObj{f})
36 case _:
37 Act.fail(Json.Json, 1, p ++ " is not a JSON object; fix or delete it before changing settings")
38
39def read_w.parse(+p: String, r: Result<&2, &2, String, Json.Json>) -> Act.of(Json.Json):
40 match r:
41 case Fail{e}:
42 Act.fail(Json.Json, 1, p ++ " is not valid JSON (" ++ e ++ "); fix or delete it before changing settings")
43 case Done{j}:
44 read_w.obj(p, j)
45
46def read_w.text(+p: String, r: Fs.Res(String)) -> Act.of(Json.Json):
47 match r:
48 case Fail{e}:
49 Act.fail(Json.Json, 1, "could not read " ++ p ++ ": " ++ err.msg(e))
50 case Done{t}:
51 read_w.parse(p, Json.parse(t))
52
53def read_w.go(+p: String, ex: Bool) -> Act.of(Json.Json):
54 match ex:
55 case False{}:
56 Act.pure(Json.Json, Setload.empty())
57 case True{}:
58 IO.bind(Fs.Res(String), Fs.Res(Json.Json), Fs.read_file(p), r => read_w.text(p, r))
59
60
61
62def load_raw_w(+root: String) -> Act.of(Json.Json):
63 IO.bind(Bool, Fs.Res(Json.Json), Fs.exists(Setload.path(root)), ex => read_w.go(Setload.path(root), ex))
64
65
66def write_settings(+root: String, +j: Json.Json) -> Act.of(Unit):
67 do ActM<Unit>:
68 FWrite.mkdir_p(root)
69 FWrite.chmod("700", root)
70 FWrite.write_atomic(Setload.path(root), Json.emit_pretty(j), "600")
71
72def list.show(+root: String, json: Bool, +rs: List<&2, Setwrite.Row>) -> Cmd.Out:
73 match json:
74 case True{}:
75 Cmd.doc(Json.of_pairs([("schemaVersion", Json.JNum{"1"}), ("path", Json.JStr{Setload.path(root)}),
76 ("settings", Json.arr(Setwrite.rows.json(rs)))]))
77 case False{}:
78 Cmd.ok(Setwrite.list.text(rs))
79
80def list.out(+root: String, json: Bool, raw: Json.Json) -> Cmd.Out:
81 list.show(root, json, Setwrite.effective(raw))
82
83def run.list(+root: String, json: Bool) -> IO(Cmd.Out):
84 IO.bind(Json.Json, Cmd.Out, Setload.load_raw(root), raw => IO.pure(Cmd.Out, list.out(root, json, raw)))
85
86def get.show2(json: Bool, +sp: Settings.Spec, value: Json.Json, set: Bool, text: String) -> Cmd.Out:
87 match json:
88 case True{}:
89 Cmd.doc(Json.of_pairs([("schemaVersion", Json.JNum{"1"}), ("key", Json.JStr{Settings.dotted(sp)}),
90 ("value", value), ("isSet", Json.JBool{set})]))
91 case False{}:
92 Cmd.ok(text)
93
94def get.show(json: Bool, +r: Setwrite.Row) -> Cmd.Out:
95 match r:
96 case Setwrite.Row{+sp, +value, +set}:
97 get.show2(json, sp, value, set, Setwrite.row.val(Setwrite.Row{sp, value, set}))
98
99def get.row(json: Bool, m: Maybe<&2, Setwrite.Row>) -> Cmd.Out:
100 match m:
101 case Some{r}:
102 get.show(json, r)
103 case None{}:
104 Cmd.failed(json, "ConfigError", "unknown setting")
105
106def get.spec(json: Bool, r: Result<&2, &2, String, Settings.Spec>, raw: Json.Json) -> Cmd.Out:
107 match r:
108 case Fail{m}:
109 Cmd.failed(json, "ConfigError", m)
110 case Done{sp}:
111 get.row(json, Setwrite.row.find(Settings.dotted(sp), Setwrite.effective(raw)))
112
113def run.get(+root: String, key: String, json: Bool) -> IO(Cmd.Out):
114 IO.bind(Json.Json, Cmd.Out, Setload.load_raw(root), raw => IO.pure(Cmd.Out, get.spec(json, Settings.spec_of(key), raw)))
115
116def set.msg(+sp: Settings.Spec, +v: Json.Json) -> String:
117 Settings.dotted(sp) ++ " = " ++ Setwrite.fmt.val(Setwrite.is_float(sp), v)
118
119def set.act(+root: String, +sp: Settings.Spec, +v: Json.Json) -> Act.of(Cmd.Out):
120 do ActM<Cmd.Out>:
121 +raw : Json.Json <- load_raw_w(root)
122 w : Unit <- write_settings(root, Setwrite.set_in(raw, sp, v))
123 Act.pure(Cmd.Out, Cmd.ok(set.msg(sp, v)))
124
125def set.parsed(+root: String, sp: Settings.Spec, p: Setwrite.Parsed) -> IO(Cmd.Out):
126 match p:
127 case Setwrite.Bad{m}:
128 IO.pure(Cmd.Out, Cmd.fail(m))
129 case Setwrite.Good{v}:
130 IO.bind(Fs.Res(Cmd.Out), Cmd.Out, set.act(root, sp, v), r => IO.pure(Cmd.Out, fin(r)))
131
132def set.spec(+root: String, r: Result<&2, &2, String, Settings.Spec>, value: String) -> IO(Cmd.Out):
133 match r:
134 case Fail{m}:
135 IO.pure(Cmd.Out, Cmd.fail(m))
136 case Done{+sp}:
137 set.parsed(root, sp, Setwrite.parse_value(sp, value))
138
139def run.set(+root: String, key: String, value: String) -> IO(Cmd.Out):
140 set.spec(root, Settings.spec_of(key), value)
141
142def dflt_of(sp: Settings.Spec) -> Json.Json:
143 match sp:
144 case Settings.Spec{section, key, kind, lo, hi, choices, dflt}:
145 dflt
146
147def unset.msg(+sp: Settings.Spec) -> String:
148 Settings.dotted(sp) ++ " unset (default: " ++ Setwrite.fmt.val(Setwrite.is_float(sp), dflt_of(sp)) ++ ")"
149
150def unset.write(+root: String, +sp: Settings.Spec, +raw: Json.Json) -> Act.of(Cmd.Out):
151 do ActM<Cmd.Out>:
152 w : Unit <- write_settings(root, Setwrite.unset_in(raw, sp))
153 Act.pure(Cmd.Out, Cmd.ok(unset.msg(sp)))
154
155def unset.pick(present: Bool, +root: String, +sp: Settings.Spec, +raw: Json.Json) -> Act.of(Cmd.Out):
156 match present:
157 case True{}:
158 unset.write(root, sp, raw)
159 case False{}:
160 Act.pure(Cmd.Out, Cmd.Out{0, "", Settings.dotted(sp) ++ " is not set; nothing to do\n"})
161
162def section_of_spec(sp: Settings.Spec) -> String:
163 match sp:
164 case Settings.Spec{section, key, kind, lo, hi, choices, dflt}:
165 section
166
167def key_of(sp: Settings.Spec) -> String:
168 match sp:
169 case Settings.Spec{section, key, kind, lo, hi, choices, dflt}:
170 key
171
172def unset.act(+root: String, +sp: Settings.Spec) -> Act.of(Cmd.Out):
173 do ActM<Cmd.Out>:
174 +raw : Json.Json <- load_raw_w(root)
175 unset.pick(Setwrite.has.in(Json.get(section_of_spec(sp), raw), key_of(sp)), root, sp, raw)
176
177def unset.spec(+root: String, r: Result<&2, &2, String, Settings.Spec>) -> IO(Cmd.Out):
178 match r:
179 case Fail{m}:
180 IO.pure(Cmd.Out, Cmd.fail(m))
181 case Done{sp}:
182 IO.bind(Fs.Res(Cmd.Out), Cmd.Out, unset.act(root, sp), r2 => IO.pure(Cmd.Out, fin(r2)))
183
184def run.unset(+root: String, key: String) -> IO(Cmd.Out):
185 unset.spec(root, Settings.spec_of(key))
186
187type Plan is Data:
188 PList{}
189 PGet{key: String}
190 PSet{key: String, value: String}
191 PUnset{key: String}
192 PPath{}
193 PBad{msg: String}
194
195def join_sp(xs: List<String>) -> String:
196 match xs:
197 case Nil{}:
198 ""
199 case Con{h, Nil{}}:
200 h
201 case Con{h, t}:
202 h ++ " " ++ join_sp(t)
203
204
205def plan.none(rest: List<String>, p: Plan) -> Plan:
206 match rest:
207 case Nil{}:
208 p
209 case Con{x, t}:
210 PBad{"unrecognized arguments: " ++ join_sp(Con{x, t})}
211
212def plan.get(rest: List<String>) -> Plan:
213 match rest:
214 case Nil{}:
215 PBad{"the following arguments are required: KEY"}
216 case Con{k, t}:
217 plan.none(t, PGet{k})
218
219def plan.unset(rest: List<String>) -> Plan:
220 match rest:
221 case Nil{}:
222 PBad{"the following arguments are required: KEY"}
223 case Con{k, t}:
224 plan.none(t, PUnset{k})
225
226def plan.set(rest: List<String>) -> Plan:
227 match rest:
228 case Nil{}:
229 PBad{"the following arguments are required: KEY, VALUE"}
230 case Con{k, Nil{}}:
231 PBad{"the following arguments are required: VALUE"}
232 case Con{k, Con{v, t}}:
233 plan.none(t, PSet{k, v})
234
235def plan.verb(l: Bool, g: Bool, s: Bool, u: Bool, p: Bool, a: String, rest: List<String>) -> Plan:
236 match l g s u p:
237 case True{} _ _ _ _:
238 plan.none(rest, PList{})
239 case False{} True{} _ _ _:
240 plan.get(rest)
241 case False{} False{} True{} _ _:
242 plan.set(rest)
243 case False{} False{} False{} True{} _:
244 plan.unset(rest)
245 case False{} False{} False{} False{} True{}:
246 plan.none(rest, PPath{})
247 case _ _ _ _ _:
248 PBad{"argument action: invalid choice: '" ++ a ++ "' (choose from 'list', 'get', 'set', 'unset', 'path')"}
249
250
251def plan.of(args: List<String>) -> Plan:
252 match args:
253 case Nil{}:
254 PList{}
255 case Con{+a, rest}:
256 plan.verb(String.eq(a, "list"), String.eq(a, "get"), String.eq(a, "set"), String.eq(a, "unset"), String.eq(a, "path"), a, rest)
257
258def plan.gate(json: Bool, p: Plan) -> Plan:
259 match json:
260 case True{}:
261 PBad{"--json can only be used with list or get"}
262 case False{}:
263 p
264
265def plan.json(json: Bool, p: Plan) -> Plan:
266 match p:
267 case PSet{k, v}:
268 plan.gate(json, PSet{k, v})
269 case PUnset{k}:
270 plan.gate(json, PUnset{k})
271 case PPath{}:
272 plan.gate(json, PPath{})
273 case _:
274 p
275
276
277
278def run(+root: String, p: Plan, json: Bool) -> IO(Cmd.Out):
279 match p:
280 case PList{}:
281 run.list(root, json)
282 case PGet{key}:
283 run.get(root, key, json)
284 case PSet{key, value}:
285 run.set(root, key, value)
286 case PUnset{key}:
287 run.unset(root, key)
288 case PPath{}:
289 IO.pure(Cmd.Out, Cmd.ok(Setload.path(root)))
290 case PBad{msg}:
291 IO.pure(Cmd.Out, Cmd.Out{2, "", "cswap config: error: " ++ msg ++ "\n"})
292
293
294
295def config(+root: String, args: List<String>, +json: Bool) -> IO(Cmd.Out):
296 run(root, plan.json(json, plan.of(args)), json)