tangled
alpha
login
or
join now
fuzzko.neocities.org
/
requwu
0
fork
atom
A cutesy HTTP client for Gleam
0
fork
atom
overview
issues
pulls
pipelines
feat: partially working http client
fuzzko.neocities.org
1 month ago
4f2ebccc
368edd84
+739
5 changed files
expand all
collapse all
unified
split
src
requwu
errors.gleam
internal
ascii_util.gleam
http_codec.gleam
request.gleam
requwu.gleam
+189
src/requwu.gleam
···
1
1
+
import gleam/bit_array
2
2
+
import gleam/function
3
3
+
import gleam/http
4
4
+
import gleam/http/response
5
5
+
import gleam/json
6
6
+
import gleam/list
7
7
+
import gleam/option
8
8
+
import gleam/result
9
9
+
import gleam/uri
10
10
+
import mug
11
11
+
import requwu/errors
12
12
+
import requwu/internal/http_codec
13
13
+
import requwu/request.{
14
14
+
type CompleteRequest, type RequestBuilder, CompleteRequest,
15
15
+
} as requwu_request
16
16
+
17
17
+
// TODO: detect cyclic redirection
18
18
+
19
19
+
pub fn request(method method: http.Method, to uri: String) -> RequestBuilder {
20
20
+
requwu_request.to(method, uri)
21
21
+
}
22
22
+
23
23
+
pub const get: fn(String) -> RequestBuilder = requwu_request.get
24
24
+
25
25
+
pub const head: fn(String) -> RequestBuilder = requwu_request.head
26
26
+
27
27
+
pub const post: fn(String) -> RequestBuilder = requwu_request.post
28
28
+
29
29
+
pub const put: fn(String) -> RequestBuilder = requwu_request.put
30
30
+
31
31
+
pub const delete: fn(String) -> RequestBuilder = requwu_request.delete
32
32
+
33
33
+
pub const connect: fn(String) -> RequestBuilder = requwu_request.connect
34
34
+
35
35
+
pub const options: fn(String) -> RequestBuilder = requwu_request.options
36
36
+
37
37
+
pub const trace: fn(String) -> RequestBuilder = requwu_request.trace
38
38
+
39
39
+
pub const header: fn(RequestBuilder, String, String) -> RequestBuilder = requwu_request.header
40
40
+
41
41
+
pub const headers: fn(RequestBuilder, List(#(String, String))) -> RequestBuilder = requwu_request.headers
42
42
+
43
43
+
pub const body: fn(RequestBuilder, String) -> RequestBuilder = requwu_request.body
44
44
+
45
45
+
pub const json: fn(RequestBuilder, json.Json) -> RequestBuilder = requwu_request.json
46
46
+
47
47
+
pub const basic_auth: fn(RequestBuilder, String, option.Option(String)) ->
48
48
+
RequestBuilder = requwu_request.basic_auth
49
49
+
50
50
+
pub const bearer_auth: fn(RequestBuilder, String) -> RequestBuilder = requwu_request.bearer_auth
51
51
+
52
52
+
pub const auth: fn(RequestBuilder, String) -> RequestBuilder = requwu_request.auth
53
53
+
54
54
+
pub const user_agent: fn(RequestBuilder, String) -> RequestBuilder = requwu_request.user_agent
55
55
+
56
56
+
pub const max_redirects: fn(RequestBuilder, Int) -> RequestBuilder = requwu_request.max_redirects
57
57
+
58
58
+
pub const timeout: fn(RequestBuilder, Int) -> RequestBuilder = requwu_request.timeout
59
59
+
60
60
+
pub const query: fn(RequestBuilder, String) -> RequestBuilder = requwu_request.query
61
61
+
62
62
+
pub fn send(
63
63
+
builder_request: RequestBuilder,
64
64
+
) -> Result(response.Response(String), errors.SendError) {
65
65
+
use complete_request <- result.try(
66
66
+
requwu_request.complete(builder_request)
67
67
+
|> result.map_error(errors.EncodeRequestError),
68
68
+
)
69
69
+
use socket <- result.try(new_socket(complete_request))
70
70
+
71
71
+
internal_request(socket, complete_request, 0)
72
72
+
}
73
73
+
74
74
+
fn new_socket(
75
75
+
complete_request: CompleteRequest,
76
76
+
) -> Result(mug.Socket, errors.SendError) {
77
77
+
use host <- result.try(
78
78
+
complete_request.uri.host
79
79
+
|> option.to_result(errors.EncodeRequestError(errors.NotAValidUri)),
80
80
+
)
81
81
+
use scheme <- result.try(
82
82
+
complete_request.uri.scheme
83
83
+
|> option.to_result(errors.EncodeRequestError(errors.NotAValidUri)),
84
84
+
)
85
85
+
use scheme <- result.try(
86
86
+
http.scheme_from_string(scheme)
87
87
+
|> result.replace_error(errors.EncodeRequestError(errors.NotAValidUri)),
88
88
+
)
89
89
+
90
90
+
mug.new(
91
91
+
host,
92
92
+
complete_request.uri.port |> option.unwrap(scheme_to_port(scheme)),
93
93
+
)
94
94
+
|> mug.timeout(complete_request.timeout)
95
95
+
|> case scheme {
96
96
+
http.Https -> mug.tls
97
97
+
_ -> function.identity
98
98
+
}
99
99
+
|> mug.connect
100
100
+
|> result.map_error(errors.FailedToConnect)
101
101
+
}
102
102
+
103
103
+
fn internal_request(
104
104
+
socket: mug.Socket,
105
105
+
complete_request: CompleteRequest,
106
106
+
redirects: Int,
107
107
+
) -> Result(response.Response(String), errors.SendError) {
108
108
+
let message_request = http_codec.encode_request(complete_request)
109
109
+
use _ <- result.try(
110
110
+
mug.send(socket, message_request)
111
111
+
|> result.map_error(errors.SocketSendError),
112
112
+
)
113
113
+
114
114
+
use response <- result.try(read_response(socket, complete_request))
115
115
+
116
116
+
case response.status, response.get_header(response, "location") {
117
117
+
// TODO: support 300 multiple choice via location header
118
118
+
301, Ok(location)
119
119
+
| 302, Ok(location)
120
120
+
| 303, Ok(location)
121
121
+
| 307, Ok(location)
122
122
+
| 308, Ok(location)
123
123
+
-> {
124
124
+
let redirects = redirects + 1
125
125
+
case
126
126
+
redirects <= complete_request.max_redirects
127
127
+
|| complete_request.max_redirects < 0
128
128
+
{
129
129
+
True -> {
130
130
+
use uri <- result.try(
131
131
+
uri.parse(location)
132
132
+
|> result.replace_error(errors.EncodeRequestError(
133
133
+
errors.FailedToParseUri,
134
134
+
)),
135
135
+
)
136
136
+
use host <- result.try(option.to_result(
137
137
+
uri.host,
138
138
+
errors.EncodeRequestError(errors.FailedToParseUri),
139
139
+
))
140
140
+
let new_request =
141
141
+
CompleteRequest(
142
142
+
..complete_request,
143
143
+
uri:,
144
144
+
headers: complete_request.headers
145
145
+
|> list.key_set("host", host),
146
146
+
)
147
147
+
148
148
+
let _ = mug.shutdown(socket)
149
149
+
use new_socket <- result.try(new_socket(new_request))
150
150
+
151
151
+
internal_request(new_socket, new_request, redirects)
152
152
+
}
153
153
+
False -> Ok(response)
154
154
+
}
155
155
+
}
156
156
+
_, _ -> Ok(response)
157
157
+
}
158
158
+
}
159
159
+
160
160
+
fn read_response(
161
161
+
socket: mug.Socket,
162
162
+
request: CompleteRequest,
163
163
+
) -> Result(response.Response(String), errors.SendError) {
164
164
+
read_response_loop(<<>>, socket, request)
165
165
+
}
166
166
+
167
167
+
fn read_response_loop(
168
168
+
acc: BitArray,
169
169
+
socket: mug.Socket,
170
170
+
request: CompleteRequest,
171
171
+
) -> Result(response.Response(String), errors.SendError) {
172
172
+
use message_response <- result.try(
173
173
+
mug.receive(socket, request.timeout)
174
174
+
|> result.map_error(errors.SocketRecvError),
175
175
+
)
176
176
+
let acc = bit_array.append(acc, message_response)
177
177
+
case http_codec.decode_response(acc) {
178
178
+
Ok(response) -> Ok(response)
179
179
+
Error(errors.IncompleteMessage) -> read_response_loop(acc, socket, request)
180
180
+
Error(error) -> Error(errors.DecodeResponseError(error))
181
181
+
}
182
182
+
}
183
183
+
184
184
+
fn scheme_to_port(scheme: http.Scheme) -> Int {
185
185
+
case scheme {
186
186
+
http.Http -> 80
187
187
+
http.Https -> 443
188
188
+
}
189
189
+
}
+24
src/requwu/errors.gleam
···
1
1
+
import mug
2
2
+
3
3
+
pub type EncodeRequestError {
4
4
+
FailedToParseUri
5
5
+
NotAValidUri
6
6
+
}
7
7
+
8
8
+
pub type DecodeResponseError {
9
9
+
InvalidHttpVersion(BitArray)
10
10
+
IncompleteMessage
11
11
+
MalformedMessage
12
12
+
MalformedHeaderTable
13
13
+
MalformedChunkedContent
14
14
+
IntParseFailed
15
15
+
BitArrayConversionFailed
16
16
+
}
17
17
+
18
18
+
pub type SendError {
19
19
+
FailedToConnect(mug.ConnectError)
20
20
+
SocketSendError(mug.Error)
21
21
+
SocketRecvError(mug.Error)
22
22
+
EncodeRequestError(EncodeRequestError)
23
23
+
DecodeResponseError(DecodeResponseError)
24
24
+
}
+80
src/requwu/internal/ascii_util.gleam
···
1
1
+
import gleam/bit_array
2
2
+
import gleam/list
3
3
+
4
4
+
pub fn take_nth(bits: BitArray, n: Int) -> #(BitArray, BitArray) {
5
5
+
case n {
6
6
+
0 -> #(<<>>, bits)
7
7
+
_ -> take_nth_loop(<<>>, bits, n)
8
8
+
}
9
9
+
}
10
10
+
11
11
+
fn take_nth_loop(acc: BitArray, rest: BitArray, n: Int) -> #(BitArray, BitArray) {
12
12
+
case n {
13
13
+
0 -> #(acc, rest)
14
14
+
_ ->
15
15
+
case rest {
16
16
+
<<char:unsigned-int, rest:bits>> ->
17
17
+
take_nth_loop(<<acc:bits, char>>, rest, n - 1)
18
18
+
// return if rest = <<>> while n > 0
19
19
+
_ -> #(acc, rest)
20
20
+
}
21
21
+
}
22
22
+
}
23
23
+
24
24
+
pub fn take_till(bits: BitArray, needle: BitArray) -> #(BitArray, BitArray) {
25
25
+
case needle {
26
26
+
<<>> -> #(bits, <<>>)
27
27
+
_ -> take_till_loop(<<>>, bits, needle)
28
28
+
}
29
29
+
}
30
30
+
31
31
+
fn take_till_loop(
32
32
+
acc: BitArray,
33
33
+
rest: BitArray,
34
34
+
needle: BitArray,
35
35
+
) -> #(BitArray, BitArray) {
36
36
+
case rest {
37
37
+
<<>> -> #(acc, <<>>)
38
38
+
_ -> {
39
39
+
// check if nth part of rest is actually the needle we need
40
40
+
let #(match, _) = take_nth(rest, bit_array.byte_size(needle))
41
41
+
case match {
42
42
+
_ if match == needle -> #(acc, rest)
43
43
+
_ -> {
44
44
+
// trim by 1 if it's not because it needs accuracy
45
45
+
let #(char, rest) = take_nth(rest, 1)
46
46
+
take_till_loop(<<acc:bits, char:bits>>, rest, needle)
47
47
+
}
48
48
+
}
49
49
+
}
50
50
+
}
51
51
+
}
52
52
+
53
53
+
pub fn contains(bits: BitArray, needles: List(BitArray)) -> Bool {
54
54
+
case needles {
55
55
+
[] -> True
56
56
+
_ -> contains_loop(bits, needles)
57
57
+
}
58
58
+
}
59
59
+
60
60
+
fn contains_loop(rest: BitArray, needles: List(BitArray)) -> Bool {
61
61
+
case rest {
62
62
+
<<>> -> False
63
63
+
_ -> {
64
64
+
let match =
65
65
+
// check if the start of bitarray is actually one of the needles
66
66
+
list.any(needles, fn(needle) {
67
67
+
let #(match, _) = take_nth(rest, bit_array.byte_size(needle))
68
68
+
match == needle
69
69
+
})
70
70
+
case match {
71
71
+
False -> {
72
72
+
// trim by 1 if it's not
73
73
+
let #(_, rest) = take_nth(rest, 1)
74
74
+
contains_loop(rest, needles)
75
75
+
}
76
76
+
_ -> True
77
77
+
}
78
78
+
}
79
79
+
}
80
80
+
}
+231
src/requwu/internal/http_codec.gleam
···
1
1
+
import given
2
2
+
import gleam/bit_array
3
3
+
import gleam/bytes_tree as btree
4
4
+
import gleam/http
5
5
+
import gleam/http/response
6
6
+
import gleam/int
7
7
+
import gleam/list
8
8
+
import gleam/option
9
9
+
import gleam/result
10
10
+
import gleam/string
11
11
+
import requwu/errors
12
12
+
import requwu/internal/ascii_util
13
13
+
import requwu/request
14
14
+
15
15
+
const crlf: BitArray = <<"\r\n">>
16
16
+
17
17
+
const space: BitArray = <<" ">>
18
18
+
19
19
+
pub fn encode_request(request: request.CompleteRequest) -> BitArray {
20
20
+
let query =
21
21
+
request.uri.query
22
22
+
|> option.map(fn(query) { "?" <> query })
23
23
+
|> option.unwrap("")
24
24
+
25
25
+
let request_line =
26
26
+
btree.new()
27
27
+
|> btree.append_string(http.method_to_string(request.method))
28
28
+
|> btree.append(space)
29
29
+
|> btree.append_string(normalise_path(request.uri.path) <> query)
30
30
+
|> btree.append(space)
31
31
+
|> btree.append_string(http_version_to_string(request.version))
32
32
+
33
33
+
let headers =
34
34
+
list.map(request.headers, fn(header) {
35
35
+
let #(name, value) = header
36
36
+
name <> ": " <> value
37
37
+
})
38
38
+
|> string.join("\r\n")
39
39
+
|> string.to_option
40
40
+
|> option.map(fn(headers) { headers <> "\r\n" })
41
41
+
|> option.unwrap("")
42
42
+
43
43
+
btree.new()
44
44
+
|> btree.append_tree(request_line)
45
45
+
|> btree.append(crlf)
46
46
+
|> btree.append_string(headers)
47
47
+
|> btree.append(crlf)
48
48
+
|> btree.append_string(request.body)
49
49
+
|> btree.to_bit_array
50
50
+
}
51
51
+
52
52
+
pub fn decode_response(
53
53
+
msg: BitArray,
54
54
+
) -> Result(response.Response(String), errors.DecodeResponseError) {
55
55
+
// * ascii_util.take_nth(rest, 1) means that we throw away space
56
56
+
// * ascii_util.take_nth(rest, 2) means that we throw away CRLF
57
57
+
58
58
+
let #(http_version, rest) = ascii_util.take_till(msg, space)
59
59
+
use <- given.that(http_version == <<"HTTP/1.1">>, else_return: fn() {
60
60
+
Error(errors.InvalidHttpVersion(http_version))
61
61
+
})
62
62
+
63
63
+
let #(_, rest) = ascii_util.take_nth(rest, 1)
64
64
+
let #(status_code, rest) = ascii_util.take_till(rest, space)
65
65
+
use status_code <- result.try(
66
66
+
bit_array.to_string(status_code)
67
67
+
|> result.replace_error(errors.BitArrayConversionFailed),
68
68
+
)
69
69
+
use status <- result.try(
70
70
+
int.base_parse(status_code, 10)
71
71
+
|> result.replace_error(errors.IntParseFailed),
72
72
+
)
73
73
+
74
74
+
let #(_, rest) = ascii_util.take_nth(rest, 1)
75
75
+
let #(_reason, rest) = ascii_util.take_till(rest, crlf)
76
76
+
77
77
+
let #(_, rest) = ascii_util.take_nth(rest, 2)
78
78
+
use #(headers, rest) <- result.try(parse_header_loop([], rest))
79
79
+
80
80
+
let #(_, body) = ascii_util.take_nth(rest, 2)
81
81
+
case
82
82
+
list.key_find(headers, "content-length"),
83
83
+
list.key_find(headers, "transfer-encoding")
84
84
+
|> result.unwrap("")
85
85
+
|> string.contains("chunked")
86
86
+
{
87
87
+
Ok(length), False -> {
88
88
+
use length <- result.try(
89
89
+
int.base_parse(length, 10)
90
90
+
|> result.replace_error(errors.IntParseFailed),
91
91
+
)
92
92
+
case bit_array.byte_size(body) < length {
93
93
+
True -> Error(errors.IncompleteMessage)
94
94
+
False -> {
95
95
+
use body <- result.try(
96
96
+
bit_array.to_string(body)
97
97
+
|> result.replace_error(errors.BitArrayConversionFailed),
98
98
+
)
99
99
+
Ok(response.Response(status:, headers:, body:))
100
100
+
}
101
101
+
}
102
102
+
}
103
103
+
Error(Nil), True -> {
104
104
+
// check if at the end of body is CRLF
105
105
+
case bit_array.slice(body, bit_array.byte_size(body), -2) {
106
106
+
Ok(<<"\r\n">>) -> {
107
107
+
use body <- result.try(unwrap_chunked(body))
108
108
+
use body <- result.try(
109
109
+
bit_array.to_string(body)
110
110
+
|> result.replace_error(errors.BitArrayConversionFailed),
111
111
+
)
112
112
+
Ok(response.Response(status:, headers:, body:))
113
113
+
}
114
114
+
_ -> Error(errors.IncompleteMessage)
115
115
+
}
116
116
+
}
117
117
+
_, _ -> Error(errors.MalformedMessage)
118
118
+
}
119
119
+
}
120
120
+
121
121
+
fn http_version_to_string(version: request.HttpVersion) -> String {
122
122
+
"HTTP/"
123
123
+
<> case version {
124
124
+
request.Http11 -> "1.1"
125
125
+
}
126
126
+
}
127
127
+
128
128
+
fn normalise_path(path: String) {
129
129
+
case path {
130
130
+
"" -> "/"
131
131
+
_ -> path
132
132
+
}
133
133
+
}
134
134
+
135
135
+
fn parse_header_loop(
136
136
+
acc: List(#(String, String)),
137
137
+
rest: BitArray,
138
138
+
) -> Result(#(List(#(String, String)), BitArray), errors.DecodeResponseError) {
139
139
+
let #(match, _) = ascii_util.take_nth(rest, 2)
140
140
+
case match {
141
141
+
// if we found CRLF, that means we found an end
142
142
+
<<"\r\n">> -> Ok(#(acc, rest))
143
143
+
_ -> {
144
144
+
let #(name, rest) = ascii_util.take_till(rest, <<": ">>)
145
145
+
use <- given.not(name == <<>>, else_return: fn() {
146
146
+
Error(errors.MalformedHeaderTable)
147
147
+
})
148
148
+
149
149
+
// throw ": " away
150
150
+
let #(_, rest) = ascii_util.take_nth(rest, 2)
151
151
+
let #(value, rest) = ascii_util.take_till(rest, crlf)
152
152
+
153
153
+
use name <- result.try(
154
154
+
bit_array.to_string(name)
155
155
+
|> result.replace_error(errors.BitArrayConversionFailed),
156
156
+
)
157
157
+
use value <- result.try(
158
158
+
bit_array.to_string(value)
159
159
+
|> result.replace_error(errors.BitArrayConversionFailed),
160
160
+
)
161
161
+
let name = string.lowercase(name)
162
162
+
let value = string.lowercase(value) |> string.trim_end
163
163
+
164
164
+
// throw CRLF away so the first case doesn't get messed up
165
165
+
let #(_, rest) = ascii_util.take_nth(rest, 2)
166
166
+
parse_header_loop([#(name, value), ..acc], rest)
167
167
+
}
168
168
+
}
169
169
+
}
170
170
+
171
171
+
pub fn unwrap_chunked(
172
172
+
chunks: BitArray,
173
173
+
) -> Result(BitArray, errors.DecodeResponseError) {
174
174
+
unwrap_chunk_loop(<<>>, chunks)
175
175
+
}
176
176
+
177
177
+
fn unwrap_chunk_loop(
178
178
+
acc: BitArray,
179
179
+
chunks: BitArray,
180
180
+
) -> Result(BitArray, errors.DecodeResponseError) {
181
181
+
use #(chunk, rest) <- result.try(unwrap_single_chunk(chunks))
182
182
+
case chunk {
183
183
+
<<>> -> Ok(acc)
184
184
+
_ -> unwrap_chunk_loop(<<acc:bits, chunk:bits>>, rest)
185
185
+
}
186
186
+
}
187
187
+
188
188
+
fn unwrap_single_chunk(
189
189
+
chunks: BitArray,
190
190
+
) -> Result(#(BitArray, BitArray), errors.DecodeResponseError) {
191
191
+
// assume we're after last chunk, take till CRLF
192
192
+
let #(chunk_size, rest) = ascii_util.take_till(chunks, crlf)
193
193
+
// chunk_ext might be exist, let's just ignore that
194
194
+
let #(chunk_size, _chunk_ext) = ascii_util.take_till(chunk_size, <<";">>)
195
195
+
196
196
+
// if both chunk_size and rest empty, it means that we're parsing incomplete message
197
197
+
198
198
+
use chunk_size <- result.try(
199
199
+
bit_array.to_string(chunk_size)
200
200
+
|> result.replace_error(case chunk_size, rest {
201
201
+
<<>>, <<>> -> errors.IncompleteMessage
202
202
+
_, _ -> errors.MalformedChunkedContent
203
203
+
}),
204
204
+
)
205
205
+
use chunk_size <- result.try(
206
206
+
int.base_parse(chunk_size, 16)
207
207
+
|> result.replace_error(case chunk_size, rest {
208
208
+
"", <<>> -> errors.IncompleteMessage
209
209
+
_, _ -> errors.MalformedChunkedContent
210
210
+
}),
211
211
+
)
212
212
+
213
213
+
let #(_, rest) = ascii_util.take_nth(rest, 2)
214
214
+
215
215
+
case chunk_size {
216
216
+
// the "actual" chunk_data of last-chunk is trailers, let's discard that
217
217
+
0 -> Ok(#(<<>>, rest))
218
218
+
_ -> {
219
219
+
let #(chunk_data, rest) = ascii_util.take_till(rest, crlf)
220
220
+
221
221
+
use <- given.that(
222
222
+
// we check if the chunk_data's size is the same as the chunk_size
223
223
+
bit_array.byte_size(chunk_data) == chunk_size,
224
224
+
else_return: fn() { Error(errors.MalformedChunkedContent) },
225
225
+
)
226
226
+
227
227
+
let #(_, rest) = ascii_util.take_nth(rest, 2)
228
228
+
Ok(#(chunk_data, rest))
229
229
+
}
230
230
+
}
231
231
+
}
+215
src/requwu/request.gleam
···
1
1
+
import gleam/bit_array
2
2
+
import gleam/function
3
3
+
import gleam/http
4
4
+
import gleam/http/request
5
5
+
import gleam/int
6
6
+
import gleam/json
7
7
+
import gleam/list
8
8
+
import gleam/option.{type Option, None, Some}
9
9
+
import gleam/result
10
10
+
import gleam/string
11
11
+
import gleam/string_tree
12
12
+
import gleam/uri
13
13
+
import requwu/errors
14
14
+
15
15
+
pub type RequestBuilder {
16
16
+
RequestBuilder(
17
17
+
timeout: Int,
18
18
+
method: http.Method,
19
19
+
uri: String,
20
20
+
query: Option(String),
21
21
+
version: HttpVersion,
22
22
+
headers: List(#(String, String)),
23
23
+
auth: Option(String),
24
24
+
user_agent: Option(String),
25
25
+
max_redirects: Int,
26
26
+
body: String,
27
27
+
)
28
28
+
}
29
29
+
30
30
+
pub type HttpVersion {
31
31
+
Http11
32
32
+
}
33
33
+
34
34
+
pub fn to(method: http.Method, to uri: String) -> RequestBuilder {
35
35
+
let #(uri, query) =
36
36
+
string.split_once(uri, on: "?") |> result.unwrap(#(uri, ""))
37
37
+
38
38
+
RequestBuilder(
39
39
+
timeout: 1000,
40
40
+
method:,
41
41
+
uri:,
42
42
+
query: string.to_option(query),
43
43
+
version: Http11,
44
44
+
headers: [],
45
45
+
auth: None,
46
46
+
user_agent: None,
47
47
+
max_redirects: -1,
48
48
+
body: "",
49
49
+
)
50
50
+
}
51
51
+
52
52
+
pub fn get(uri: String) -> RequestBuilder {
53
53
+
to(http.Get, uri)
54
54
+
}
55
55
+
56
56
+
pub fn head(uri: String) -> RequestBuilder {
57
57
+
to(http.Head, uri)
58
58
+
}
59
59
+
60
60
+
pub fn post(uri: String) -> RequestBuilder {
61
61
+
to(http.Post, uri)
62
62
+
}
63
63
+
64
64
+
pub fn put(uri: String) -> RequestBuilder {
65
65
+
to(http.Put, uri)
66
66
+
}
67
67
+
68
68
+
pub fn delete(uri: String) -> RequestBuilder {
69
69
+
to(http.Delete, uri)
70
70
+
}
71
71
+
72
72
+
pub fn connect(uri: String) -> RequestBuilder {
73
73
+
to(http.Connect, uri)
74
74
+
}
75
75
+
76
76
+
pub fn options(uri: String) -> RequestBuilder {
77
77
+
to(http.Options, uri)
78
78
+
}
79
79
+
80
80
+
pub fn trace(uri: String) -> RequestBuilder {
81
81
+
to(http.Trace, uri)
82
82
+
}
83
83
+
84
84
+
pub fn header(
85
85
+
builder: RequestBuilder,
86
86
+
key: String,
87
87
+
value: String,
88
88
+
) -> RequestBuilder {
89
89
+
RequestBuilder(..builder, headers: [#(key, value), ..builder.headers])
90
90
+
}
91
91
+
92
92
+
pub fn headers(
93
93
+
builder: RequestBuilder,
94
94
+
headers: List(#(String, String)),
95
95
+
) -> RequestBuilder {
96
96
+
RequestBuilder(..builder, headers: list.append(builder.headers, headers))
97
97
+
}
98
98
+
99
99
+
pub fn body(builder: RequestBuilder, body: String) -> RequestBuilder {
100
100
+
RequestBuilder(..builder, body:)
101
101
+
}
102
102
+
103
103
+
pub fn json(builder: RequestBuilder, json: json.Json) -> RequestBuilder {
104
104
+
RequestBuilder(
105
105
+
..builder,
106
106
+
body: json.to_string_tree(json) |> string_tree.to_string,
107
107
+
)
108
108
+
|> header("content-type", "application/json")
109
109
+
}
110
110
+
111
111
+
pub fn basic_auth(
112
112
+
builder: RequestBuilder,
113
113
+
user: String,
114
114
+
password: Option(String),
115
115
+
) -> RequestBuilder {
116
116
+
let basic_auth =
117
117
+
bit_array.from_string(user <> ":" <> password |> option.unwrap(""))
118
118
+
|> bit_array.base64_encode(True)
119
119
+
120
120
+
RequestBuilder(..builder, auth: Some("Basic " <> basic_auth))
121
121
+
}
122
122
+
123
123
+
pub fn bearer_auth(builder: RequestBuilder, token: String) -> RequestBuilder {
124
124
+
RequestBuilder(..builder, auth: Some("Bearer " <> token))
125
125
+
}
126
126
+
127
127
+
pub fn auth(builder: RequestBuilder, auth: String) -> RequestBuilder {
128
128
+
RequestBuilder(..builder, auth: Some(auth))
129
129
+
}
130
130
+
131
131
+
pub fn user_agent(builder: RequestBuilder, user_agent: String) -> RequestBuilder {
132
132
+
RequestBuilder(..builder, user_agent: Some(user_agent))
133
133
+
}
134
134
+
135
135
+
pub fn max_redirects(builder: RequestBuilder, redirects: Int) {
136
136
+
RequestBuilder(..builder, max_redirects: redirects)
137
137
+
}
138
138
+
139
139
+
pub fn timeout(
140
140
+
builder: RequestBuilder,
141
141
+
miliseconds timeout: Int,
142
142
+
) -> RequestBuilder {
143
143
+
RequestBuilder(..builder, timeout:)
144
144
+
}
145
145
+
146
146
+
pub fn query(builder: RequestBuilder, query: String) -> RequestBuilder {
147
147
+
RequestBuilder(..builder, query: Some(query))
148
148
+
}
149
149
+
150
150
+
pub fn from_request(req: request.Request(String)) -> RequestBuilder {
151
151
+
let uri = request.to_uri(req) |> uri.to_string
152
152
+
153
153
+
to(req.method, uri)
154
154
+
|> headers(req.headers)
155
155
+
|> case list.key_find(req.headers, "authorization") {
156
156
+
Error(Nil) -> function.identity
157
157
+
Ok(auth_str) -> auth(_, auth_str)
158
158
+
}
159
159
+
|> body(req.body)
160
160
+
}
161
161
+
162
162
+
@internal
163
163
+
pub type CompleteRequest {
164
164
+
CompleteRequest(
165
165
+
timeout: Int,
166
166
+
method: http.Method,
167
167
+
uri: uri.Uri,
168
168
+
version: HttpVersion,
169
169
+
headers: List(#(String, String)),
170
170
+
max_redirects: Int,
171
171
+
body: String,
172
172
+
)
173
173
+
}
174
174
+
175
175
+
@internal
176
176
+
pub fn complete(
177
177
+
builder: RequestBuilder,
178
178
+
) -> Result(CompleteRequest, errors.EncodeRequestError) {
179
179
+
let query = case builder.query {
180
180
+
None -> ""
181
181
+
Some(query) -> "?" <> query
182
182
+
}
183
183
+
let uri = builder.uri <> query
184
184
+
use uri <- result.try(
185
185
+
uri.parse(uri) |> result.replace_error(errors.FailedToParseUri),
186
186
+
)
187
187
+
188
188
+
use host <- result.try(option.to_result(uri.host, errors.FailedToParseUri))
189
189
+
190
190
+
let headers = {
191
191
+
case builder.auth {
192
192
+
None -> []
193
193
+
Some(auth) -> [#("authorization", auth)]
194
194
+
}
195
195
+
|> list.append(builder.headers)
196
196
+
|> list.prepend(#("host", host))
197
197
+
|> list.prepend(#(
198
198
+
"content-length",
199
199
+
bit_array.from_string(builder.body)
200
200
+
|> bit_array.byte_size
201
201
+
|> int.to_string,
202
202
+
))
203
203
+
|> list.prepend(#("te", "chunked"))
204
204
+
}
205
205
+
206
206
+
Ok(CompleteRequest(
207
207
+
timeout: builder.timeout,
208
208
+
method: builder.method,
209
209
+
uri:,
210
210
+
version: builder.version,
211
211
+
headers:,
212
212
+
max_redirects: builder.max_redirects,
213
213
+
body: builder.body,
214
214
+
))
215
215
+
}