http.bend
cswap-bend-cli@0.1.0.0 · 206 lines · 33 definitions
1import Base
2
3
4
5
6
7def quote.pick(esc: Bool, c: Char, rest: String) -> String:
8 match esc:
9 case True{}:
10 SCon{'\\', SCon{c, rest}}
11 case False{}:
12 SCon{c, rest}
13
14def quote.char(+c: Char, rest: String) -> String:
15 quote.pick(Char.is_eq(c, '\\') || Char.is_eq(c, '"'), c, rest)
16
17
18def quote(s: String) -> String:
19 match s:
20 case SNil{}:
21 SNil{}
22 case SCon{h, t}:
23 quote.char(h, quote(t))
24
25def line(key: String, value: String) -> String:
26 key ++ " = \"" ++ quote(value) ++ "\"\n"
27
28def header.line(kv: String & String) -> String:
29 (k, v) = kv
30 line("header", k ++ ": " ++ v)
31
32def headers(hs: List<(String & String)>) -> String:
33 match hs:
34 case Nil{}:
35 ""
36 case Con{kv, t}:
37 header.line(kv) ++ headers(t)
38
39def body.pick(empty: Bool, body: String) -> String:
40 match empty:
41 case True{}:
42 ""
43 case False{}:
44 line("data-raw", body)
45
46def body.line(+body: String) -> String:
47 body.pick(String.is_empty(body), body)
48
49
50
51
52def config(method: String, url: String, hs: List<(String & String)>, body: String, timeout_s: U32) -> String:
53 line("url", url)
54 ++ line("request", method)
55 ++ "silent\nshow-error\ndump-header = -\n"
56 ++ line("max-time", U32.show(timeout_s))
57 ++ line("write-out", "\\n%{http_code}")
58 ++ headers(hs)
59 ++ body.line(body)
60
61
62
63def header.pick(hit: Bool, +name: String, +line: String) -> Maybe<&2, String>:
64 match hit:
65 case True{}:
66 Some{String.trim(String.drop(line, String.length(name)))}
67 case False{}:
68 None{}
69
70def header.match(+name: String, +line: String) -> Maybe<&2, String>:
71 header.pick(String.starts_with(String.to_lower(line), name), name, line)
72
73def header.find(+name: String, ls: List<&2, String>, found: Maybe<&2, String>) -> Maybe<&2, String>:
74 match ls found:
75 case _ Some{v}:
76 Some{v}
77 case Nil{} None{}:
78 None{}
79 case Con{h, t} None{}:
80 header.find(name, t, header.match(name, h))
81
82def header(+name: String, raw: String) -> Maybe<&2, String>:
83 header.find(name, String.lines(raw), None{})
84
85
86
87def run(cfg: String, timeout_ms: U32) -> IO(Result<&1, &1, U32 & String, U32 & (String & String)>):
88 Process.run("curl", ["--config", "-"], cfg, 4194304, timeout_ms)
89
90
91
92def after_headers.pick(blank: Bool, rest: List<&2, String>, deeper: List<&2, String>) -> List<&2, String>:
93 match blank:
94 case True{}:
95 rest
96 case False{}:
97 deeper
98
99
100def after_headers(ls: List<&2, String>) -> List<&2, String>:
101 match ls:
102 case Nil{}:
103 []
104 case Con{+h, +t}:
105 after_headers.pick(String.is_empty(String.trim(h)), t, after_headers(t))
106
107def last_line.pick(m: Maybe<&2, String>) -> String:
108 match m:
109 case None{}:
110 ""
111 case Some{s}:
112 s
113
114
115def status(raw: String) -> U32:
116 Maybe.default(&2, U32, U32.read(String.trim(last_line.pick(List.last(&2, String, String.lines(raw))))), 0)
117
118def body.of(+ls: List<&2, String>) -> String:
119 String.join(List.take(&2, String, ls, Nat.sub(List.length(&2, String, ls), 1n)), "\n")
120
121
122def body(raw: String) -> String:
123 body.of(after_headers(String.lines(raw)))
124
125
126
127def rewrite.pick(live: Bool, base: String, +url: String) -> String:
128 match live:
129 case True{}:
130 base ++ "/" ++ String.drop(url, 8n)
131 case False{}:
132 url
133
134
135
136def rewrite(+base: String, +url: String) -> String:
137 rewrite.pick(Bool.not(String.is_empty(base)) && String.starts_with(url, "https://"), base, url)
138
139def base.pick(r: Result<&1, &1, U32 & String, String>) -> String:
140 match r:
141 case Fail{e}:
142 ""
143 case Done{v}:
144 v
145
146
147def test_base() -> IO(String):
148 IO.bind(Result<&1, &1, U32 & String, String>, String, IO.get_env("CSWAP_TEST_HTTP_BASE"), r => IO.pure(String, base.pick(r)))
149
150
151
152
153type Reply is Data:
154 Reply{status: U32, head: String, body: String}
155
156def head.pick(blank: Bool, h: String, rest: String) -> String:
157 match blank:
158 case True{}:
159 ""
160 case False{}:
161 h ++ "\n" ++ rest
162
163
164def head.of(ls: List<&2, String>) -> String:
165 match ls:
166 case Nil{}:
167 ""
168 case Con{+h, t}:
169 head.pick(String.is_empty(String.trim(h)), h, head.of(t))
170
171def reply.ok(+out: String) -> Result<&2, &2, String, Reply>:
172 Done{Reply{status(out), head.of(String.lines(out)), body(out)}}
173
174
175def reply.exit(ok: Bool, timed_out: Bool, out: String) -> Result<&2, &2, String, Reply>:
176 match ok timed_out:
177 case True{} _:
178 reply.ok(out)
179 case False{} True{}:
180 Fail{"timeout"}
181 case False{} False{}:
182 Fail{"network"}
183
184def reply.of2(+st: U32, oe: String & String) -> Result<&2, &2, String, Reply>:
185 (o, e) = oe
186 reply.exit(U32.is_eq(st, 0), U32.is_eq(st, 28), o)
187
188def reply.of(r: U32 & (String & String)) -> Result<&2, &2, String, Reply>:
189 (st, oe) = r
190 reply.of2(st, oe)
191
192def reply(r: Result<&1, &1, U32 & String, U32 & (String & String)>) -> Result<&2, &2, String, Reply>:
193 match r:
194 case Fail{e}:
195 Fail{"network"}
196 case Done{v}:
197 reply.of(v)
198
199
200
201
202def request(method: String, url: String, hs: List<(String & String)>, body: String, +timeout_s: U32) -> IO(Result<&2, &2, String, Reply>):
203 IO.bind(String, Result<&2, &2, String, Reply>, test_base(),
204 base => IO.bind(Result<&1, &1, U32 & String, U32 & (String & String)>, Result<&2, &2, String, Reply>,
205 run(config(method, rewrite(base, url), hs, body, timeout_s), (timeout_s * 1000 + 3000 : U32)),
206 r => IO.pure(Result<&2, &2, String, Reply>, reply(r))))