setload.bend

cswap-bend-cli@0.1.0.0 · 339 lines · 61 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/fs/write.bend as FWrite
3import cswap-bend-cli@0.1.0.0/json.bend as Json
4import cswap-bend-cli@0.1.0.0/fs.bend as Fs
5import cswap-bend-cli@0.1.0.0/act.bend as Act
6import cswap-bend-cli@0.1.0.0/cmd.bend as Cmd
7import cswap-bend-cli@0.1.0.0/settings.bend as Settings
8
9def truthy.items(j: Json.Json) -> Bool:
10  match j:
11    case Json.JNil{}:
12      False{}
13    case _:
14      True{}
15
16# Python truthiness of a JSON value.
17def truthy(j: Json.Json) -> Bool:
18  match j:
19    case Json.JNull{}:
20      False{}
21    case Json.JBool{b}:
22      b
23    case Json.JNum{t}:
24      Bool.not(Settings.is_zero(Settings.lit(t)))
25    case Json.JStr{s}:
26      Bool.not(String.is_empty(s))
27    case Json.JArr{items}:
28      truthy.items(items)
29    case Json.JObj{fields}:
30      truthy.items(fields)
31    case _:
32      False{}
33
34def empty() -> Json.Json:
35  Json.of_pairs([])
36
37def obj_or_empty(j: Json.Json) -> Json.Json:
38  match j:
39    case Json.JObj{f}:
40      Json.JObj{f}
41    case _:
42      empty()
43
44def has_str(xs: List<&2, String>, +v: String) -> Bool:
45  match xs:
46    case Nil{}:
47      False{}
48    case Con{h, t}:
49      String.eq(h, v) || has_str(t, v)
50
51def get_or_null(+key: String, +j: Json.Json) -> Json.Json:
52  Maybe.default(&2, Json.Json, Json.get(key, j), Json.JNull{})
53
54def int_text(d: Settings.Dec) -> String:
55  match d:
56    case Settings.Dec{neg, i, f}:
57      i
58
59def norm.show(+isint: Bool, +d: Settings.Dec) -> Json.Json:
60  Json.JNum{Settings.sel.s(isint, int_text(d), Settings.repr(d))}
61
62def norm.fin(m: Maybe<&2, Settings.Dec>, isint: Bool, lo: String, hi: String, dflt: Json.Json) -> Json.Json:
63  match m:
64    case Some{d}:
65      norm.show(isint, Settings.clamp(d, Settings.lit(lo), Settings.lit(hi)))
66    case None{}:
67      dflt
68
69def norm.numv(isint: Bool, lo: String, hi: String, dflt: Json.Json, v: Json.Json) -> Json.Json:
70  match v:
71    case Json.JNum{t}:
72      norm.fin(Settings.num(t), isint, lo, hi, dflt)
73    case _:
74      dflt
75
76def norm.pick(ok: Bool, v: Json.Json, dflt: Json.Json) -> Json.Json:
77  match ok:
78    case True{}:
79      v
80    case False{}:
81      dflt
82
83def norm.choice(choices: String, dflt: Json.Json, v: Json.Json) -> Json.Json:
84  match v:
85    case Json.JStr{+s}:
86      norm.pick(has_str(String.split(choices, ','), s), Json.JStr{s}, dflt)
87    case _:
88      dflt
89
90def norm.str(dflt: Json.Json, v: Json.Json) -> Json.Json:
91  match v:
92    case Json.JStr{+s}:
93      norm.pick(Bool.not(String.is_empty(s)), Json.JStr{s}, dflt)
94    case _:
95      dflt
96
97# One setting's lenient value: clamped into range, or the default for a value
98# of the wrong type.
99def norm.kind(kind: Settings.Sort, lo: String, hi: String, choices: String, dflt: Json.Json, v: Json.Json) -> Json.Json:
100  match kind:
101    case Settings.KFloat{}:
102      norm.numv(False{}, lo, hi, dflt, v)
103    case Settings.KInt{}:
104      norm.numv(True{}, lo, hi, dflt, v)
105    case Settings.KBool{}:
106      Json.JBool{truthy(v)}
107    case Settings.KChoice{}:
108      norm.choice(choices, dflt, v)
109    case Settings.KString{}:
110      norm.str(dflt, v)
111
112def norm.one2(hit: Bool, key: String, v: Json.Json, out: Json.Json) -> Json.Json:
113  match hit:
114    case True{}:
115      Json.set(key, v, out)
116    case False{}:
117      out
118
119def norm.one(sp: Settings.Spec, sec: String, src: Json.Json, out: Json.Json) -> Json.Json:
120  match sp:
121    case Settings.Spec{section, +key, kind, lo, hi, choices, dflt}:
122      norm.one2(String.eq(section, sec), key, norm.kind(kind, lo, hi, choices, dflt, get_or_null(key, src)), out)
123
124def norm.sec(xs: List<&2, Settings.Spec>, +sec: String, +src: Json.Json, out: Json.Json) -> Json.Json:
125  match xs:
126    case Nil{}:
127      out
128    case Con{h, t}:
129      norm.sec(t, sec, src, norm.one(h, sec, src, out))
130
131def sect_obj(m: Maybe<&2, Json.Json>) -> Json.Json:
132  match m:
133    case Some{j}:
134      obj_or_empty(j)
135    case None{}:
136      empty()
137
138# The section object (`{}` when missing or not an object).
139def section_of(+raw: Json.Json, +sec: String) -> Json.Json:
140  sect_obj(Json.get(sec, raw))
141
142def norm.section2(+sec: String, base: Json.Json, +s: Json.Json) -> Json.Json:
143  Json.set(sec, norm.sec(Settings.specs(), sec, s, s), base)
144
145def norm.section(+sec: String, +base: Json.Json) -> Json.Json:
146  norm.section2(sec, base, section_of(base, sec))
147
148def ver.pick(m: Maybe<&2, Json.Json>, raw: Json.Json) -> Json.Json:
149  match m:
150    case Some{v}:
151      raw
152    case None{}:
153      Json.set("schemaVersion", Json.JNum{"1"}, raw)
154
155# raw.setdefault("schemaVersion", 1)
156def with_version(+raw: Json.Json) -> Json.Json:
157  ver.pick(Json.get("schemaVersion", raw), raw)
158
159# The whole settings object with every known key present: defaults filled in,
160# numbers clamped, wrong types replaced. Unknown keys and sections survive.
161def normalize(raw: Json.Json) -> Json.Json:
162  norm.section("ui", norm.section("autoswitch", with_version(obj_or_empty(raw))))
163
164def path(+root: String) -> String:
165  root ++ "/settings.json"
166
167def raw.parsed(r: Result<&2, &2, String, Json.Json>) -> Json.Json:
168  match r:
169    case Fail{e}:
170      empty()
171    case Done{j}:
172      obj_or_empty(j)
173
174def raw.of(r: Fs.Res(String)) -> Json.Json:
175  match r:
176    case Fail{e}:
177      empty()
178    case Done{t}:
179      raw.parsed(Json.parse(t))
180
181# settings.json as read by the lenient paths: `{}` for a missing, unreadable,
182# torn or non-object file.
183def load_raw(+root: String) -> IO(Json.Json):
184  IO.bind(Fs.Res(String), Json.Json, Fs.read_file(path(root)), r => IO.pure(Json.Json, raw.of(r)))
185
186# Lenient load: the full object, defaults filled, numbers clamped.
187def load(+root: String) -> IO(Json.Json):
188  IO.bind(Json.Json, Json.Json, load_raw(root), raw => IO.pure(Json.Json, normalize(raw)))
189
190def autoswitch(+s: Json.Json, +key: String) -> Json.Json:
191  get_or_null(key, get_or_null("autoswitch", s))
192
193def text_of(j: Json.Json) -> String:
194  match j:
195    case Json.JNum{t}:
196      t
197    case _:
198      ""
199
200# Decimal text as Python prints the float: "90.0".
201def threshold(s: Json.Json) -> String:
202  text_of(autoswitch(s, "threshold"))
203
204def interval_seconds(s: Json.Json) -> String:
205  text_of(autoswitch(s, "intervalSeconds"))
206
207def cooldown_seconds(s: Json.Json) -> String:
208  text_of(autoswitch(s, "cooldownSeconds"))
209
210def hysteresis_pct(s: Json.Json) -> String:
211  text_of(autoswitch(s, "hysteresisPct"))
212
213def strategy.of(j: Json.Json) -> String:
214  match j:
215    case Json.JStr{s}:
216      s
217    case _:
218      "best"
219
220def strategy(s: Json.Json) -> String:
221  strategy.of(autoswitch(s, "strategy"))
222
223def include_api_key_accounts(s: Json.Json) -> Bool:
224  truthy(autoswitch(s, "includeApiKeyAccounts"))
225
226def unhealthy_ticks(s: Json.Json) -> U32:
227  Maybe.default(&2, U32, U32.read(text_of(autoswitch(s, "unhealthyTicks"))), 3)
228
229# Tenths of a percent or second (floored) for integer comparisons.
230def threshold_tenths(s: Json.Json) -> U32:
231  Settings.tenths(threshold(s))
232
233def interval_tenths(s: Json.Json) -> U32:
234  Settings.tenths(interval_seconds(s))
235
236def cooldown_tenths(s: Json.Json) -> U32:
237  Settings.tenths(cooldown_seconds(s))
238
239def hysteresis_tenths(s: Json.Json) -> U32:
240  Settings.tenths(hysteresis_pct(s))
241
242def models.keep2(skip: Bool, +q: String, rest: List<&2, String>) -> List<&2, String>:
243  match skip:
244    case True{}:
245      rest
246    case False{}:
247      q <> rest
248
249def models.keep(+q: String, rest: List<&2, String>) -> List<&2, String>:
250  models.keep2(String.is_empty(q), q, rest)
251
252# Each part trimmed, empties dropped.
253def models.clean(ps: List<&2, String>) -> List<&2, String>:
254  match ps:
255    case Nil{}:
256      []
257    case Con{p, t}:
258      models.keep(String.trim(p), models.clean(t))
259
260def models.without2(same: Bool, +h: String, rest: List<&2, String>) -> List<&2, String>:
261  match same:
262    case True{}:
263      rest
264    case False{}:
265      h <> rest
266
267# The list without the entries equal to `low` ignoring case.
268def models.without(xs: List<&2, String>, +low: String) -> List<&2, String>:
269  match xs:
270    case Nil{}:
271      []
272    case Con{+h, t}:
273      models.without2(String.eq(String.to_lower(h), low), h, models.without(t, low))
274
275def models.dedup(fuel: Nat, xs: List<&2, String>) -> List<&2, String>:
276  match fuel xs:
277    case 0n _:
278      xs
279    case 1n+p Nil{}:
280      []
281    case 1n+p Con{+h, t}:
282      h <> models.dedup(p, models.without(t, String.to_lower(h)))
283
284def models.dedup2(+xs: List<&2, String>) -> List<&2, String>:
285  models.dedup(List.length(&2, String, xs), xs)
286
287# parse_model_names: split on `,`, trim, drop empties, de-duplicate
288# case-insensitively (the first spelling wins).
289def model_names(v: String) -> List<&2, String>:
290  models.dedup2(models.clean(String.split(v, ',')))
291
292def model.of(j: Json.Json) -> List<&2, String>:
293  match j:
294    case Json.JStr{s}:
295      model_names(s)
296    case _:
297      []
298
299# `autoswitch.model` as a list of names ([] = account-wide windows only).
300def model(s: Json.Json) -> List<&2, String>:
301  model.of(autoswitch(s, "model"))
302
303# A numeric flag clamped into the setting's range, as Settings.repr text; None for
304# text that is not a number.
305def clamp_text.fin(m: Maybe<&2, Settings.Dec>, lo: String, hi: String) -> Maybe<&2, String>:
306  match m:
307    case Some{d}:
308      Some{Settings.repr(Settings.clamp(d, Settings.lit(lo), Settings.lit(hi)))}
309    case None{}:
310      None{}
311
312def clamp_text(+s: String, lo: String, hi: String) -> Maybe<&2, String>:
313  clamp_text.fin(Settings.num(s), lo, hi)
314
315# `--threshold 120` -> "99.9"
316def clamp_threshold(s: String) -> Maybe<&2, String>:
317  clamp_text(s, "50", "99.9")
318
319# `--interval 5` -> "15.0"
320def clamp_interval(s: String) -> Maybe<&2, String>:
321  clamp_text(s, "15", "3600")
322
323def clamp_cooldown(s: String) -> Maybe<&2, String>:
324  clamp_text(s, "0", "86400")
325
326def clamp_hysteresis(s: String) -> Maybe<&2, String>:
327  clamp_text(s, "0", "50")
328
329# A --strategy flag: an unknown value falls back to the default.
330def clamp_strategy.pick(ok: Bool, s: String) -> String:
331  match ok:
332    case True{}:
333      s
334    case False{}:
335      "best"
336
337def clamp_strategy(+s: String) -> String:
338  clamp_strategy.pick(has_str(["best", "consume-first"], s), s)
339