cliops.bend

cswap-bend-cli@0.1.0.0 · 334 lines · 56 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/urec.bend as Urec
3import cswap-bend-cli@0.1.0.0/store/ops.bend as SOps
4import cswap-bend-cli@0.1.0.0/store/io.bend as SIo
5import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
6import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
7import cswap-bend-cli@0.1.0.0/json.bend as Json
8import cswap-bend-cli@0.1.0.0/clock.bend as Clock
9import cswap-bend-cli@0.1.0.0/fs.bend as Fs
10import cswap-bend-cli@0.1.0.0/act.bend as Act
11import cswap-bend-cli@0.1.0.0/cmd.bend as Cmd
12import cswap-bend-cli@0.1.0.0/ustore.bend as Ustore
13import cswap-bend-cli@0.1.0.0/store.bend as Store
14import cswap-bend-cli@0.1.0.0/importu.bend as Importu
15import cswap-bend-cli@0.1.0.0/clibase.bend as Clibase
16
17# How a command may talk to the user: `noin` forbids reading standard input
18# (a prompt becomes an error), `yes` answers the remove confirmation.
19type Ni is Data:
20  Ni{noin: Bool, yes: Bool}
21
22def ni_noin(+ni: Ni) -> Bool:
23  match ni:
24    case Ni{noin, yes}:
25      noin
26
27def ni_yes(+ni: Ni) -> Bool:
28  match ni:
29    case Ni{noin, yes}:
30      yes
31
32def slot_in(+m: String) -> String:
33  Clibase.first_line(String.split(String.drop(m, 5n), ' '))
34
35def answer(yes: Bool, +prefix: String, retry: Act.of(String)) -> Act.of(String):
36  match yes:
37    case True{}:
38      Act.bind(String, String, retry, t => Act.pure(String, prefix ++ "\n" ++ t))
39    case False{}:
40      Act.pure(String, prefix ++ "\nCancelled")
41
42def ask(+m: String, retry: Act.of(String)) -> Act.of(String):
43  Act.bind(String, String, Clibase.read_line(), a => answer(Clibase.is_yes(a), m ++ "\nOverwrite slot " ++ slot_in(m) ++ "? [y/N] ", retry))
44
45def ow.pick(occupied: Bool, +c: U32, +m: String, retry: Act.of(String)) -> Act.of(String):
46  match occupied:
47    case True{}:
48      ask(m, retry)
49    case False{}:
50      Act.fail(String, c, m)
51
52def ow.err(ce: U32 & String, retry: Act.of(String), +noin: Bool) -> Act.of(String):
53  (+c, m) = ce
54  ow.pick(U32.is_eq(c, 10) && Bool.not(noin), c, m, retry)
55
56def ow.handle(r: Fs.Res(String), retry: Act.of(String), +noin: Bool) -> Act.of(String):
57  match r:
58    case Done{t}:
59      Act.pure(String, t)
60    case Fail{e}:
61      ow.err(e, retry, noin)
62
63# Run `first`; when it stops at an occupied slot (code 10) ask on stdin and,
64# on a yes, run `retry` (the same call, told to overwrite). With `noin` the
65# occupied slot is the error.
66def with_overwrite(first: Act.of(String), retry: Act.of(String), +noin: Bool) -> Act.of(String):
67  IO.bind(Fs.Res(String), Fs.Res(String), first, r => ow.handle(r, retry, noin))
68
69def digits.pick(d: Bool, +c: Char, rest: String) -> String:
70  match d:
71    case True{}:
72      SCon{c, rest}
73    case False{}:
74      ""
75
76def digits_of(s: String) -> String:
77  match s:
78    case SNil{}:
79      ""
80    case SCon{+c, t}:
81      digits.pick(Char.is_digit(c), c, digits_of(t))
82
83def nonempty.pick(empty: Bool, +s: String) -> Maybe<&2, String>:
84  match empty:
85    case True{}:
86      None{}
87    case False{}:
88      Some{s}
89
90def nonempty_str(+s: String) -> Maybe<&2, String>:
91  nonempty.pick(String.is_empty(s), s)
92
93def touched.pick(added: Bool, updated: Bool, +l: String) -> Maybe<&2, String>:
94  match added updated:
95    case True{} _:
96      nonempty_str(digits_of(String.drop(l, 14n)))
97    case False{} True{}:
98      nonempty_str(digits_of(String.drop(l, 32n)))
99    case False{} False{}:
100      None{}
101
102def touched.line(+l: String) -> Maybe<&2, String>:
103  touched.pick(String.starts_with(l, "Added Account "), String.starts_with(l, "Updated credentials for Account "), l)
104
105def first_some(a: Maybe<&2, String>, b: Maybe<&2, String>) -> Maybe<&2, String>:
106  match a:
107    case Some{x}:
108      Some{x}
109    case None{}:
110      b
111
112# The slot a successful add reported ("Added Account N: ..." and the like).
113def touched(ls: List<&2, String>) -> Maybe<&2, String>:
114  match ls:
115    case Nil{}:
116      None{}
117    case Con{h, t}:
118      first_some(touched.line(h), touched(t))
119
120def clear_rec(m: Maybe<&2, Json.Json>, +root: String, +slot: String) -> Act.of(Unit):
121  match m:
122    case None{}:
123      Act.pure(Unit, Unit{})
124    case Some{+rec}:
125      Urec.clear_dead(root, [Ustore.Id{slot, SSeq.field_str(rec, "email"), SSeq.field_str(rec, "organizationUuid")}])
126
127def clear_seq(m: Maybe<&2, Json.Json>, +root: String, +slot: String) -> Act.of(Unit):
128  match m:
129    case None{}:
130      Act.pure(Unit, Unit{})
131    case Some{seq}:
132      clear_rec(SSeq.slot_record(seq, slot), root, slot)
133
134def clear_slot(m: Maybe<&2, String>, +root: String) -> Act.of(Unit):
135  match m:
136    case None{}:
137      Act.pure(Unit, Unit{})
138    case Some{slot}:
139      Act.bind(Maybe<&2, Json.Json>, Unit, SIo.read_seq(root), sq => clear_seq(sq, root, slot))
140
141# A credential was written for the slot: its dead-token strike (and failure
142# state) no longer describes it, so the usage row starts clean.
143def after_write(+root: String, +text: String) -> Act.of(Cmd.Out):
144  Act.bind(Unit, Cmd.Out, clear_slot(touched(String.lines(text)), root), u => Act.pure(Cmd.Out, Clibase.text_out(text)))
145
146def add_cmd(+ctx: SBase.Ctx, +alias: Maybe<&2, String>, +slot: Maybe<&2, U32>, +ni: Ni) -> Act.of(Cmd.Out):
147  Act.bind(String, Cmd.Out, with_overwrite(Store.add_current(ctx, alias, slot, False{}, t0 => Store.profile_http(t0)), Store.add_current(ctx, alias, slot, True{}, t1 => Store.profile_http(t1)), ni_noin(ni)),
148    t => after_write(SBase.root_of(ctx), t))
149
150# The token: the argument, one line of stdin for `-`, or a prompt line.
151def line_or_stop(noin: Bool) -> Act.of(String):
152  match noin:
153    case True{}:
154      Act.fail(String, 2, "the token must be on the command line (standard input is not available)")
155    case False{}:
156      Clibase.read_line()
157
158def token_src2(dash: Bool, +t: String, +noin: Bool) -> Act.of(String):
159  match dash:
160    case True{}:
161      line_or_stop(noin)
162    case False{}:
163      Act.pure(String, t)
164
165def token_src(m: Maybe<&2, String>, +noin: Bool) -> Act.of(String):
166  match m:
167    case Some{+t}:
168      token_src2(String.eq(t, "-"), t, noin)
169    case None{}:
170      line_or_stop(noin)
171
172def token_run2(empty: Bool, +ctx: SBase.Ctx, +t: String, +email: Maybe<&2, String>, +slot: Maybe<&2, U32>, +noin: Bool) -> Act.of(Cmd.Out):
173  match empty:
174    case True{}:
175      Act.fail(Cmd.Out, 2, "Token cannot be empty")
176    case False{}:
177      Act.bind(String, Cmd.Out, with_overwrite(SOps.add_token(ctx, t, email, slot, False{}), SOps.add_token(ctx, t, email, slot, True{}), noin),
178        out => after_write(SBase.root_of(ctx), out))
179
180def token_run(+ctx: SBase.Ctx, +token: String, email: Maybe<&2, String>, slot: Maybe<&2, U32>, +noin: Bool) -> Act.of(Cmd.Out):
181  token_run2(String.is_empty(String.trim(token)), ctx, String.trim(token), email, slot, noin)
182
183def add_token_cmd(+ctx: SBase.Ctx, src: Maybe<&2, String>, email: Maybe<&2, String>, slot: Maybe<&2, U32>, +ni: Ni) -> Act.of(Cmd.Out):
184  Act.bind(String, Cmd.Out, token_src(src, ni_noin(ni)), t => token_run(ctx, t, email, slot, ni_noin(ni)))
185
186def remove_answer(yes: Bool, +prompt: String, +ctx: SBase.Ctx, +num: String) -> Act.of(Cmd.Out):
187  match yes:
188    case True{}:
189      Act.bind(String, Cmd.Out, SOps.remove(ctx, num), t => Act.pure(Cmd.Out, Cmd.Out{0, prompt ++ t ++ "\n", ""}))
190    case False{}:
191      Act.pure(Cmd.Out, Cmd.Out{0, prompt ++ "Cancelled\n", ""})
192
193def remove_ask(+ctx: SBase.Ctx, +num: String, +email: String) -> Act.of(Cmd.Out):
194  Act.bind(String, Cmd.Out, Clibase.read_line(), a => remove_answer(Clibase.is_yes(a), "Are you sure you want to permanently remove Account-" ++ num ++ " (" ++ email ++ ")? [y/N] ", ctx, num))
195
196def remove_now(+ctx: SBase.Ctx, +num: String) -> Act.of(Cmd.Out):
197  Act.bind(String, Cmd.Out, SOps.remove(ctx, num), t => Act.pure(Cmd.Out, Cmd.ok(t)))
198
199def remove_gate(noin: Bool, +ctx: SBase.Ctx, +num: String, +email: String) -> Act.of(Cmd.Out):
200  match noin:
201    case True{}:
202      Act.fail(Cmd.Out, 2, "removing Account-" ++ num ++ " needs confirmation; pass --yes")
203    case False{}:
204      remove_ask(ctx, num, email)
205
206def remove_confirm(yes: Bool, noin: Bool, +ctx: SBase.Ctx, +num: String, +email: String) -> Act.of(Cmd.Out):
207  match yes:
208    case True{}:
209      remove_now(ctx, num)
210    case False{}:
211      remove_gate(noin, ctx, num, email)
212
213def remove_rec(m: Maybe<&2, Json.Json>, +ctx: SBase.Ctx, +num: String, +ni: Ni) -> Act.of(Cmd.Out):
214  match m:
215    case None{}:
216      Act.fail(Cmd.Out, 3, "Account-" ++ num ++ " does not exist")
217    case Some{rec}:
218      remove_confirm(ni_yes(ni), ni_noin(ni), ctx, num, SSeq.field_str(rec, "email"))
219
220def remove_res(r: SSeq.Resolved, +ctx: SBase.Ctx, +seq: Json.Json, +ident: String, +ni: Ni) -> Act.of(Cmd.Out):
221  match r:
222    case SSeq.RNone{}:
223      Act.fail(Cmd.Out, 3, "No account found with identifier: " ++ ident)
224    case SSeq.RAmb{msg}:
225      Act.fail(Cmd.Out, 1, msg)
226    case SSeq.RSlot{+n}:
227      remove_rec(SSeq.slot_record(seq, n), ctx, n, ni)
228
229def remove_seq(m: Maybe<&2, Json.Json>, +ctx: SBase.Ctx, +ident: String, +ni: Ni) -> Act.of(Cmd.Out):
230  match m:
231    case None{}:
232      Act.fail(Cmd.Out, 1, "No accounts are managed yet")
233    case Some{+seq}:
234      remove_res(SSeq.resolve(seq, ident), ctx, seq, ident, ni)
235
236def remove_cmd(+ctx: SBase.Ctx, +ident: String, +ni: Ni) -> Act.of(Cmd.Out):
237  Act.bind(Maybe<&2, Json.Json>, Cmd.Out, SIo.read_seq(SBase.root_of(ctx)), m => remove_seq(m, ctx, ident, ni))
238
239def source_pick(ex: Bool, +path: String) -> Act.of(String):
240  match ex:
241    case True{}:
242      Fs.read_file(path)
243    case False{}:
244      Act.fail(String, 4, "usage file not found: " ++ path)
245
246def source_text(+path: String) -> Act.of(String):
247  Act.bind(Bool, String, Act.lift(Bool, Fs.exists(path)), ex => source_pick(ex, path))
248
249def source_dash(dash: Bool, +path: String) -> Act.of(String):
250  match dash:
251    case True{}:
252      IO.bind(Fs.Res(String), Fs.Res(String), Fs.read_stdin(), r => IO.pure(Fs.Res(String), r))
253    case False{}:
254      source_text(path)
255
256def source_of(+path: String) -> Act.of(String):
257  source_dash(String.eq(path, "-"), path)
258
259def slot_of_reading(+r: Urec.Reading) -> String:
260  match r:
261    case Urec.Reading{id, usage, age}:
262      Ustore.slot_of(id)
263
264def email_of_reading(+r: Urec.Reading) -> String:
265  match r:
266    case Urec.Reading{Ustore.Id{slot, email, org}, usage, age}:
267      email
268
269def adopt_line(yes: Bool, +r: Urec.Reading) -> String:
270  match yes:
271    case True{}:
272      "Adopted usage for " ++ email_of_reading(r) ++ " \u{2192} slot " ++ slot_of_reading(r) ++ "\n"
273    case False{}:
274      "Kept slot " ++ slot_of_reading(r) ++ "'s own reading for " ++ email_of_reading(r) ++ ": it is newer\n"
275
276def adopt_lines(rs: List<&2, Urec.Reading>, +adopted: List<&2, String>) -> String:
277  match rs:
278    case Nil{}:
279      ""
280    case Con{+r, t}:
281      adopt_line(Importu.has_slot(slot_of_reading(r), adopted), r) ++ adopt_lines(t, adopted)
282
283def hold_text(any: Bool, +h: U32) -> String:
284  match any:
285    case True{}:
286      Clock.sel.s(U32.is_gt(h, 0), "; fetching held for up to " ++ U32.show(h) ++ "s", "; holds lifted")
287    case False{}:
288      ""
289
290def hold_note(hold: Maybe<&2, U32>, +any: Bool) -> String:
291  match hold:
292    case Some{h}:
293      hold_text(any, h)
294    case None{}:
295      ""
296
297def summary(+adopted: Nat, +kept: Nat, +skipped: U32, hold: Maybe<&2, U32>, +any: Bool) -> String:
298  "Done: " ++ Nat.show(adopted) ++ " adopted, " ++ Nat.show(kept) ++ " kept, " ++ U32.show(skipped) ++ " skipped" ++ hold_note(hold, any)
299
300def import_report(+rs: List<&2, Urec.Reading>, +skipped: U32, hold: Maybe<&2, U32>, ar: Urec.AR) -> Cmd.Out:
301  match ar:
302    case Urec.AR{rows, +adopted, kept}:
303      Cmd.Out{0, "", adopt_lines(rs, adopted) ++ summary(List.length(&2, String, adopted), List.length(&2, String, kept), skipped, hold, Bool.not(List.is_empty(&2, Urec.Reading, rs))) ++ "\n"}
304
305def adopt_run(+root: String, +rs: List<&2, Urec.Reading>, +skipped: U32, +hold: Maybe<&2, U32>, +now: U32) -> Act.of(Cmd.Out):
306  Act.bind(Urec.AR, Cmd.Out, Urec.adopt(root, rs, hold, now), ar => Act.pure(Cmd.Out, import_report(rs, skipped, hold, ar)))
307
308def import_plan_run(r: Result<&2, &2, String, Importu.Plan>, +root: String, hold: Maybe<&2, U32>, +now: U32) -> Act.of(Cmd.Out):
309  match r:
310    case Fail{e}:
311      Act.fail(Cmd.Out, 4, e)
312    case Done{Importu.Plan{rs, sk}}:
313      adopt_run(root, rs, sk, hold, now)
314
315def import_seq(m: Maybe<&2, Json.Json>) -> Json.Json:
316  match m:
317    case Some{j}:
318      j
319    case None{}:
320      Json.of_pairs([])
321
322def import_with(+ctx: SBase.Ctx, +text: String, hold: Maybe<&2, U32>, tm: Clibase.Tm, m: Maybe<&2, Json.Json>) -> Act.of(Cmd.Out):
323  match tm:
324    case Clibase.Tm{+now, off, tick}:
325      import_plan_run(Importu.import_plan(text, import_seq(m), now, off), SBase.root_of(ctx), hold, now)
326
327def import_run(+ctx: SBase.Ctx, +text: String, hold: Maybe<&2, U32>) -> Act.of(Cmd.Out):
328  do Clibase.ActM<Cmd.Out>:
329    tm : Clibase.Tm <- Clibase.times()
330    m : Maybe<&2, Json.Json> <- SIo.read_seq(SBase.root_of(ctx))
331    import_with(ctx, text, hold, tm, m)
332
333def import_cmd(+ctx: SBase.Ctx, +path: String, hold: Maybe<&2, U32>) -> Act.of(Cmd.Out):
334  Act.bind(String, Cmd.Out, source_of(path), text => import_run(ctx, text, hold))