setwrite.bend

cswap-bend-cli@0.1.0.0 · 289 lines · 47 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/setload.bend as Setload
3import cswap-bend-cli@0.1.0.0/fs/write.bend as FWrite
4import cswap-bend-cli@0.1.0.0/json.bend as Json
5import cswap-bend-cli@0.1.0.0/fs.bend as Fs
6import cswap-bend-cli@0.1.0.0/act.bend as Act
7import cswap-bend-cli@0.1.0.0/cmd.bend as Cmd
8import cswap-bend-cli@0.1.0.0/settings.bend as Settings
9
10type Parsed is Data:
11  Bad{msg: String}
12  Good{value: Json.Json}
13
14def strip_sign(+t: String) -> String:
15  Settings.sel.s(String.starts_with(t, "-") || String.starts_with(t, "+"), String.drop(t, 1n), t)
16
17def special.w(+w: String) -> Bool:
18  String.eq(w, "inf") || String.eq(w, "infinity") || String.eq(w, "nan")
19
20# float() accepts these and they are never inside a range.
21def special(t: String) -> Bool:
22  special.w(String.to_lower(strip_sign(t)))
23
24def int.pick(ok: Bool, t: String) -> Maybe<&2, Settings.Dec>:
25  match ok:
26    case True{}:
27      Settings.num(t)
28    case False{}:
29      None{}
30
31# int(): an optional sign and digits only.
32def int.cand(+t: String) -> Maybe<&2, Settings.Dec>:
33  int.pick(Settings.all_digits(strip_sign(t)) && Bool.not(String.is_empty(strip_sign(t))), t)
34
35def cand.pick(isint: Bool, t: String) -> Maybe<&2, Settings.Dec>:
36  match isint:
37    case True{}:
38      int.cand(t)
39    case False{}:
40      Settings.num(t)
41
42def range_msg(dot: String, lo: String, hi: String) -> String:
43  dot ++ " must be between " ++ lo ++ " and " ++ hi
44
45def parse.chk(bad: Bool, isint: Bool, msg: String, d: Settings.Dec) -> Parsed:
46  match bad:
47    case True{}:
48      Bad{msg}
49    case False{}:
50      Good{Setload.norm.show(isint, d)}
51
52def parse.dec(m: Maybe<&2, Settings.Dec>, isint: Bool, +dot: String, raw: String, +lo: String, +hi: String) -> Parsed:
53  match m:
54    case Some{+d}:
55      parse.chk(Settings.below(d, Settings.lit(lo)) || Settings.above(d, Settings.lit(hi)), isint, range_msg(dot, lo, hi), d)
56    case None{}:
57      Bad{dot ++ " expects " ++ Settings.sel.s(isint, "an integer", "a number") ++ ", got '" ++ raw ++ "'"}
58
59def parse.number2(inf: Bool, +isint: Bool, +dot: String, raw: String, +lo: String, +hi: String, t: String) -> Parsed:
60  match inf:
61    case True{}:
62      Bad{range_msg(dot, lo, hi)}
63    case False{}:
64      parse.dec(cand.pick(isint, t), isint, dot, raw, lo, hi)
65
66def parse.number(+isint: Bool, dot: String, +raw: String, lo: String, hi: String) -> Parsed:
67  parse.number2(Bool.not(isint) && special(String.trim(raw)), isint, dot, raw, lo, hi, String.trim(raw))
68
69def bool.word2(t: Bool, f: Bool) -> Maybe<&2, Bool>:
70  match t f:
71    case True{} _:
72      Some{True{}}
73    case False{} True{}:
74      Some{False{}}
75    case False{} False{}:
76      None{}
77
78def bool.word(+w: String) -> Maybe<&2, Bool>:
79  bool.word2(String.eq(w, "true") || String.eq(w, "1") || String.eq(w, "yes"),
80    String.eq(w, "false") || String.eq(w, "0") || String.eq(w, "no"))
81
82def parse.bool2(m: Maybe<&2, Bool>, dot: String, raw: String) -> Parsed:
83  match m:
84    case Some{b}:
85      Good{Json.JBool{b}}
86    case None{}:
87      Bad{dot ++ " expects true or false (or 1/0, yes/no), got '" ++ raw ++ "'"}
88
89def parse.choice2(ok: Bool, dot: String, choices: String, raw: String) -> Parsed:
90  match ok:
91    case True{}:
92      Good{Json.JStr{raw}}
93    case False{}:
94      Bad{dot ++ " must be one of: " ++ String.join(String.split(choices, ','), ", ")}
95
96def parse.str2(Setload.empty: Bool, +dot: String, v: String) -> Parsed:
97  match Setload.empty:
98    case True{}:
99      Bad{dot ++ " expects a non-empty value; use 'cswap config unset " ++ dot ++ "' to clear it"}
100    case False{}:
101      Good{Json.JStr{v}}
102
103def parse.kind(kind: Settings.Sort, +dot: String, +raw: String, lo: String, hi: String, +choices: String) -> Parsed:
104  match kind:
105    case Settings.KFloat{}:
106      parse.number(False{}, dot, raw, lo, hi)
107    case Settings.KInt{}:
108      parse.number(True{}, dot, raw, lo, hi)
109    case Settings.KBool{}:
110      parse.bool2(bool.word(String.to_lower(String.trim(raw))), dot, raw)
111    case Settings.KChoice{}:
112      parse.choice2(Setload.has_str(String.split(choices, ','), raw), dot, choices, raw)
113    case Settings.KString{}:
114      parse.str2(String.is_empty(String.trim(raw)), dot, String.trim(raw))
115
116# Strictly parse the text given to `config set`: out of range or mistyped is a
117# Bad{message}, never clamped.
118def parse_value(sp: Settings.Spec, raw: String) -> Parsed:
119  match sp:
120    case Settings.Spec{section, key, kind, lo, hi, choices, dflt}:
121      parse.kind(kind, section ++ "." ++ key, raw, lo, hi, choices)
122
123def is_float.kind(k: Settings.Sort) -> Bool:
124  match k:
125    case Settings.KFloat{}:
126      True{}
127    case _:
128      False{}
129
130def is_float(sp: Settings.Spec) -> Bool:
131  match sp:
132    case Settings.Spec{section, key, kind, lo, hi, choices, dflt}:
133      is_float.kind(kind)
134
135def has.in(sec: Maybe<&2, Json.Json>, key: String) -> Bool:
136  match sec:
137    case Some{j}:
138      Maybe.is_some(&2, Json.Json, Json.get(key, j))
139    case None{}:
140      False{}
141
142# Is the key present in the raw file (an explicit default still counts)?
143def is_set(raw: Json.Json, sp: Settings.Spec) -> Bool:
144  match sp:
145    case Settings.Spec{section, key, kind, lo, hi, choices, dflt}:
146      has.in(Json.get(section, raw), key)
147
148# raw[section][key] = v, with schemaVersion defaulted; only that key written.
149def set_in(+raw: Json.Json, sp: Settings.Spec, v: Json.Json) -> Json.Json:
150  match sp:
151    case Settings.Spec{+section, key, kind, lo, hi, choices, dflt}:
152      Json.set(section, Json.set(key, v, Setload.section_of(raw, section)), Setload.with_version(raw))
153
154def obj_empty(j: Json.Json) -> Bool:
155  match j:
156    case Json.JObj{Json.JNil{}}:
157      True{}
158    case _:
159      False{}
160
161def unset.fin(gone: Bool, +section: String, +sect: Json.Json, r2: Json.Json) -> Json.Json:
162  match gone:
163    case True{}:
164      Json.del(section, r2)
165    case False{}:
166      Json.set(section, sect, r2)
167
168def unset.fin2(+sect: Json.Json, +section: String, r2: Json.Json) -> Json.Json:
169  unset.fin(obj_empty(sect), section, sect, r2)
170
171# del raw[section][key]; an emptied section goes too.
172def unset_in(+raw: Json.Json, sp: Settings.Spec) -> Json.Json:
173  match sp:
174    case Settings.Spec{+section, +key, kind, lo, hi, choices, dflt}:
175      unset.fin2(Json.del(key, Setload.section_of(raw, section)), section, Setload.with_version(raw))
176
177type Row is Data:
178  Row{sp: Settings.Spec, value: Json.Json, set: Bool}
179
180def value_of(+norm: Json.Json, sp: Settings.Spec) -> Json.Json:
181  match sp:
182    case Settings.Spec{+section, key, kind, lo, hi, choices, dflt}:
183      Setload.get_or_null(key, Setload.get_or_null(section, norm))
184
185def rows(+raw: Json.Json, +norm: Json.Json, xs: List<&2, Settings.Spec>) -> List<&2, Row>:
186  match xs:
187    case Nil{}:
188      []
189    case Con{+h, t}:
190      Row{h, value_of(norm, h), is_set(raw, h)} <> rows(raw, norm, t)
191
192# (spec, effective value, explicitly set?) per key, registry order.
193def effective(+raw: Json.Json) -> List<&2, Row>:
194  rows(raw, Setload.normalize(raw), Settings.specs())
195
196def fmt.num(isfloat: Bool, +t: String) -> String:
197  Settings.sel.s(isfloat, Settings.fmt(Settings.lit(t)), t)
198
199# format_setting_value
200def fmt.val(isfloat: Bool, j: Json.Json) -> String:
201  match j:
202    case Json.JNull{}:
203      "(none)"
204    case Json.JBool{b}:
205      Settings.sel.s(b, "true", "false")
206    case Json.JNum{t}:
207      fmt.num(isfloat, t)
208    case Json.JStr{s}:
209      s
210    case _:
211      ""
212
213def row.key(r: Row) -> String:
214  match r:
215    case Row{sp, value, set}:
216      Settings.dotted(sp)
217
218def row.val(r: Row) -> String:
219  match r:
220    case Row{+sp, value, set}:
221      fmt.val(is_float(sp), value)
222
223def row.json(r: Row) -> Json.Json:
224  match r:
225    case Row{sp, value, set}:
226      Json.of_pairs([("key", Json.JStr{Settings.dotted(sp)}), ("value", value), ("isSet", Json.JBool{set})])
227
228def rows.json(rs: List<&2, Row>) -> List<&2, Json.Json>:
229  match rs:
230    case Nil{}:
231      []
232    case Con{+h, t}:
233      row.json(h) <> rows.json(t)
234
235def row.find.pick(hit: Bool, r: Row, rest: Maybe<&2, Row>) -> Maybe<&2, Row>:
236  match hit:
237    case True{}:
238      Some{r}
239    case False{}:
240      rest
241
242def row.find(+key: String, rs: List<&2, Row>) -> Maybe<&2, Row>:
243  match rs:
244    case Nil{}:
245      None{}
246    case Con{+h, t}:
247      row.find.pick(String.eq(row.key(h), key), h, row.find(key, t))
248
249def keys.of(rs: List<&2, Row>) -> List<&2, String>:
250  match rs:
251    case Nil{}:
252      []
253    case Con{+h, t}:
254      row.key(h) <> keys.of(t)
255
256def vals.of(rs: List<&2, Row>) -> List<&2, String>:
257  match rs:
258    case Nil{}:
259      []
260    case Con{+h, t}:
261      row.val(h) <> vals.of(t)
262
263def width(xs: List<&2, String>, acc: Nat) -> Nat:
264  match xs:
265    case Nil{}:
266      acc
267    case Con{h, t}:
268      width(t, Nat.max(acc, String.length(h)))
269
270def pad(+s: String, n: Nat) -> String:
271  s ++ String.repeat(" ", Nat.sub(n, String.length(s)))
272
273def line(kw: Nat, vw: Nat, +r: Row) -> String:
274  match r:
275    case Row{sp, value, +set}:
276      pad(row.key(Row{sp, value, set}), kw) ++ "  " ++ pad(row.val(Row{sp, value, set}), vw) ++ Settings.sel.s(set, "", "  (default)")
277
278def lines(+kw: Nat, +vw: Nat, rs: List<&2, Row>) -> List<&2, String>:
279  match rs:
280    case Nil{}:
281      []
282    case Con{h, t}:
283      line(kw, vw, h) <> lines(kw, vw, t)
284
285# The text of `config list`: padded key and value columns, `(default)` on
286# the keys the file does not set.
287def list.text(+rs: List<&2, Row>) -> String:
288  String.join(lines(width(keys.of(rs), 0n), width(vals.of(rs), 0n), rs), "\n")
289