1 import Base 2 import ./urec.bend as Urec 3 import ./store/ops.bend as SOps 4 import ./store/io.bend as SIo 5 import ./store/base.bend as SBase 6 import ./store/seq.bend as SSeq 7 import ./json.bend as Json 8 import ./clock.bend as Clock 9 import ./fs.bend as Fs 10 import ./act.bend as Act 11 import ./cmd.bend as Cmd 12 import ./ustore.bend as Ustore 13 import ./store.bend as Store 14 import ./importu.bend as Importu 15 import ./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. 19 type Ni is Data: 20 Ni{noin: Bool, yes: Bool} 21 22 def ni_noin(+ni: Ni) -> Bool: 23 match ni: 24 case Ni{noin, yes}: 25 noin 26 27 def ni_yes(+ni: Ni) -> Bool: 28 match ni: 29 case Ni{noin, yes}: 30 yes 31 32 def slot_in(+m: String) -> String: 33 Clibase.first_line(String.split(String.drop(m, 5n), ' ')) 34 35 def 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 42 def 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 45 def 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 52 def 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 56 def 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. 66 def 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 69 def digits.pick(d: Bool, +c: Char, rest: String) -> String: 70 match d: 71 case True{}: 72 SCon{c, rest} 73 case False{}: 74 "" 75 76 def 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 83 def nonempty.pick(empty: Bool, +s: String) -> Maybe<&2, String>: 84 match empty: 85 case True{}: 86 None{} 87 case False{}: 88 Some{s} 89 90 def nonempty_str(+s: String) -> Maybe<&2, String>: 91 nonempty.pick(String.is_empty(s), s) 92 93 def 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 102 def 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 105 def 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). 113 def 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 120 def 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 127 def 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 134 def 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. 143 def 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 146 def 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. 151 def 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 158 def 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 165 def 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 172 def 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 180 def 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 183 def 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 186 def 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 193 def 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 196 def 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 199 def 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 206 def 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 213 def 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 220 def 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 229 def 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 236 def 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 239 def 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 246 def source_text(+path: String) -> Act.of(String): 247 Act.bind(Bool, String, Act.lift(Bool, Fs.exists(path)), ex => source_pick(ex, path)) 248 249 def 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 256 def source_of(+path: String) -> Act.of(String): 257 source_dash(String.eq(path, "-"), path) 258 259 def slot_of_reading(+r: Urec.Reading) -> String: 260 match r: 261 case Urec.Reading{id, usage, age}: 262 Ustore.slot_of(id) 263 264 def email_of_reading(+r: Urec.Reading) -> String: 265 match r: 266 case Urec.Reading{Ustore.Id{slot, email, org}, usage, age}: 267 email 268 269 def 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 276 def 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 283 def 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 290 def 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 297 def 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 300 def 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 305 def 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 308 def 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 315 def 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 322 def 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 327 def import_run(+ctx: SBase.Ctx, +text: String, hold: Maybe<&2, U32>) -> Act.of(Cmd.Out): 328 do Clibase.ActM: 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 333 def 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))