rpcmsg.bend

cswap-bend-cli@0.1.0.0 · 241 lines · 35 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/cmd.bend as Cmd
4import cswap-bend-cli@0.1.0.0/fmt/out.bend as O
5
6# The pure half of `cswap rpc`: a request document becomes a command line, a
7# command's output becomes a response document. docs/API.md has the schema.
8#
9#   request   {"id": any, "cmd": "list", "args": ["..."], "opts": {"name": value}}
10#   success   {"id": ..., "ok": true,  "result": <the command's --format json document>}
11#   failure   {"id": ..., "ok": false, "error": {"code": "...", "message": "...", "exit": N}}
12#
13# `opts` names are options without the dashes: true is the bare flag, a string
14# or number is its value, false and null are left out. No effects.
15
16type Req is Data:
17  ReqOk{id: Json.Json, argv: List<&2, String>}
18  ReqBad{id: Json.Json, code: String, msg: String}
19
20# ---- the request ------------------------------------------------------------------
21
22type Bad is Data:
23  Bad{code: String, msg: String}
24
25def or_null(m: Maybe<&2, Json.Json>) -> Json.Json:
26  match m:
27    case Some{v}:
28      v
29    case None{}:
30      Json.JNull{}
31
32def item_str(+h: Json.Json) -> Result<&2, &2, String, String>:
33  match h:
34    case Json.JStr{s}:
35      Done{s}
36    case Json.JNum{t}:
37      Done{t}
38    case _:
39      Fail{"args must be strings or numbers"}
40
41def cons_str(x: Result<&2, &2, String, String>, rest: Result<&2, &2, String, List<&2, String>>) -> Result<&2, &2, String, List<&2, String>>:
42  match x rest:
43    case Done{a} Done{b}:
44      Done{a <> b}
45    case Fail{e} _:
46      Fail{e}
47    case _ Fail{e}:
48      Fail{e}
49
50def strs_of(items: Json.Json) -> Result<&2, &2, String, List<&2, String>>:
51  match items:
52    case Json.JCons{h, t}:
53      cons_str(item_str(h), strs_of(t))
54    case _:
55      Done{[]}
56
57def args_of(+v: Json.Json) -> Result<&2, &2, String, List<&2, String>>:
58  match v:
59    case Json.JNull{}:
60      Done{[]}
61    case Json.JArr{items}:
62      strs_of(items)
63    case _:
64      Fail{"args must be an array"}
65
66def dashes(+k: String) -> String:
67  Bool.pick(String, String.starts_with(k, "-"), k, "--" ++ k)
68
69def opt_flag(+k: String, +v: Json.Json) -> Result<&2, &2, String, List<&2, String>>:
70  match v:
71    case Json.JBool{True{}}:
72      Done{[dashes(k)]}
73    case Json.JBool{False{}}:
74      Done{[]}
75    case Json.JNull{}:
76      Done{[]}
77    case Json.JStr{s}:
78      Done{[dashes(k), s]}
79    case Json.JNum{t}:
80      Done{[dashes(k), t]}
81    case _:
82      Fail{"option '" ++ k ++ "' must be a string, a number or a boolean"}
83
84def cat(x: Result<&2, &2, String, List<&2, String>>, rest: Result<&2, &2, String, List<&2, String>>) -> Result<&2, &2, String, List<&2, String>>:
85  match x rest:
86    case Done{a} Done{b}:
87      Done{List.append(&2, String, a, b)}
88    case Fail{e} _:
89      Fail{e}
90    case _ Fail{e}:
91      Fail{e}
92
93def flags_of(fields: Json.Json) -> Result<&2, &2, String, List<&2, String>>:
94  match fields:
95    case Json.JField{k, v, t}:
96      cat(opt_flag(k, v), flags_of(t))
97    case _:
98      Done{[]}
99
100def opts_of(+v: Json.Json) -> Result<&2, &2, String, List<&2, String>>:
101  match v:
102    case Json.JNull{}:
103      Done{[]}
104    case Json.JObj{f}:
105      flags_of(f)
106    case _:
107      Fail{"opts must be an object"}
108
109# ---- which commands -------------------------------------------------------------------
110
111def allowed(+v: String) -> Bool:
112  String.eq(v, "list") || String.eq(v, "status") || String.eq(v, "switch") || String.eq(v, "add") || String.eq(v, "add-token") || String.eq(v, "remove")
113    || String.eq(v, "import-usage") || String.eq(v, "config") || String.eq(v, "profile") || String.eq(v, "upgrade") || String.eq(v, "help")
114
115def refused(+v: String) -> String:
116  Bool.pick(String, String.eq(v, "auto"), "auto is not available over rpc: the poller is the one decider (run `cswap auto`)",
117  Bool.pick(String, String.eq(v, "tui") || String.eq(v, "watch"), "interactive commands are not available over rpc",
118  Bool.pick(String, String.eq(v, "menubar"), "menubar is not supported in cswap-bend", "unknown command")))
119
120def cmd_ok(+v: String, +name: String) -> Result<&2, &2, Bad, String>:
121  Bool.pick(Result<&2, &2, Bad, String>, allowed(v), Done{v},
122  Bool.pick(Result<&2, &2, Bad, String>, String.eq(v, "auto") || String.eq(v, "tui") || String.eq(v, "watch") || String.eq(v, "menubar"),
123    Fail{Bad{"not_allowed", refused(v)}}, Fail{Bad{"unknown_command", "unknown command '" ++ name ++ "'"}}))
124
125# The command a name stands for (the CLI's aliases).
126def canon(+n: String) -> String:
127  Bool.pick(String, String.eq(n, "ls"), "list", Bool.pick(String, String.eq(n, "rm"), "remove", Bool.pick(String, String.eq(n, "update"), "upgrade", n)))
128
129# The program word for a command name; Fail names why it is not served.
130def cmd_word(+name: String) -> Result<&2, &2, Bad, String>:
131  Bool.pick(Result<&2, &2, Bad, String>, String.eq(name, "version"), Done{"--version"}, cmd_ok(canon(name), name))
132
133def cmd_of(+m: Maybe<&2, Json.Json>) -> Result<&2, &2, Bad, String>:
134  match m:
135    case Some{Json.JStr{s}}:
136      cmd_word(s)
137    case _:
138      Fail{Bad{"bad_request", "a request needs a \"cmd\" string"}}
139
140# ---- building ---------------------------------------------------------------------------
141
142def bad(+id: Json.Json, +code: String, +msg: String) -> Req:
143  ReqBad{id, code, msg}
144
145def build3(+id: Json.Json, +w: String, a: Result<&2, &2, String, List<&2, String>>, o: Result<&2, &2, String, List<&2, String>>) -> Req:
146  match a o:
147    case Done{xs} Done{fs}:
148      ReqOk{id, w <> List.append(&2, String, xs, List.append(&2, String, fs, ["--no-input"]))}
149    case Fail{e} _:
150      bad(id, "bad_request", e)
151    case _ Fail{e}:
152      bad(id, "bad_request", e)
153
154def build2(+id: Json.Json, c: Result<&2, &2, Bad, String>, +j: Json.Json) -> Req:
155  match c:
156    case Fail{Bad{code, msg}}:
157      bad(id, code, msg)
158    case Done{w}:
159      build3(id, w, args_of(or_null(Json.get("args", j))), opts_of(or_null(Json.get("opts", j))))
160
161def build(+j: Json.Json) -> Req:
162  build2(or_null(Json.get("id", j)), cmd_of(Json.get("cmd", j)), j)
163
164# The command line a request asks for, or why it cannot be served.
165def plan(+j: Json.Json) -> Req:
166  match j:
167    case Json.JObj{f}:
168      build(j)
169    case _:
170      bad(Json.JNull{}, "bad_request", "a request is an object: {\"id\": ..., \"cmd\": ..., \"args\": [...], \"opts\": {...}}")
171
172# ---- the response -----------------------------------------------------------------------------
173
174def ok_resp(+id: Json.Json, +result: Json.Json) -> Json.Json:
175  Json.of_pairs([("id", id), ("ok", Json.JBool{True{}}), ("result", result)])
176
177def err_resp(+id: Json.Json, +code: String, +msg: String, +exit: U32) -> Json.Json:
178  Json.of_pairs([("id", id), ("ok", Json.JBool{False{}}), ("error", Json.of_pairs([("code", Json.JStr{code}), ("message", Json.JStr{msg}), ("exit", Json.JNum{U32.show(exit)})]))])
179
180def last_part(ps: List<&2, String>) -> String:
181  match ps:
182    case Nil{}:
183      ""
184    case Con{h, Nil{}}:
185      h
186    case Con{h, Con{h2, t}}:
187      last_part(Con{h2, t})
188
189def cut_pick(hit: Bool, rest: String, more: String) -> String:
190  match hit:
191    case True{}:
192      rest
193    case False{}:
194      more
195
196# The text after the first `error: ` in `s`, else `whole`.
197def cut_error(s: String, +whole: String) -> String:
198  match s:
199    case SNil{}:
200      whole
201    case SCon{+c, +t}:
202      cut_pick(String.starts_with(s, "error: "), String.drop(s, 7n), cut_error(t, whole))
203
204# The message of a usage error: the last line, after its `error: `.
205def after_error(+err: String) -> String:
206  cut_error(String.trim(last_part(String.lines(String.trim(err)))), String.trim(last_part(String.lines(String.trim(err)))))
207
208def str_at(+j: Json.Json, +k: String) -> String:
209  Maybe.default(&2, String, Json.as_string(or_null(Json.get(k, j))), "")
210
211def fail_doc(+id: Json.Json, +exit: U32, +d: Json.Json, +err: String) -> Json.Json:
212  Bool.pick(Json.Json, Maybe.is_some(&2, Json.Json, Json.get("error", d)),
213    err_resp(id, str_at(or_null(Json.get("error", d)), "type"), str_at(or_null(Json.get("error", d)), "message"), exit),
214    err_resp(id, Bool.pick(String, U32.is_eq(exit, 2), "usage", "error"), after_error(err), exit))
215
216def err_of(m: Maybe<&2, Json.Json>, +id: Json.Json, +exit: U32, +err: String) -> Json.Json:
217  match m:
218    case Some{d}:
219      fail_doc(id, exit, d, err)
220    case None{}:
221      err_resp(id, Bool.pick(String, U32.is_eq(exit, 2), "usage", "error"), after_error(err), exit)
222
223def ok_of(m: Maybe<&2, Json.Json>, +id: Json.Json) -> Json.Json:
224  match m:
225    case Some{d}:
226      ok_resp(id, d)
227    case None{}:
228      ok_resp(id, Json.JObj{Json.JNil{}})
229
230def response2(good: Bool, +id: Json.Json, +exit: U32, +err: String, m: Maybe<&2, Json.Json>) -> Json.Json:
231  match good:
232    case True{}:
233      ok_of(m, id)
234    case False{}:
235      err_of(m, id, exit, err)
236
237# The response document for a command's output.
238def response(+id: Json.Json, o: Cmd.Out) -> Json.Json:
239  match o:
240    case Cmd.Out{+code, out, +err}:
241      response2(U32.is_eq(code, 0), id, code, err, O.doc_of(Cmd.Out{code, out, err}))