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
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
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
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
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
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
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
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
182
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
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
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
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)