daemon/config.bend
cswap-bend-cli@0.1.0.0 · 407 lines · 69 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/fmt/code.bend as Code
4
5
6
7
8
9type 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
12type 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
15def defaults() -> Cfg:
16 Cfg{9000, 60, 300, 1000, "best", False{}, 3, []}
17
18def sel_u(+c: Bool, +x: U32, +y: U32) -> U32:
19 match c:
20 case True{}:
21 x
22 case False{}:
23 y
24
25def 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
29
30def 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
37def 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
44def read_u32(+s: String) -> Maybe<&2, U32>:
45 read_u32b(digits(s) && Bool.not(String.is_empty(s)), s)
46
47def 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
54def 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
64
65def centi(+t: String) -> Maybe<&2, U32>:
66 centi2(String.split(t, '.'))
67
68def 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
75def 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
82def 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
90def 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
94def 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
98def 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
113def pick_strategy(+ok: Bool, +s: String, +d: String) -> String:
114 match ok:
115 case True{}:
116 s
117 case False{}:
118 d
119
120
121
122def 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
129def 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
136def keep_name(+h: String, +seen: List<&2, String>) -> Bool:
137 Bool.not(String.is_empty(h)) && Bool.not(has_ci(h, seen))
138
139def 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
146def 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
153def 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
160def 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
167def parse_models(+s: String) -> List<&2, String>:
168 dedupe(trim_all(String.split(s, ',')), [])
169
170def 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
178
179def 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
186def autoswitch_block(+j: Json.Json) -> Json.Json:
187 block_of(Json.get("autoswitch", j))
188
189def 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
195def from_settings(+j: Json.Json) -> Cfg:
196 from_block(autoswitch_block(j))
197
198def 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
206
207def from_text(+t: String) -> Cfg:
208 from_result(Json.parse(t))
209
210def no_flags() -> Flags:
211 Flags{False{}, 0, False{}, None{}, None{}, None{}, None{}, None{}, False{}}
212
213def 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
221def 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
228def split_eq(+a: String) -> List<&2, String>:
229 split_eq_b(String.starts_with(a, "--") && String.contains(a, "="), a)
230
231def 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
238def bad_value(+flag: String, +kind: String, +v: String) -> String:
239 "argument " ++ flag ++ ": invalid " ++ kind ++ " value: '" ++ v ++ "'"
240
241def 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
250def with_num(+f: Flags, +which: String, +n: U32) -> Flags:
251 with_num2(String.eq(which, "--threshold"), String.eq(which, "--interval"), f, n)
252
253def 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
260def set_num(+f: Flags, +which: String, +v: String) -> Result<&2, &2, String, Flags>:
261 set_num2(f, which, v, centi(v))
262
263def 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
268def 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
277def 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
280def 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
283def 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
286def 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
294def 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
301def 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
308def apply_value(+f: Flags, +a: String, +v: String) -> Result<&2, &2, String, Flags>:
309 apply_value3(String.eq(a, "--format"), f, a, v)
310
311def 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
318def rest_after(+a: String, +rest: List<&2, String>) -> List<&2, String>:
319 rest_after2(takes_value(a), rest)
320
321def 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
328def 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
337def 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
340def 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
348def 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
352
353def over_u(+m: Maybe<&2, U32>, +d: U32) -> U32:
354 match m:
355 case Some{v}:
356 v
357 case None{}:
358 d
359
360def over_s(+m: Maybe<&2, String>, +d: String) -> String:
361 match m:
362 case Some{v}:
363 v
364 case None{}:
365 d
366
367def 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
374def 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
379def cfg_threshold(+c: Cfg) -> U32:
380 match c:
381 case Cfg{th, iv, cd, hy, st, ia, ut, ms}:
382 th
383
384def cfg_models(+c: Cfg) -> List<&2, String>:
385 match c:
386 case Cfg{th, iv, cd, hy, st, ia, ut, ms}:
387 ms
388
389def cfg_interval(+c: Cfg) -> U32:
390 match c:
391 case Cfg{th, iv, cd, hy, st, ia, ut, ms}:
392 iv
393
394def cfg_strategy(+c: Cfg) -> String:
395 match c:
396 case Cfg{th, iv, cd, hy, st, ia, ut, ms}:
397 st
398
399def cfg_cooldown(+c: Cfg) -> U32:
400 match c:
401 case Cfg{th, iv, cd, hy, st, ia, ut, ms}:
402 cd
403
404def cfg_include_api(+c: Cfg) -> Bool:
405 match c:
406 case Cfg{th, iv, cd, hy, st, ia, ut, ms}:
407 ia