1 import Base 2 import ./setload.bend as Setload 3 import ./fs/write.bend as FWrite 4 import ./json.bend as Json 5 import ./fs.bend as Fs 6 import ./act.bend as Act 7 import ./cmd.bend as Cmd 8 import ./settings.bend as Settings 9 10 type Parsed is Data: 11 Bad{msg: String} 12 Good{value: Json.Json} 13 14 def strip_sign(+t: String) -> String: 15 Settings.sel.s(String.starts_with(t, "-") || String.starts_with(t, "+"), String.drop(t, 1n), t) 16 17 def 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. 21 def special(t: String) -> Bool: 22 special.w(String.to_lower(strip_sign(t))) 23 24 def 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. 32 def 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 35 def 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 42 def range_msg(dot: String, lo: String, hi: String) -> String: 43 dot ++ " must be between " ++ lo ++ " and " ++ hi 44 45 def 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 52 def 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 59 def 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 66 def 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 69 def 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 78 def 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 82 def 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 89 def 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 96 def 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 103 def 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. 118 def 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 123 def is_float.kind(k: Settings.Sort) -> Bool: 124 match k: 125 case Settings.KFloat{}: 126 True{} 127 case _: 128 False{} 129 130 def 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 135 def 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)? 143 def 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. 149 def 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 154 def obj_empty(j: Json.Json) -> Bool: 155 match j: 156 case Json.JObj{Json.JNil{}}: 157 True{} 158 case _: 159 False{} 160 161 def 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 168 def 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. 172 def 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 177 type Row is Data: 178 Row{sp: Settings.Spec, value: Json.Json, set: Bool} 179 180 def 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 185 def 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. 193 def effective(+raw: Json.Json) -> List<&2, Row>: 194 rows(raw, Setload.normalize(raw), Settings.specs()) 195 196 def fmt.num(isfloat: Bool, +t: String) -> String: 197 Settings.sel.s(isfloat, Settings.fmt(Settings.lit(t)), t) 198 199 # format_setting_value 200 def 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 213 def row.key(r: Row) -> String: 214 match r: 215 case Row{sp, value, set}: 216 Settings.dotted(sp) 217 218 def row.val(r: Row) -> String: 219 match r: 220 case Row{+sp, value, set}: 221 fmt.val(is_float(sp), value) 222 223 def 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 228 def 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 235 def 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 242 def 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 249 def 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 256 def 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 263 def 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 270 def pad(+s: String, n: Nat) -> String: 271 s ++ String.repeat(" ", Nat.sub(n, String.length(s))) 272 273 def 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 278 def 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. 287 def list.text(+rs: List<&2, Row>) -> String: 288 String.join(lines(width(keys.of(rs), 0n), width(vals.of(rs), 0n), rs), "\n") 289