1 import Base 2 import ./json.bend as Json 3 import ./cmd.bend as Cmd 4 import ./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": } 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 16 type 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 22 type Bad is Data: 23 Bad{code: String, msg: String} 24 25 def 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 32 def 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 41 def 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 50 def 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 57 def 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 66 def dashes(+k: String) -> String: 67 Bool.pick(String, String.starts_with(k, "-"), k, "--" ++ k) 68 69 def 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 84 def 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 93 def 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 100 def 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 111 def 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 115 def 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 120 def 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). 126 def 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. 130 def 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 133 def 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 142 def bad(+id: Json.Json, +code: String, +msg: String) -> Req: 143 ReqBad{id, code, msg} 144 145 def 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 154 def 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 161 def 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. 165 def 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 174 def ok_resp(+id: Json.Json, +result: Json.Json) -> Json.Json: 175 Json.of_pairs([("id", id), ("ok", Json.JBool{True{}}), ("result", result)]) 176 177 def 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 180 def 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 189 def 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`. 197 def 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: `. 205 def 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 208 def str_at(+j: Json.Json, +k: String) -> String: 209 Maybe.default(&2, String, Json.as_string(or_null(Json.get(k, j))), "") 210 211 def 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 216 def 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 223 def 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 230 def 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. 238 def 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}))