cfgio.bend

cswap-bend-cli@0.1.0.0 · 378 lines · 65 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/act.bend as Act
4import cswap-bend-cli@0.1.0.0/cmd.bend as Cmd
5import cswap-bend-cli@0.1.0.0/fs.bend as Fs
6import cswap-bend-cli@0.1.0.0/setcmd.bend as Setcmd
7import cswap-bend-cli@0.1.0.0/setload.bend as Setload
8import cswap-bend-cli@0.1.0.0/setimp.bend as Imp
9import cswap-bend-cli@0.1.0.0/profile.bend as P
10import cswap-bend-cli@0.1.0.0/profcmd.bend as Profcmd
11import cswap-bend-cli@0.1.0.0/clibase.bend as Clibase
12
13# `cswap config export | import` and `cswap profile export | import`: the
14# whole settings file (or just the profiles) out as a document, a settings
15# document in from a file or standard input, in json, yaml, toon or csv.
16# Export prints JSON; `--format` (the global one) converts it. Import checks
17# every known key as `config set` / `profile set` does, keeps what it does
18# not know, merges into the settings on disk (`--replace` replaces them) and
19# writes the file atomically; nothing is written when any value is bad.
20
21# What the input held: its format, its top-level keys and the checked document.
22type Doc is Data:
23  Doc{fmt: String, keys: List<&2, String>, norm: Json.Json}
24
25type Parsed is Data:
26  Parsed{fmt: String, doc: Json.Json}
27
28# FILE (or `-`), `--format NAME`, `--replace`, `--data TEXT` (the document
29# itself, instead of a file), and the flag whose value is due (`want`).
30type IA is Data:
31  IA{file: Maybe<&2, String>, fmt: Maybe<&2, String>, replace: Bool, err: String, want: String, data: Maybe<&2, String>}
32
33def ActM(-A: Type) -> Type:
34  Act.of(A)
35
36def ActM.pure(-A: Type, x: A) -> Act.of(A):
37  Act.pure(A, x)
38
39def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B):
40  Act.bind(A, B, m, f)
41
42# ---- the arguments of an import ---------------------------------------------------------
43
44def ia.err(+a: IA, msg: String) -> IA:
45  match a:
46    case IA{file, fmt, replace, err, w, data}:
47      IA{file, fmt, replace, Bool.pick(String, String.is_empty(err), msg, err), "", data}
48
49def ia.val(+a: IA, +v: String) -> IA:
50  match a:
51    case IA{file, fmt, replace, err, w, data}:
52      Bool.pick(IA, String.eq(w, "--data"), IA{file, fmt, replace, err, "", Some{v}}, IA{file, Some{v}, replace, err, "", data})
53
54def ia.want(+a: IA, +flag: String) -> IA:
55  match a:
56    case IA{file, fmt, replace, err, w, data}:
57      IA{file, fmt, replace, err, flag, data}
58
59def ia.rep(+a: IA) -> IA:
60  match a:
61    case IA{file, fmt, replace, err, w, data}:
62      IA{file, fmt, True{}, err, w, data}
63
64def ia.file2(m: Maybe<&2, String>, +h: String, +a: IA) -> IA:
65  match m a:
66    case None{} IA{file, fmt, replace, err, w, data}:
67      IA{Some{h}, fmt, replace, err, w, data}
68    case Some{f} _:
69      ia.err(a, "unrecognized arguments: " ++ h)
70
71def ia.get_file(+a: IA) -> Maybe<&2, String>:
72  match a:
73    case IA{file, fmt, replace, err, w, data}:
74      file
75
76def ia.file(+h: String, +a: IA) -> IA:
77  ia.file2(ia.get_file(a), h, a)
78
79def ia.is_pending(+a: IA) -> Bool:
80  match a:
81    case IA{file, fmt, replace, err, w, data}:
82      Bool.not(String.is_empty(w))
83
84def sel.ia(c: Bool, +x: IA, +y: IA) -> IA:
85  match c:
86    case True{}:
87      x
88    case False{}:
89      y
90
91def ia.free(+h: String, +a: IA) -> IA:
92  sel.ia(String.eq(h, "--format") || String.eq(h, "--data"), ia.want(a, h),
93  sel.ia(String.starts_with(h, "--format="), ia.val(ia.want(a, "--format"), String.drop(h, 9n)),
94  sel.ia(String.starts_with(h, "--data="), ia.val(ia.want(a, "--data"), String.drop(h, 7n)),
95  sel.ia(String.eq(h, "--replace"), ia.rep(a),
96  sel.ia(String.starts_with(h, "-") && Bool.not(String.eq(h, "-")), ia.err(a, "unrecognized arguments: " ++ h),
97  ia.file(h, a))))))
98
99def ia.step(+h: String, +a: IA) -> IA:
100  sel.ia(ia.is_pending(a), ia.val(a, h), ia.free(h, a))
101
102def ia.go(args: List<&2, String>, +a: IA) -> IA:
103  match args:
104    case Nil{}:
105      a
106    case Con{h, t}:
107      ia.go(t, ia.step(h, a))
108
109def ia.need(file: Bool, data: Bool, +a: IA) -> IA:
110  match file data:
111    case False{} False{}:
112      ia.err(a, "the following arguments are required: FILE")
113    case True{} True{}:
114      ia.err(a, "give a FILE or --data, not both")
115    case _ _:
116      a
117
118def ia.end2(+a: IA) -> IA:
119  match a:
120    case IA{file, fmt, replace, err, w, data}:
121      ia.need(Maybe.is_some(&2, String, file), Maybe.is_some(&2, String, data), a)
122
123def ia.end(+a: IA) -> IA:
124  match a:
125    case IA{file, fmt, replace, err, w, data}:
126      ia.end2(Bool.pick(IA, String.is_empty(w), a, ia.err(a, "argument " ++ w ++ ": expected one argument")))
127
128def ia.parse(args: List<&2, String>) -> IA:
129  ia.end(ia.go(args, IA{None{}, None{}, False{}, "", "", None{}}))
130
131# ---- reading --------------------------------------------------------------------------------
132
133def failed(+code: U32, +msg: String) -> Act.of(String):
134  Act.fail(String, code, msg)
135
136def read_stdin(noin: Bool) -> Act.of(String):
137  match noin:
138    case True{}:
139      failed(2, "standard input is not available here; give a file")
140    case False{}:
141      Fs.read_stdin()
142
143def read_path(r: Fs.Res(String), +path: String) -> Act.of(String):
144  match r:
145    case Done{t}:
146      Act.pure(String, t)
147    case Fail{e}:
148      failed(1, "could not read " ++ path ++ ": " ++ Setcmd.err.msg(e))
149
150def read_input2(dash: Bool, +path: String, noin: Bool) -> Act.of(String):
151  match dash:
152    case True{}:
153      read_stdin(noin)
154    case False{}:
155      IO.bind(Fs.Res(String), Fs.Res(String), Fs.read_file(path), r => read_path(r, path))
156
157def lift_str(r: Result<&2, &2, String, String>) -> Act.of(String):
158  match r:
159    case Done{s}:
160      Act.pure(String, s)
161    case Fail{m}:
162      failed(1, m)
163
164def lift_json(r: Result<&2, &2, String, Json.Json>) -> Act.of(Json.Json):
165  match r:
166    case Done{j}:
167      Act.pure(Json.Json, j)
168    case Fail{m}:
169      Act.fail(Json.Json, 1, m)
170
171def lift_unit(r: Result<&2, &2, String, Unit>) -> Act.of(Unit):
172  match r:
173    case Done{u}:
174      Act.pure(Unit, Unit{})
175    case Fail{m}:
176      Act.fail(Unit, 1, m)
177
178def keys_check(only: Bool, +doc: Json.Json) -> Act.of(Unit):
179  match only:
180    case True{}:
181      lift_unit(Imp.profile_only(Json.keys(doc)))
182    case False{}:
183      Act.pure(Unit, Unit{})
184
185def active_found(m: Maybe<&2, P.Prof>, +name: String) -> Act.of(Unit):
186  match m:
187    case Some{p}:
188      Act.pure(Unit, Unit{})
189    case None{}:
190      Act.fail(Unit, 1, "active_profile '" ++ name ++ "' names no profile; `cswap profile list` shows the profiles")
191
192# ---- merging ---------------------------------------------------------------------------------
193
194def active_check(m: Maybe<&2, String>, +merged: Json.Json) -> Act.of(Unit):
195  match m:
196    case None{}:
197      Act.pure(Unit, Unit{})
198    case Some{+name}:
199      active_found(P.find(merged, name), name)
200
201# --replace for `profile import` drops the stored profiles first.
202def base_of(only: Bool, replace: Bool, +raw: Json.Json) -> Json.Json:
203  Bool.pick(Json.Json, only && replace, Json.del("active_profile", Json.del("profiles", raw)), raw)
204
205def combine(+replace: Bool, +only: Bool, +norm: Json.Json, +raw: Json.Json) -> Json.Json:
206  Setload.with_version(Bool.pick(Json.Json, replace && Bool.not(only), Imp.strip(norm), Imp.merge(norm, base_of(only, replace, raw))))
207
208# ---- the result ----------------------------------------------------------------------------------
209
210def key_names(ks: List<&2, String>) -> String:
211  String.join(ks, ", ")
212
213def strs(ks: List<&2, String>) -> List<&2, Json.Json>:
214  match ks:
215    case Nil{}:
216      []
217    case Con{h, t}:
218      Json.JStr{h} <> strs(t)
219
220def done_out(json: Bool, +path: String, +f: String, +keys: List<&2, String>) -> Cmd.Out:
221  match json:
222    case True{}:
223      Cmd.doc(Json.of_pairs([("schemaVersion", Json.JNum{"1"}), ("source", Json.JStr{path}), ("format", Json.JStr{f}), ("keys", Json.arr(strs(keys)))]))
224    case False{}:
225      Cmd.ok("Imported " ++ Nat.show(List.length(&2, String, keys)) ++ " key(s) from " ++ path ++ " as " ++ f ++ ": " ++ key_names(keys))
226
227def ia.get_fmt(+a: IA) -> Maybe<&2, String>:
228  match a:
229    case IA{file, fmt, replace, err, w, data}:
230      fmt
231
232def ia.get_replace(+a: IA) -> Bool:
233  match a:
234    case IA{file, fmt, replace, err, w, data}:
235      replace
236
237def ia.get_data(+a: IA) -> Maybe<&2, String>:
238  match a:
239    case IA{file, fmt, replace, err, w, data}:
240      data
241
242def read_src(data: Maybe<&2, String>, +path: String, noin: Bool) -> Act.of(String):
243  match data:
244    case Some{t}:
245      Act.pure(String, t)
246    case None{}:
247      read_input2(String.eq(path, "-"), path, noin)
248
249def parse_input(+path: String, fmt: Maybe<&2, String>, data: Maybe<&2, String>, noin: Bool) -> Act.of(Parsed):
250  do ActM<Parsed>:
251    +text : String <- read_src(data, path, noin)
252    +f : String <- lift_str(Imp.pick_format(fmt, path))
253    +doc : Json.Json <- lift_json(Imp.parse_doc(f, text))
254    Act.pure(Parsed, Parsed{f, doc})
255
256def check_doc(only: Bool, +doc: Json.Json) -> Act.of(Json.Json):
257  do ActM<Json.Json>:
258    keys_check(only, doc)
259    lift_json(Imp.normalize(doc))
260
261def loaded(+p: Parsed, only: Bool) -> Act.of(Doc):
262  match p:
263    case Parsed{f, doc}:
264      Act.bind(Json.Json, Doc, check_doc(only, doc), norm => Act.pure(Doc, Doc{f, Json.keys(doc), norm}))
265
266def load_doc(+path: String, fmt: Maybe<&2, String>, data: Maybe<&2, String>, noin: Bool, only: Bool) -> Act.of(Doc):
267  Act.bind(Parsed, Doc, parse_input(path, fmt, data, noin), p => loaded(p, only))
268
269def save_doc(+root: String, +path: String, +d: Doc, replace: Bool, json: Bool, only: Bool) -> Act.of(Cmd.Out):
270  match d:
271    case Doc{f, keys, norm}:
272      do ActM<Cmd.Out>:
273        +raw : Json.Json <- Setcmd.load_raw_w(root)
274        +merged : Json.Json = combine(replace, only, norm, raw)
275        active_check(P.active(merged), merged)
276        Setcmd.write_settings(root, merged)
277        Act.pure(Cmd.Out, done_out(json, path, f, keys))
278
279def import_run(+root: String, +path: String, fmt: Maybe<&2, String>, data: Maybe<&2, String>, replace: Bool, noin: Bool, json: Bool, +only: Bool) -> Act.of(Cmd.Out):
280  Act.bind(Doc, Cmd.Out, load_doc(path, fmt, data, noin, only), d => save_doc(root, path, d, replace, json, only))
281
282def import_act(+root: String, +a: IA, noin: Bool, json: Bool, only: Bool) -> Act.of(Cmd.Out):
283  import_run(root, Maybe.default(&2, String, ia.get_file(a), "(data)"), ia.get_fmt(a), ia.get_data(a), ia.get_replace(a), noin, json, only)
284
285def ia.err_of(+a: IA) -> String:
286  match a:
287    case IA{file, fmt, replace, err, w, data}:
288      err
289
290def import_ok(ok: Bool, +err: String, +a: IA, +root: String, noin: Bool, json: Bool, only: Bool) -> IO(Cmd.Out):
291  match ok:
292    case True{}:
293      IO.bind(Fs.Res(Cmd.Out), Cmd.Out, import_act(root, a, noin, json, only), r => IO.pure(Cmd.Out, Setcmd.fin(r)))
294    case False{}:
295      IO.pure(Cmd.Out, Cmd.Out{2, "", "cswap: error: " ++ err ++ "\n"})
296
297# ---- export ------------------------------------------------------------------------------------------
298
299def import_gate(+a: IA, +root: String, noin: Bool, json: Bool, only: Bool) -> IO(Cmd.Out):
300  import_ok(String.is_empty(ia.err_of(a)), ia.err_of(a), a, root, noin, json, only)
301
302def import_cmd(+root: String, args: List<&2, String>, noin: Bool, json: Bool, only: Bool) -> IO(Cmd.Out):
303  import_gate(ia.parse(args), root, noin, json, only)
304
305def config_export(+root: String) -> IO(Cmd.Out):
306  IO.bind(Json.Json, Cmd.Out, Setload.load_raw(root), raw => IO.pure(Cmd.Out, Cmd.doc(raw)))
307
308def with_active(m: Maybe<&2, String>, +doc: Json.Json) -> Json.Json:
309  match m:
310    case Some{n}:
311      Json.set("active_profile", Json.JStr{n}, doc)
312    case None{}:
313      doc
314
315def all_profiles(+raw: Json.Json) -> Json.Json:
316  with_active(P.active(raw), Json.of_pairs([("profiles", P.obj_of(Json.get("profiles", raw)))]))
317
318def one_profile(stored: Bool, +raw: Json.Json, +name: String) -> Cmd.Out:
319  match stored:
320    case True{}:
321      Cmd.doc(Json.of_pairs([("profiles", Json.of_pairs([(name, P.obj_of(Json.get(name, P.obj_of(Json.get("profiles", raw)))))]))]))
322    case False{}:
323      Cmd.fail(Profcmd.gone_missing(P.is_builtin(name), name))
324
325def profile_export_out(m: Maybe<&2, String>, +raw: Json.Json) -> Cmd.Out:
326  match m:
327    case None{}:
328      Cmd.doc(all_profiles(raw))
329    case Some{+name}:
330      one_profile(P.has_stored(raw, name), raw, name)
331
332def profile_export(+root: String, n: Maybe<&2, String>) -> IO(Cmd.Out):
333  IO.bind(Json.Json, Cmd.Out, Setload.load_raw(root), raw => IO.pure(Cmd.Out, profile_export_out(n, raw)))
334
335# ---- dispatch ------------------------------------------------------------------------------------------
336
337def usage(+verb: String, +msg: String) -> Cmd.Out:
338  Cmd.Out{2, "", "cswap " ++ verb ++ ": error: " ++ msg ++ "\n"}
339
340def export_args(+verb: String, rest: List<&2, String>, run: IO(Cmd.Out)) -> IO(Cmd.Out):
341  match rest:
342    case Nil{}:
343      run
344    case Con{h, t}:
345      IO.pure(Cmd.Out, usage(verb, "unrecognized arguments: " ++ String.join(Con{h, t}, " ")))
346
347def profile_name_args(+root: String, rest: List<&2, String>) -> IO(Cmd.Out):
348  match rest:
349    case Nil{}:
350      profile_export(root, None{})
351    case Con{n, Nil{}}:
352      profile_export(root, Some{n})
353    case Con{n, Con{x, t}}:
354      IO.pure(Cmd.Out, usage("profile", "unrecognized arguments: " ++ String.join(Con{x, t}, " ")))
355
356def config2(+root: String, +word: String, +rest: List<&2, String>, args: List<&2, String>, +json: Bool, noin: Bool) -> IO(Cmd.Out):
357  Bool.pick(IO(Cmd.Out), String.eq(word, "export"), export_args("config", rest, config_export(root)),
358  Bool.pick(IO(Cmd.Out), String.eq(word, "import"), import_cmd(root, rest, noin, json, False{}), Setcmd.config(root, Clibase.affine(args), json)))
359
360# `cswap config ARGS...` (the words after `config`, --json/--debug taken off).
361def config(+root: String, args: List<&2, String>, json: Bool, noin: Bool) -> IO(Cmd.Out):
362  match args:
363    case Nil{}:
364      Setcmd.config(root, Clibase.affine(args), json)
365    case Con{+h, +t}:
366      config2(root, h, t, Con{h, t}, json, noin)
367
368def profile2(+root: String, +word: String, +rest: List<&2, String>, args: List<&2, String>, +json: Bool, noin: Bool) -> IO(Cmd.Out):
369  Bool.pick(IO(Cmd.Out), String.eq(word, "export"), profile_name_args(root, rest),
370  Bool.pick(IO(Cmd.Out), String.eq(word, "import"), import_cmd(root, rest, noin, json, True{}), Profcmd.profile(root, Clibase.affine(args), json)))
371
372# `cswap profile ARGS...`.
373def profile(+root: String, args: List<&2, String>, json: Bool, noin: Bool) -> IO(Cmd.Out):
374  match args:
375    case Nil{}:
376      Profcmd.profile(root, Clibase.affine(args), json)
377    case Con{+h, +t}:
378      profile2(root, h, t, Con{h, t}, json, noin)