1 import Base 2 import ../json.bend as Json 3 import ./base.bend as SBase 4 5 # ---- sequence.json, as Json ------------------------------------------------------- 6 7 def jstr(+s: String) -> Json.Json: 8 Json.JStr{s} 9 10 def jnum(+n: U32) -> Json.Json: 11 Json.JNum{U32.show(n)} 12 13 def 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. 17 def 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 21 def 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 28 def get_or(+key: String, +j: Json.Json, +d: Json.Json) -> Json.Json: 29 get_or2(Json.get(key, j), d) 30 31 def 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. 39 def 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 42 def unwrap_obj(+j: Json.Json) -> Json.Json: 43 match j: 44 case Json.JObj{fields}: 45 fields 46 case _: 47 Json.JNil{} 48 49 def unwrap_arr(+j: Json.Json) -> Json.Json: 50 match j: 51 case Json.JArr{items}: 52 items 53 case _: 54 Json.JNil{} 55 56 def 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`. 60 def fields_of(+seq: Json.Json) -> Json.Json: 61 unwrap_obj(accounts_of(seq)) 62 63 def items_of(+seq: Json.Json) -> Json.Json: 64 unwrap_arr(get_or("sequence", seq, Json.JNil{})) 65 66 def 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 69 def 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 76 def 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`. 84 def find_slot(+seq: Json.Json, +email: String, +org: String) -> Maybe<&2, String>: 85 find_in(fields_of(seq), email, org) 86 87 def sel.u(+c: Bool, +x: U32, +y: U32) -> U32: 88 match c: 89 case True{}: 90 x 91 case False{}: 92 y 93 94 def max_u32(+a: U32, +b: U32) -> U32: 95 sel.u(U32.is_gt(a, b), a, b) 96 97 def str_u32(+s: String) -> U32: 98 Maybe.default(&2, U32, U32.read(s), 0) 99 100 def 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. 108 def next_slot(+seq: Json.Json) -> U32: 109 (max_in(fields_of(seq), 0) + 1 : U32) 110 111 def slot_record(+seq: Json.Json, +num: String) -> Maybe<&2, Json.Json>: 112 Json.get(num, accounts_of(seq)) 113 114 def is_api_key_rec(+rec: Json.Json) -> Bool: 115 String.eq(field_str(rec, "kind"), "api_key") 116 117 def 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`. 125 def 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 132 def seq_nums(+seq: Json.Json) -> List<&2, U32>: 133 nums_of(items_of(seq)) 134 135 def 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 144 def 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 151 def 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 158 def 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 165 def 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 172 def nums_json(+xs: List<&2, U32>) -> Json.Json: 173 Json.arr(nums_json2(xs)) 174 175 def stamp(+seq: Json.Json, +ts: String) -> Json.Json: 176 Json.set("lastUpdated", jstr(ts), seq) 177 178 def 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. 183 def 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`. 188 def 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 192 def 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 199 def find_alias(+seq: Json.Json, +alias: String) -> Maybe<&2, String>: 200 alias_in(fields_of(seq), String.to_lower(alias)) 201 202 def 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 209 def 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 216 def slots_for_email(+seq: Json.Json, +email: String) -> List<&2, String>: 217 emails_in(fields_of(seq), email) 218 219 def org_tag2(+n: String) -> String: 220 SBase.sel.s(String.is_empty(n), "personal", n) 221 222 def org_tag(+rec: Json.Json) -> String: 223 org_tag2(field_str(rec, "organizationName")) 224 225 def amb_org(+rec: Json.Json) -> Maybe<&2, String>: 226 Some{org_tag(rec)} 227 228 def 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. 236 def 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`). 241 type Resolved is Data: 242 RSlot{num: String} 243 RNone{} 244 RAmb{msg: String} 245 246 def 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 255 def 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 262 def 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 269 def resolve(+seq: Json.Json, +ident: String) -> Resolved: 270 resolve_digits(SBase.all_digits(ident) && SBase.nonempty(ident), seq, ident)