1 import Base 2 import ../json.bend as Json 3 import ../fmt/code.bend as Code 4 5 # The autoswitch settings (the `autoswitch` block of settings.json, loaded the 6 # lenient way of settings.py) and the flags of `cswap auto`, which override 7 # them. Percentages are centi-percent U32 (90 % = 9000); times are whole seconds. 8 9 type Cfg is Data: 10 Cfg{threshold: U32, interval: U32, cooldown: U32, hysteresis: U32, strategy: String, include_api: Bool, unhealthy: U32, models: List<&2, String>} 11 12 type Flags is Data: 13 Flags{once: Bool, fmt: U32, dry_run: Bool, threshold: Maybe<&2, U32>, interval: Maybe<&2, U32>, cooldown: Maybe<&2, U32>, strategy: Maybe<&2, String>, models: Maybe<&2, String>, include_api: Bool} 14 15 def defaults() -> Cfg: 16 Cfg{9000, 60, 300, 1000, "best", False{}, 3, []} 17 18 def sel_u(+c: Bool, +x: U32, +y: U32) -> U32: 19 match c: 20 case True{}: 21 x 22 case False{}: 23 y 24 25 def clamp(+v: U32, +lo: U32, +hi: U32) -> U32: 26 sel_u(U32.is_lt(v, lo), lo, sel_u(U32.is_gt(v, hi), hi, v)) 27 28 # ---- numbers ---------------------------------------------------------------------------------- 29 30 def digits(+s: String) -> Bool: 31 match s: 32 case SNil{}: 33 True{} 34 case SCon{c, t}: 35 Char.is_digit(c) && digits(t) 36 37 def read_u32b(+ok: Bool, +s: String) -> Maybe<&2, U32>: 38 match ok: 39 case True{}: 40 U32.read(s) 41 case False{}: 42 None{} 43 44 def read_u32(+s: String) -> Maybe<&2, U32>: 45 read_u32b(digits(s) && Bool.not(String.is_empty(s)), s) 46 47 def cents2(+i: Maybe<&2, U32>, +f: Maybe<&2, U32>) -> Maybe<&2, U32>: 48 match i f: 49 case Some{a} Some{b}: 50 Some{(a * 100 + b : U32)} 51 case _ _: 52 None{} 53 54 def centi2(+parts: List<&2, String>) -> Maybe<&2, U32>: 55 match parts: 56 case Con{i, Nil{}}: 57 cents2(read_u32(i), Some{0}) 58 case Con{i, Con{f, Nil{}}}: 59 cents2(read_u32(i), read_u32(String.take(f ++ "00", 2n))) 60 case _: 61 None{} 62 63 # "12.34" -> 1234, "7" -> 700, "5.5" -> 550; None for anything else (signs, 64 # exponents, text). Digits past the second decimal are dropped. 65 def centi(+t: String) -> Maybe<&2, U32>: 66 centi2(String.split(t, '.')) 67 68 def num_text(+m: Maybe<&2, Json.Json>) -> Maybe<&2, String>: 69 match m: 70 case Some{Json.JNum{t}}: 71 Some{t} 72 case _: 73 None{} 74 75 def centi_of(+m: Maybe<&2, String>) -> Maybe<&2, U32>: 76 match m: 77 case Some{t}: 78 centi(t) 79 case None{}: 80 None{} 81 82 def field_centi2(+m: Maybe<&2, U32>, +d: U32, +lo: U32, +hi: U32) -> U32: 83 match m: 84 case Some{v}: 85 clamp(v, lo, hi) 86 case None{}: 87 d 88 89 # A numeric settings field, centi, clamped; the default when absent or not a number. 90 def field_centi(+j: Json.Json, +key: String, +d: U32, +lo: U32, +hi: U32) -> U32: 91 field_centi2(centi_of(num_text(Json.get(key, j))), d, lo, hi) 92 93 # Whole seconds from a centi field (the fraction is dropped). 94 def field_secs(+j: Json.Json, +key: String, +d: U32, +lo: U32, +hi: U32) -> U32: 95 (field_centi(j, key, (d * 100 : U32), (lo * 100 : U32), (hi * 100 : U32)) / 100 : U32) 96 97 # A bool field is read by truthiness: the string "false" is true. 98 def truthy(+m: Maybe<&2, Json.Json>, +d: Bool) -> Bool: 99 match m: 100 case Some{Json.JBool{b}}: 101 b 102 case Some{Json.JNull{}}: 103 False{} 104 case Some{Json.JStr{s}}: 105 Bool.not(String.is_empty(s)) 106 case Some{Json.JNum{t}}: 107 Bool.not(String.eq(t, "0") || String.eq(t, "0.0")) 108 case Some{j}: 109 True{} 110 case None{}: 111 d 112 113 def pick_strategy(+ok: Bool, +s: String, +d: String) -> String: 114 match ok: 115 case True{}: 116 s 117 case False{}: 118 d 119 120 # ---- model names: comma list, trimmed, de-duplicated without regard to case --------------------- 121 122 def strategy_of(+m: Maybe<&2, Json.Json>, +d: String) -> String: 123 match m: 124 case Some{Json.JStr{s}}: 125 pick_strategy(String.eq(s, "best") || String.eq(s, "consume-first"), s, d) 126 case _: 127 d 128 129 def has_ci(+x: String, +xs: List<&2, String>) -> Bool: 130 match xs: 131 case Nil{}: 132 False{} 133 case Con{h, t}: 134 String.eq(String.to_lower(h), String.to_lower(x)) || has_ci(x, t) 135 136 def keep_name(+h: String, +seen: List<&2, String>) -> Bool: 137 Bool.not(String.is_empty(h)) && Bool.not(has_ci(h, seen)) 138 139 def seen_after(+keep: Bool, +h: String, +seen: List<&2, String>) -> List<&2, String>: 140 match keep: 141 case True{}: 142 List.append(&2, String, seen, [h]) 143 case False{}: 144 seen 145 146 def out_name(+keep: Bool, +h: String, +rest: List<&2, String>) -> List<&2, String>: 147 match keep: 148 case True{}: 149 h <> rest 150 case False{}: 151 rest 152 153 def dedupe(+xs: List<&2, String>, +seen: List<&2, String>) -> List<&2, String>: 154 match xs: 155 case Nil{}: 156 [] 157 case Con{h, t}: 158 out_name(keep_name(h, seen), h, dedupe(t, seen_after(keep_name(h, seen), h, seen))) 159 160 def trim_all(+xs: List<&2, String>) -> List<&2, String>: 161 match xs: 162 case Nil{}: 163 [] 164 case Con{h, t}: 165 String.trim(h) <> trim_all(t) 166 167 def parse_models(+s: String) -> List<&2, String>: 168 dedupe(trim_all(String.split(s, ',')), []) 169 170 def models_of(+m: Maybe<&2, Json.Json>) -> List<&2, String>: 171 match m: 172 case Some{Json.JStr{s}}: 173 parse_models(s) 174 case _: 175 [] 176 177 # ---- settings.json ------------------------------------------------------------------------------ 178 179 def block_of(+m: Maybe<&2, Json.Json>) -> Json.Json: 180 match m: 181 case Some{b}: 182 b 183 case None{}: 184 Json.JNull{} 185 186 def autoswitch_block(+j: Json.Json) -> Json.Json: 187 block_of(Json.get("autoswitch", j)) 188 189 def from_block(+b: Json.Json) -> Cfg: 190 Cfg{field_centi(b, "threshold", 9000, 5000, 9990), field_secs(b, "intervalSeconds", 60, 15, 3600), field_secs(b, "cooldownSeconds", 300, 0, 86400), 191 field_centi(b, "hysteresisPct", 1000, 0, 5000), strategy_of(Json.get("strategy", b), "best"), truthy(Json.get("includeApiKeyAccounts", b), False{}), 192 (field_centi(b, "unhealthyTicks", 300, 100, 10000) / 100 : U32), models_of(Json.get("model", b))} 193 194 # The lenient load: anything unreadable is the default, numbers are clamped. 195 def from_settings(+j: Json.Json) -> Cfg: 196 from_block(autoswitch_block(j)) 197 198 def from_result(r: Result<&2, &2, String, Json.Json>) -> Cfg: 199 match r: 200 case Done{j}: 201 from_settings(j) 202 case Fail{e}: 203 defaults() 204 205 # ---- flags -------------------------------------------------------------------------------------- 206 207 def from_text(+t: String) -> Cfg: 208 from_result(Json.parse(t)) 209 210 def no_flags() -> Flags: 211 Flags{False{}, 0, False{}, None{}, None{}, None{}, None{}, None{}, False{}} 212 213 def split_eq2(+parts: List<&2, String>) -> List<&2, String>: 214 match parts: 215 case Con{k, Con{v, rest}}: 216 k <> String.join(Con{v, rest}, "=") <> [] 217 case _: 218 parts 219 220 # `--flag value` and `--flag=value`. 221 def split_eq_b(+eq: Bool, +a: String) -> List<&2, String>: 222 match eq: 223 case True{}: 224 split_eq2(String.split(a, '=')) 225 case False{}: 226 [a] 227 228 def split_eq(+a: String) -> List<&2, String>: 229 split_eq_b(String.starts_with(a, "--") && String.contains(a, "="), a) 230 231 def normalize(+xs: List<&2, String>) -> List<&2, String>: 232 match xs: 233 case Nil{}: 234 [] 235 case Con{h, t}: 236 List.append(&2, String, split_eq(h), normalize(t)) 237 238 def bad_value(+flag: String, +kind: String, +v: String) -> String: 239 "argument " ++ flag ++ ": invalid " ++ kind ++ " value: '" ++ v ++ "'" 240 241 def with_num2(+is_th: Bool, +is_iv: Bool, +f: Flags, +n: U32) -> Flags: 242 match is_th is_iv f: 243 case True{} _ Flags{o, j, d, th, iv, cd, st, md, ia}: 244 Flags{o, j, d, Some{clamp(n, 5000, 9990)}, iv, cd, st, md, ia} 245 case False{} True{} Flags{o, j, d, th, iv, cd, st, md, ia}: 246 Flags{o, j, d, th, Some{clamp((n / 100 : U32), 15, 3600)}, cd, st, md, ia} 247 case False{} False{} Flags{o, j, d, th, iv, cd, st, md, ia}: 248 Flags{o, j, d, th, iv, Some{clamp((n / 100 : U32), 0, 86400)}, st, md, ia} 249 250 def with_num(+f: Flags, +which: String, +n: U32) -> Flags: 251 with_num2(String.eq(which, "--threshold"), String.eq(which, "--interval"), f, n) 252 253 def set_num2(+f: Flags, +which: String, +v: String, +c: Maybe<&2, U32>) -> Result<&2, &2, String, Flags>: 254 match c: 255 case None{}: 256 Fail{bad_value(which, "float", v)} 257 case Some{n}: 258 Done{with_num(f, which, n)} 259 260 def set_num(+f: Flags, +which: String, +v: String) -> Result<&2, &2, String, Flags>: 261 set_num2(f, which, v, centi(v)) 262 263 def set_flag(+f: Flags, +name: String) -> Flags: 264 match f: 265 case Flags{o, j, d, th, iv, cd, st, md, ia}: 266 Flags{o || String.eq(name, "--once"), Bool.pick(U32, String.eq(name, "--json"), 1, j), d || String.eq(name, "--dry-run"), th, iv, cd, st, md, ia || String.eq(name, "--include-api-key-accounts")} 267 268 def set_str2(+is_strategy: Bool, +ok: Bool, +f: Flags, +v: String) -> Result<&2, &2, String, Flags>: 269 match is_strategy ok f: 270 case True{} True{} Flags{o, j, d, th, iv, cd, st, md, ia}: 271 Done{Flags{o, j, d, th, iv, cd, Some{v}, md, ia}} 272 case True{} False{} Flags{o, j, d, th, iv, cd, st, md, ia}: 273 Fail{"argument --strategy: invalid choice: '" ++ v ++ "' (choose from 'best', 'consume-first')"} 274 case False{} _ Flags{o, j, d, th, iv, cd, st, md, ia}: 275 Done{Flags{o, j, d, th, iv, cd, st, Some{v}, ia}} 276 277 def set_str(+f: Flags, +which: String, +v: String) -> Result<&2, &2, String, Flags>: 278 set_str2(String.eq(which, "--strategy"), String.eq(v, "best") || String.eq(v, "consume-first"), f, v) 279 280 def takes_value(+a: String) -> Bool: 281 String.eq(a, "--format") || String.eq(a, "--threshold") || String.eq(a, "--interval") || String.eq(a, "--cooldown") || String.eq(a, "--strategy") || String.eq(a, "--model") 282 283 def is_bool_flag(+a: String) -> Bool: 284 String.eq(a, "--once") || String.eq(a, "--json") || String.eq(a, "--dry-run") || String.eq(a, "--include-api-key-accounts") 285 286 def apply_value2(+str_flag: Bool, +f: Flags, +a: String, +v: String) -> Result<&2, &2, String, Flags>: 287 match str_flag: 288 case True{}: 289 set_str(f, a, v) 290 case False{}: 291 set_num(f, a, v) 292 293 # `--format NAME`: the output format of the event stream (src/fmt/code.bend). 294 def set_fmt2(m: Maybe<&2, U32>, +f: Flags, +v: String) -> Result<&2, &2, String, Flags>: 295 match m f: 296 case Some{n} Flags{o, j, d, th, iv, cd, st, md, ia}: 297 Done{Flags{o, n, d, th, iv, cd, st, md, ia}} 298 case None{} _: 299 Fail{Code.invalid("--format", v)} 300 301 def apply_value3(+is_fmt: Bool, +f: Flags, +a: String, +v: String) -> Result<&2, &2, String, Flags>: 302 match is_fmt: 303 case True{}: 304 set_fmt2(Code.of_name(v), f, v) 305 case False{}: 306 apply_value2(String.eq(a, "--strategy") || String.eq(a, "--model"), f, a, v) 307 308 def apply_value(+f: Flags, +a: String, +v: String) -> Result<&2, &2, String, Flags>: 309 apply_value3(String.eq(a, "--format"), f, a, v) 310 311 def rest_after2(+valued: Bool, +rest: List<&2, String>) -> List<&2, String>: 312 match valued: 313 case True{}: 314 List.drop(&2, String, rest, 1n) 315 case False{}: 316 rest 317 318 def rest_after(+a: String, +rest: List<&2, String>) -> List<&2, String>: 319 rest_after2(takes_value(a), rest) 320 321 def valued_flag(+a: String, +rest: List<&2, String>, +f: Flags) -> Result<&2, &2, String, Flags>: 322 match rest: 323 case Nil{}: 324 Fail{"argument " ++ a ++ ": expected one argument"} 325 case Con{v, more}: 326 apply_value(f, a, v) 327 328 def advance2(+bool_flag: Bool, +valued: Bool, +a: String, +rest: List<&2, String>, +f: Flags) -> Result<&2, &2, String, Flags>: 329 match bool_flag valued: 330 case True{} _: 331 Done{set_flag(f, a)} 332 case False{} True{}: 333 valued_flag(a, rest, f) 334 case False{} False{}: 335 Fail{"unrecognized arguments: " ++ a} 336 337 def advance(+a: String, +rest: List<&2, String>, +f: Flags) -> Result<&2, &2, String, Flags>: 338 advance2(is_bool_flag(a), takes_value(a), a, rest, f) 339 340 def step(+fuel: Nat, +xs: List<&2, String>, fr: Result<&2, &2, String, Flags>) -> Result<&2, &2, String, Flags>: 341 match fuel xs fr: 342 case 1n++p Con{a, rest} Done{f}: 343 step(p, rest_after(a, rest), advance(a, rest, f)) 344 case _ _ _: 345 fr 346 347 # The flags of `cswap auto` (after the verb). Exit 2 territory on Fail. 348 def parse_flags(+args: List<&2, String>) -> Result<&2, &2, String, Flags>: 349 step(Nat.add(List.length(&2, String, args), 1n), normalize(args), Done{no_flags()}) 350 351 # ---- flags over settings ------------------------------------------------------------------------ 352 353 def over_u(+m: Maybe<&2, U32>, +d: U32) -> U32: 354 match m: 355 case Some{v}: 356 v 357 case None{}: 358 d 359 360 def over_s(+m: Maybe<&2, String>, +d: String) -> String: 361 match m: 362 case Some{v}: 363 v 364 case None{}: 365 d 366 367 def over_models(+m: Maybe<&2, String>, +d: List<&2, String>) -> List<&2, String>: 368 match m: 369 case Some{v}: 370 parse_models(v) 371 case None{}: 372 d 373 374 def merge(+c: Cfg, +f: Flags) -> Cfg: 375 match c f: 376 case Cfg{th, iv, cd, hy, st, ia, ut, ms} Flags{o, j, d, fth, fiv, fcd, fst, fmd, fia}: 377 Cfg{over_u(fth, th), over_u(fiv, iv), over_u(fcd, cd), hy, over_s(fst, st), ia || fia, ut, over_models(fmd, ms)} 378 379 def cfg_threshold(+c: Cfg) -> U32: 380 match c: 381 case Cfg{th, iv, cd, hy, st, ia, ut, ms}: 382 th 383 384 def cfg_models(+c: Cfg) -> List<&2, String>: 385 match c: 386 case Cfg{th, iv, cd, hy, st, ia, ut, ms}: 387 ms 388 389 def cfg_interval(+c: Cfg) -> U32: 390 match c: 391 case Cfg{th, iv, cd, hy, st, ia, ut, ms}: 392 iv 393 394 def cfg_strategy(+c: Cfg) -> String: 395 match c: 396 case Cfg{th, iv, cd, hy, st, ia, ut, ms}: 397 st 398 399 def cfg_cooldown(+c: Cfg) -> U32: 400 match c: 401 case Cfg{th, iv, cd, hy, st, ia, ut, ms}: 402 cd 403 404 def cfg_include_api(+c: Cfg) -> Bool: 405 match c: 406 case Cfg{th, iv, cd, hy, st, ia, ut, ms}: 407 ia