1 import Base 2 import ./setload.bend as Setload 3 import ./setwrite.bend as Setwrite 4 import ./fs/write.bend as FWrite 5 import ./json.bend as Json 6 import ./fs.bend as Fs 7 import ./act.bend as Act 8 import ./cmd.bend as Cmd 9 import ./settings.bend as Settings 10 11 def ActM(-A: Type) -> Type: 12 Act.of(A) 13 14 def ActM.pure(-A: Type, x: A) -> Act.of(A): 15 Act.pure(A, x) 16 17 def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B): 18 Act.bind(A, B, m, f) 19 20 def err.msg(e: U32 & String) -> String: 21 (c, m) = e 22 m 23 24 # The text of a failed Act as the command's output (exit 1). 25 def fin(r: Fs.Res(Cmd.Out)) -> Cmd.Out: 26 match r: 27 case Fail{e}: 28 Cmd.fail(err.msg(e)) 29 case Done{o}: 30 o 31 32 def read_w.obj(+p: String, j: Json.Json) -> Act.of(Json.Json): 33 match j: 34 case Json.JObj{f}: 35 Act.pure(Json.Json, Json.JObj{f}) 36 case _: 37 Act.fail(Json.Json, 1, p ++ " is not a JSON object; fix or delete it before changing settings") 38 39 def read_w.parse(+p: String, r: Result<&2, &2, String, Json.Json>) -> Act.of(Json.Json): 40 match r: 41 case Fail{e}: 42 Act.fail(Json.Json, 1, p ++ " is not valid JSON (" ++ e ++ "); fix or delete it before changing settings") 43 case Done{j}: 44 read_w.obj(p, j) 45 46 def read_w.text(+p: String, r: Fs.Res(String)) -> Act.of(Json.Json): 47 match r: 48 case Fail{e}: 49 Act.fail(Json.Json, 1, "could not read " ++ p ++ ": " ++ err.msg(e)) 50 case Done{t}: 51 read_w.parse(p, Json.parse(t)) 52 53 def read_w.go(+p: String, ex: Bool) -> Act.of(Json.Json): 54 match ex: 55 case False{}: 56 Act.pure(Json.Json, Setload.empty()) 57 case True{}: 58 IO.bind(Fs.Res(String), Fs.Res(Json.Json), Fs.read_file(p), r => read_w.text(p, r)) 59 60 # settings.json for a read-modify-write: a missing file is `{}`, a torn or 61 # non-object file is an error (never replaced by a near-Setload.empty one). 62 def load_raw_w(+root: String) -> Act.of(Json.Json): 63 IO.bind(Bool, Fs.Res(Json.Json), Fs.exists(Setload.path(root)), ex => read_w.go(Setload.path(root), ex)) 64 65 # Atomic 0600 write of settings.json; the backup dir is made and set 0700. 66 def write_settings(+root: String, +j: Json.Json) -> Act.of(Unit): 67 do ActM: 68 FWrite.mkdir_p(root) 69 FWrite.chmod("700", root) 70 FWrite.write_atomic(Setload.path(root), Json.emit_pretty(j), "600") 71 72 def list.show(+root: String, json: Bool, +rs: List<&2, Setwrite.Row>) -> Cmd.Out: 73 match json: 74 case True{}: 75 Cmd.doc(Json.of_pairs([("schemaVersion", Json.JNum{"1"}), ("path", Json.JStr{Setload.path(root)}), 76 ("settings", Json.arr(Setwrite.rows.json(rs)))])) 77 case False{}: 78 Cmd.ok(Setwrite.list.text(rs)) 79 80 def list.out(+root: String, json: Bool, raw: Json.Json) -> Cmd.Out: 81 list.show(root, json, Setwrite.effective(raw)) 82 83 def run.list(+root: String, json: Bool) -> IO(Cmd.Out): 84 IO.bind(Json.Json, Cmd.Out, Setload.load_raw(root), raw => IO.pure(Cmd.Out, list.out(root, json, raw))) 85 86 def get.show2(json: Bool, +sp: Settings.Spec, value: Json.Json, set: Bool, text: String) -> Cmd.Out: 87 match json: 88 case True{}: 89 Cmd.doc(Json.of_pairs([("schemaVersion", Json.JNum{"1"}), ("key", Json.JStr{Settings.dotted(sp)}), 90 ("value", value), ("isSet", Json.JBool{set})])) 91 case False{}: 92 Cmd.ok(text) 93 94 def get.show(json: Bool, +r: Setwrite.Row) -> Cmd.Out: 95 match r: 96 case Setwrite.Row{+sp, +value, +set}: 97 get.show2(json, sp, value, set, Setwrite.row.val(Setwrite.Row{sp, value, set})) 98 99 def get.row(json: Bool, m: Maybe<&2, Setwrite.Row>) -> Cmd.Out: 100 match m: 101 case Some{r}: 102 get.show(json, r) 103 case None{}: 104 Cmd.failed(json, "ConfigError", "unknown setting") 105 106 def get.spec(json: Bool, r: Result<&2, &2, String, Settings.Spec>, raw: Json.Json) -> Cmd.Out: 107 match r: 108 case Fail{m}: 109 Cmd.failed(json, "ConfigError", m) 110 case Done{sp}: 111 get.row(json, Setwrite.row.find(Settings.dotted(sp), Setwrite.effective(raw))) 112 113 def run.get(+root: String, key: String, json: Bool) -> IO(Cmd.Out): 114 IO.bind(Json.Json, Cmd.Out, Setload.load_raw(root), raw => IO.pure(Cmd.Out, get.spec(json, Settings.spec_of(key), raw))) 115 116 def set.msg(+sp: Settings.Spec, +v: Json.Json) -> String: 117 Settings.dotted(sp) ++ " = " ++ Setwrite.fmt.val(Setwrite.is_float(sp), v) 118 119 def set.act(+root: String, +sp: Settings.Spec, +v: Json.Json) -> Act.of(Cmd.Out): 120 do ActM: 121 +raw : Json.Json <- load_raw_w(root) 122 w : Unit <- write_settings(root, Setwrite.set_in(raw, sp, v)) 123 Act.pure(Cmd.Out, Cmd.ok(set.msg(sp, v))) 124 125 def set.parsed(+root: String, sp: Settings.Spec, p: Setwrite.Parsed) -> IO(Cmd.Out): 126 match p: 127 case Setwrite.Bad{m}: 128 IO.pure(Cmd.Out, Cmd.fail(m)) 129 case Setwrite.Good{v}: 130 IO.bind(Fs.Res(Cmd.Out), Cmd.Out, set.act(root, sp, v), r => IO.pure(Cmd.Out, fin(r))) 131 132 def set.spec(+root: String, r: Result<&2, &2, String, Settings.Spec>, value: String) -> IO(Cmd.Out): 133 match r: 134 case Fail{m}: 135 IO.pure(Cmd.Out, Cmd.fail(m)) 136 case Done{+sp}: 137 set.parsed(root, sp, Setwrite.parse_value(sp, value)) 138 139 def run.set(+root: String, key: String, value: String) -> IO(Cmd.Out): 140 set.spec(root, Settings.spec_of(key), value) 141 142 def dflt_of(sp: Settings.Spec) -> Json.Json: 143 match sp: 144 case Settings.Spec{section, key, kind, lo, hi, choices, dflt}: 145 dflt 146 147 def unset.msg(+sp: Settings.Spec) -> String: 148 Settings.dotted(sp) ++ " unset (default: " ++ Setwrite.fmt.val(Setwrite.is_float(sp), dflt_of(sp)) ++ ")" 149 150 def unset.write(+root: String, +sp: Settings.Spec, +raw: Json.Json) -> Act.of(Cmd.Out): 151 do ActM: 152 w : Unit <- write_settings(root, Setwrite.unset_in(raw, sp)) 153 Act.pure(Cmd.Out, Cmd.ok(unset.msg(sp))) 154 155 def unset.pick(present: Bool, +root: String, +sp: Settings.Spec, +raw: Json.Json) -> Act.of(Cmd.Out): 156 match present: 157 case True{}: 158 unset.write(root, sp, raw) 159 case False{}: 160 Act.pure(Cmd.Out, Cmd.Out{0, "", Settings.dotted(sp) ++ " is not set; nothing to do\n"}) 161 162 def section_of_spec(sp: Settings.Spec) -> String: 163 match sp: 164 case Settings.Spec{section, key, kind, lo, hi, choices, dflt}: 165 section 166 167 def key_of(sp: Settings.Spec) -> String: 168 match sp: 169 case Settings.Spec{section, key, kind, lo, hi, choices, dflt}: 170 key 171 172 def unset.act(+root: String, +sp: Settings.Spec) -> Act.of(Cmd.Out): 173 do ActM: 174 +raw : Json.Json <- load_raw_w(root) 175 unset.pick(Setwrite.has.in(Json.get(section_of_spec(sp), raw), key_of(sp)), root, sp, raw) 176 177 def unset.spec(+root: String, r: Result<&2, &2, String, Settings.Spec>) -> IO(Cmd.Out): 178 match r: 179 case Fail{m}: 180 IO.pure(Cmd.Out, Cmd.fail(m)) 181 case Done{sp}: 182 IO.bind(Fs.Res(Cmd.Out), Cmd.Out, unset.act(root, sp), r2 => IO.pure(Cmd.Out, fin(r2))) 183 184 def run.unset(+root: String, key: String) -> IO(Cmd.Out): 185 unset.spec(root, Settings.spec_of(key)) 186 187 type Plan is Data: 188 PList{} 189 PGet{key: String} 190 PSet{key: String, value: String} 191 PUnset{key: String} 192 PPath{} 193 PBad{msg: String} 194 195 def join_sp(xs: List) -> String: 196 match xs: 197 case Nil{}: 198 "" 199 case Con{h, Nil{}}: 200 h 201 case Con{h, t}: 202 h ++ " " ++ join_sp(t) 203 204 # Extra words after a complete verb are an argparse error. 205 def plan.none(rest: List, p: Plan) -> Plan: 206 match rest: 207 case Nil{}: 208 p 209 case Con{x, t}: 210 PBad{"unrecognized arguments: " ++ join_sp(Con{x, t})} 211 212 def plan.get(rest: List) -> Plan: 213 match rest: 214 case Nil{}: 215 PBad{"the following arguments are required: KEY"} 216 case Con{k, t}: 217 plan.none(t, PGet{k}) 218 219 def plan.unset(rest: List) -> Plan: 220 match rest: 221 case Nil{}: 222 PBad{"the following arguments are required: KEY"} 223 case Con{k, t}: 224 plan.none(t, PUnset{k}) 225 226 def plan.set(rest: List) -> Plan: 227 match rest: 228 case Nil{}: 229 PBad{"the following arguments are required: KEY, VALUE"} 230 case Con{k, Nil{}}: 231 PBad{"the following arguments are required: VALUE"} 232 case Con{k, Con{v, t}}: 233 plan.none(t, PSet{k, v}) 234 235 def plan.verb(l: Bool, g: Bool, s: Bool, u: Bool, p: Bool, a: String, rest: List) -> Plan: 236 match l g s u p: 237 case True{} _ _ _ _: 238 plan.none(rest, PList{}) 239 case False{} True{} _ _ _: 240 plan.get(rest) 241 case False{} False{} True{} _ _: 242 plan.set(rest) 243 case False{} False{} False{} True{} _: 244 plan.unset(rest) 245 case False{} False{} False{} False{} True{}: 246 plan.none(rest, PPath{}) 247 case _ _ _ _ _: 248 PBad{"argument action: invalid choice: '" ++ a ++ "' (choose from 'list', 'get', 'set', 'unset', 'path')"} 249 250 # `config [list | get KEY | set KEY VALUE | unset KEY | path]`, flags removed. 251 def plan.of(args: List) -> Plan: 252 match args: 253 case Nil{}: 254 PList{} 255 case Con{+a, rest}: 256 plan.verb(String.eq(a, "list"), String.eq(a, "get"), String.eq(a, "set"), String.eq(a, "unset"), String.eq(a, "path"), a, rest) 257 258 def plan.gate(json: Bool, p: Plan) -> Plan: 259 match json: 260 case True{}: 261 PBad{"--json can only be used with list or get"} 262 case False{}: 263 p 264 265 def plan.json(json: Bool, p: Plan) -> Plan: 266 match p: 267 case PSet{k, v}: 268 plan.gate(json, PSet{k, v}) 269 case PUnset{k}: 270 plan.gate(json, PUnset{k}) 271 case PPath{}: 272 plan.gate(json, PPath{}) 273 case _: 274 p 275 276 277 278 def run(+root: String, p: Plan, json: Bool) -> IO(Cmd.Out): 279 match p: 280 case PList{}: 281 run.list(root, json) 282 case PGet{key}: 283 run.get(root, key, json) 284 case PSet{key, value}: 285 run.set(root, key, value) 286 case PUnset{key}: 287 run.unset(root, key) 288 case PPath{}: 289 IO.pure(Cmd.Out, Cmd.ok(Setload.path(root))) 290 case PBad{msg}: 291 IO.pure(Cmd.Out, Cmd.Out{2, "", "cswap config: error: " ++ msg ++ "\n"}) 292 293 # `cswap config ARGS...`: `args` are the words after `config` with the 294 # `--json` / `--debug` flags already taken off, `json` the --json flag. 295 def config(+root: String, args: List, +json: Bool) -> IO(Cmd.Out): 296 run(root, plan.json(json, plan.of(args)), json)