setcmd.bend

cswap-bend-cli@0.1.0.0 · 296 lines · 45 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/setload.bend as Setload
3import cswap-bend-cli@0.1.0.0/setwrite.bend as Setwrite
4import cswap-bend-cli@0.1.0.0/fs/write.bend as FWrite
5import cswap-bend-cli@0.1.0.0/json.bend as Json
6import cswap-bend-cli@0.1.0.0/fs.bend as Fs
7import cswap-bend-cli@0.1.0.0/act.bend as Act
8import cswap-bend-cli@0.1.0.0/cmd.bend as Cmd
9import cswap-bend-cli@0.1.0.0/settings.bend as Settings
10
11def ActM(-A: Type) -> Type:
12  Act.of(A)
13
14def ActM.pure(-A: Type, x: A) -> Act.of(A):
15  Act.pure(A, x)
16
17def 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
20def 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).
25def 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
32def 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
39def 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
46def 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
53def 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).
62def 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.
66def write_settings(+root: String, +j: Json.Json) -> Act.of(Unit):
67  do ActM<Unit>:
68    FWrite.mkdir_p(root)
69    FWrite.chmod("700", root)
70    FWrite.write_atomic(Setload.path(root), Json.emit_pretty(j), "600")
71
72def 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
80def list.out(+root: String, json: Bool, raw: Json.Json) -> Cmd.Out:
81  list.show(root, json, Setwrite.effective(raw))
82
83def 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
86def 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
94def 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
99def 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
106def 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
113def 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
116def set.msg(+sp: Settings.Spec, +v: Json.Json) -> String:
117  Settings.dotted(sp) ++ " = " ++ Setwrite.fmt.val(Setwrite.is_float(sp), v)
118
119def set.act(+root: String, +sp: Settings.Spec, +v: Json.Json) -> Act.of(Cmd.Out):
120  do ActM<Cmd.Out>:
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
125def 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
132def 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
139def run.set(+root: String, key: String, value: String) -> IO(Cmd.Out):
140  set.spec(root, Settings.spec_of(key), value)
141
142def dflt_of(sp: Settings.Spec) -> Json.Json:
143  match sp:
144    case Settings.Spec{section, key, kind, lo, hi, choices, dflt}:
145      dflt
146
147def unset.msg(+sp: Settings.Spec) -> String:
148  Settings.dotted(sp) ++ " unset (default: " ++ Setwrite.fmt.val(Setwrite.is_float(sp), dflt_of(sp)) ++ ")"
149
150def unset.write(+root: String, +sp: Settings.Spec, +raw: Json.Json) -> Act.of(Cmd.Out):
151  do ActM<Cmd.Out>:
152    w : Unit <- write_settings(root, Setwrite.unset_in(raw, sp))
153    Act.pure(Cmd.Out, Cmd.ok(unset.msg(sp)))
154
155def 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
162def section_of_spec(sp: Settings.Spec) -> String:
163  match sp:
164    case Settings.Spec{section, key, kind, lo, hi, choices, dflt}:
165      section
166
167def key_of(sp: Settings.Spec) -> String:
168  match sp:
169    case Settings.Spec{section, key, kind, lo, hi, choices, dflt}:
170      key
171
172def unset.act(+root: String, +sp: Settings.Spec) -> Act.of(Cmd.Out):
173  do ActM<Cmd.Out>:
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
177def 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
184def run.unset(+root: String, key: String) -> IO(Cmd.Out):
185  unset.spec(root, Settings.spec_of(key))
186
187type 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
195def join_sp(xs: List<String>) -> 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.
205def plan.none(rest: List<String>, 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
212def plan.get(rest: List<String>) -> 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
219def plan.unset(rest: List<String>) -> 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
226def plan.set(rest: List<String>) -> 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
235def plan.verb(l: Bool, g: Bool, s: Bool, u: Bool, p: Bool, a: String, rest: List<String>) -> 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.
251def plan.of(args: List<String>) -> 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
258def 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
265def 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
278def 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.
295def config(+root: String, args: List<String>, +json: Bool) -> IO(Cmd.Out):
296  run(root, plan.json(json, plan.of(args)), json)