store/seq.bend

cswap-bend-cli@0.1.0.0 · 270 lines · 52 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
4
5# ---- sequence.json, as Json -------------------------------------------------------
6
7def jstr(+s: String) -> Json.Json:
8  Json.JStr{s}
9
10def jnum(+n: U32) -> Json.Json:
11  Json.JNum{U32.show(n)}
12
13def obj1(+k: String, +v: Json.Json) -> Json.Json:
14  Json.JObj{Json.JField{k, v, Json.JNil{}}}
15
16# A new roster: no accounts, nobody active.
17def seq_fresh(+ts: String) -> Json.Json:
18  Json.JObj{Json.JField{"activeAccountNumber", Json.JNull{}, Json.JField{"lastUpdated", jstr(ts),
19    Json.JField{"sequence", Json.JArr{Json.JNil{}}, Json.JField{"accounts", Json.JObj{Json.JNil{}}, Json.JNil{}}}}}}
20
21def get_or2(+m: Maybe<&2, Json.Json>, +d: Json.Json) -> Json.Json:
22  match m:
23    case None{}:
24      d
25    case Some{v}:
26      v
27
28def get_or(+key: String, +j: Json.Json, +d: Json.Json) -> Json.Json:
29  get_or2(Json.get(key, j), d)
30
31def str_or(+m: Maybe<&2, String>, +d: String) -> String:
32  match m:
33    case None{}:
34      d
35    case Some{s}:
36      s
37
38# A string field, "" when missing or not a string.
39def field_str(+rec: Json.Json, +key: String) -> String:
40  str_or(Maybe.bind(&2, Json.Json, String, Json.get(key, rec), Json.as_string), "")
41
42def unwrap_obj(+j: Json.Json) -> Json.Json:
43  match j:
44    case Json.JObj{fields}:
45      fields
46    case _:
47      Json.JNil{}
48
49def unwrap_arr(+j: Json.Json) -> Json.Json:
50  match j:
51    case Json.JArr{items}:
52      items
53    case _:
54      Json.JNil{}
55
56def accounts_of(+seq: Json.Json) -> Json.Json:
57  get_or("accounts", seq, Json.JObj{Json.JNil{}})
58
59# The fields chain of `accounts`, and the items chain of `sequence`.
60def fields_of(+seq: Json.Json) -> Json.Json:
61  unwrap_obj(accounts_of(seq))
62
63def items_of(+seq: Json.Json) -> Json.Json:
64  unwrap_arr(get_or("sequence", seq, Json.JNil{}))
65
66def rec_is(+rec: Json.Json, +email: String, +org: String) -> Bool:
67  String.eq(field_str(rec, "email"), email) && String.eq(field_str(rec, "organizationUuid"), org)
68
69def pick_slot(+hit: Bool, +k: String, +rest: Maybe<&2, String>) -> Maybe<&2, String>:
70  match hit:
71    case True{}:
72      Some{k}
73    case False{}:
74      rest
75
76def find_in(+fields: Json.Json, +email: String, +org: String) -> Maybe<&2, String>:
77  match fields:
78    case Json.JField{k, v, t}:
79      pick_slot(rec_is(v, email, org), k, find_in(t, email, org))
80    case _:
81      None{}
82
83# The slot whose record is (email, organizationUuid), as `_find_account_slot`.
84def find_slot(+seq: Json.Json, +email: String, +org: String) -> Maybe<&2, String>:
85  find_in(fields_of(seq), email, org)
86
87def sel.u(+c: Bool, +x: U32, +y: U32) -> U32:
88  match c:
89    case True{}:
90      x
91    case False{}:
92      y
93
94def max_u32(+a: U32, +b: U32) -> U32:
95  sel.u(U32.is_gt(a, b), a, b)
96
97def str_u32(+s: String) -> U32:
98  Maybe.default(&2, U32, U32.read(s), 0)
99
100def max_in(+fields: Json.Json, +acc: U32) -> U32:
101  match fields:
102    case Json.JField{k, v, t}:
103      max_in(t, max_u32(acc, str_u32(k)))
104    case _:
105      acc
106
107# max(slot) + 1, or 1 for an empty roster.
108def next_slot(+seq: Json.Json) -> U32:
109  (max_in(fields_of(seq), 0) + 1 : U32)
110
111def slot_record(+seq: Json.Json, +num: String) -> Maybe<&2, Json.Json>:
112  Json.get(num, accounts_of(seq))
113
114def is_api_key_rec(+rec: Json.Json) -> Bool:
115  String.eq(field_str(rec, "kind"), "api_key")
116
117def num_of(+m: Maybe<&2, U32>, +rest: List<&2, U32>) -> List<&2, U32>:
118  match m:
119    case None{}:
120      rest
121    case Some{n}:
122      n <> rest
123
124# The slot numbers listed in `sequence`.
125def nums_of(+items: Json.Json) -> List<&2, U32>:
126  match items:
127    case Json.JCons{h, t}:
128      num_of(Json.as_u32(h), nums_of(t))
129    case _:
130      []
131
132def seq_nums(+seq: Json.Json) -> List<&2, U32>:
133  nums_of(items_of(seq))
134
135def insert_pick(+lt: Bool, +eq: Bool, +n: U32, +h: U32, +t: List<&2, U32>, +ins: List<&2, U32>) -> List<&2, U32>:
136  match lt eq:
137    case True{} _:
138      n <> h <> t
139    case False{} True{}:
140      h <> t
141    case False{} False{}:
142      h <> ins
143
144def insert_sorted(+n: U32, +xs: List<&2, U32>) -> List<&2, U32>:
145  match xs:
146    case Nil{}:
147      [n]
148    case Con{h, t}:
149      insert_pick(U32.is_lt(n, h), U32.is_eq(n, h), n, h, t, insert_sorted(n, t))
150
151def without_pick(+drop: Bool, +h: U32, +rest: List<&2, U32>) -> List<&2, U32>:
152  match drop:
153    case True{}:
154      rest
155    case False{}:
156      h <> rest
157
158def without(+n: U32, +xs: List<&2, U32>) -> List<&2, U32>:
159  match xs:
160    case Nil{}:
161      []
162    case Con{h, t}:
163      without_pick(U32.is_eq(n, h), h, without(n, t))
164
165def nums_json2(+xs: List<&2, U32>) -> List<&2, Json.Json>:
166  match xs:
167    case Nil{}:
168      []
169    case Con{h, t}:
170      jnum(h) <> nums_json2(t)
171
172def nums_json(+xs: List<&2, U32>) -> Json.Json:
173  Json.arr(nums_json2(xs))
174
175def stamp(+seq: Json.Json, +ts: String) -> Json.Json:
176  Json.set("lastUpdated", jstr(ts), seq)
177
178def set_active(+seq: Json.Json, +n: U32) -> Json.Json:
179  Json.set("activeAccountNumber", jnum(n), seq)
180
181# Record `rec` under slot `num` (replacing in place or appending) and list the
182# slot in `sequence`, kept sorted.
183def put_account(+seq: Json.Json, +num: String, +rec: Json.Json) -> Json.Json:
184  Json.set("sequence", nums_json(insert_sorted(str_u32(num), seq_nums(seq))),
185    Json.set("accounts", Json.set(num, rec, accounts_of(seq)), seq))
186
187# Forget slot `num`: its record and its place in `sequence`.
188def drop_account(+seq: Json.Json, +num: String) -> Json.Json:
189  Json.set("sequence", nums_json(without(str_u32(num), seq_nums(seq))),
190    Json.set("accounts", Json.del(num, accounts_of(seq)), seq))
191
192def alias_in(+fields: Json.Json, +a: String) -> Maybe<&2, String>:
193  match fields:
194    case Json.JField{k, v, t}:
195      pick_slot(String.eq(String.to_lower(field_str(v, "alias")), a) && SBase.nonempty(a), k, alias_in(t, a))
196    case _:
197      None{}
198
199def find_alias(+seq: Json.Json, +alias: String) -> Maybe<&2, String>:
200  alias_in(fields_of(seq), String.to_lower(alias))
201
202def emails_pick(+hit: Bool, +k: String, +rest: List<&2, String>) -> List<&2, String>:
203  match hit:
204    case True{}:
205      k <> rest
206    case False{}:
207      rest
208
209def emails_in(+fields: Json.Json, +email: String) -> List<&2, String>:
210  match fields:
211    case Json.JField{k, v, t}:
212      emails_pick(String.eq(field_str(v, "email"), email), k, emails_in(t, email))
213    case _:
214      []
215
216def slots_for_email(+seq: Json.Json, +email: String) -> List<&2, String>:
217  emails_in(fields_of(seq), email)
218
219def org_tag2(+n: String) -> String:
220  SBase.sel.s(String.is_empty(n), "personal", n)
221
222def org_tag(+rec: Json.Json) -> String:
223  org_tag2(field_str(rec, "organizationName"))
224
225def amb_org(+rec: Json.Json) -> Maybe<&2, String>:
226  Some{org_tag(rec)}
227
228def amb_details(+seq: Json.Json, +slots: List<&2, String>) -> List<&2, String>:
229  match slots:
230    case Nil{}:
231      []
232    case Con{n, t}:
233      (n ++ " [" ++ str_or(Maybe.bind(&2, Json.Json, String, slot_record(seq, n), r => amb_org(r)), "personal") ++ "]") <> amb_details(seq, t)
234
235# The reference's ambiguity text for an email in several orgs.
236def ambiguous_msg(+seq: Json.Json, +email: String, +slots: List<&2, String>) -> String:
237  "Email '" ++ email ++ "' is ambiguous — matches accounts: " ++ String.join(amb_details(seq, slots), ", ")
238    ++ ". Use account number instead (e.g., cswap --switch-to 1)."
239
240# A slot number, an alias, an email: in that order (`_resolve_account_identifier`).
241type Resolved is Data:
242  RSlot{num: String}
243  RNone{}
244  RAmb{msg: String}
245
246def resolve_email(+seq: Json.Json, +ident: String, +slots: List<&2, String>) -> Resolved:
247  match slots:
248    case Nil{}:
249      RNone{}
250    case Con{n, Nil{}}:
251      RSlot{n}
252    case _:
253      RAmb{ambiguous_msg(seq, ident, slots)}
254
255def resolve_by_alias(+seq: Json.Json, +ident: String, +m: Maybe<&2, String>) -> Resolved:
256  match m:
257    case Some{n}:
258      RSlot{n}
259    case None{}:
260      resolve_email(seq, ident, slots_for_email(seq, ident))
261
262def resolve_digits(+digits: Bool, +seq: Json.Json, +ident: String) -> Resolved:
263  match digits:
264    case True{}:
265      RSlot{ident}
266    case False{}:
267      resolve_by_alias(seq, ident, find_alias(seq, ident))
268
269def resolve(+seq: Json.Json, +ident: String) -> Resolved:
270  resolve_digits(SBase.all_digits(ident) && SBase.nonempty(ident), seq, ident)