Skip to content

Commit cd8c885

Browse files
committed
Implement RequestInit via FetchOptions
Does not fully implement RequestInit, only what seemed useful. Closes: #4
1 parent a18d7ff commit cd8c885

File tree

4 files changed

+328
-4
lines changed

4 files changed

+328
-4
lines changed

src/gleam/fetch.gleam

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import gleam/dynamic.{type Dynamic}
2+
import gleam/fetch/fetch_options.{type FetchOptions}
23
import gleam/fetch/form_data.{type FormData}
34
import gleam/http/request.{type Request}
45
import gleam/http/response.{type Response}
@@ -41,7 +42,27 @@ pub type FetchResponse
4142
/// |> fetch.raw_send
4243
/// ```
4344
@external(javascript, "../gleam_fetch_ffi.mjs", "raw_send")
44-
pub fn raw_send(a: FetchRequest) -> Promise(Result(FetchResponse, FetchError))
45+
pub fn raw_send(
46+
request: FetchRequest,
47+
) -> Promise(Result(FetchResponse, FetchError))
48+
49+
/// Call directly `fetch` with a `Request` and `FetchOptions`,
50+
/// then convert the result back to Gleam.
51+
/// Let you get back a `FetchResponse` instead of the Gleam
52+
/// `gleam/http/response.Response` data.
53+
///
54+
/// ```gleam
55+
/// request.new()
56+
/// |> request.set_host("example.com")
57+
/// |> request.set_path("/example")
58+
/// |> fetch.to_fetch_request
59+
/// |> fetch.raw_send_with(fetch_options.new())
60+
/// ```
61+
@external(javascript, "../gleam_fetch_ffi.mjs", "raw_send")
62+
pub fn raw_send_with(
63+
request: FetchRequest,
64+
options: FetchOptions,
65+
) -> Promise(Result(FetchResponse, FetchError))
4566

4667
/// Call `fetch` with a Gleam `Request(String)`, and convert the result back
4768
/// to Gleam. Use it to send strings or JSON stringified.
@@ -69,6 +90,34 @@ pub fn send(
6990
})
7091
}
7192

93+
/// Call `fetch` with a Gleam `Request(String)` and `FetchOptions`,
94+
/// then convert the result back to Gleam.
95+
/// Use it to send strings or JSON stringified.
96+
///
97+
/// If you're looking for something more low-level, take a look at
98+
/// [`raw_send_with`](#raw_send_with).
99+
///
100+
/// ```gleam
101+
/// let my_data = json.object([#("field", "value")])
102+
/// request.new()
103+
/// |> request.set_host("example.com")
104+
/// |> request.set_path("/example")
105+
/// |> request.set_body(json.to_string(my_data))
106+
/// |> request.set_header("content-type", "application/json")
107+
/// |> fetch.send_with(fetch_options.new())
108+
/// ```
109+
pub fn send_with(
110+
request: Request(String),
111+
options: FetchOptions,
112+
) -> Promise(Result(Response(FetchBody), FetchError)) {
113+
request
114+
|> to_fetch_request
115+
|> raw_send_with(options)
116+
|> promise.try_await(fn(resp) {
117+
promise.resolve(Ok(from_fetch_response(resp)))
118+
})
119+
}
120+
72121
/// Call `fetch` with a Gleam `Request(FormData)`, and convert the result back
73122
/// to Gleam. Request will be sent as a `multipart/form-data`, and should be
74123
/// decoded as-is on servers.
@@ -97,6 +146,36 @@ pub fn send_form_data(
97146
})
98147
}
99148

149+
/// Call `fetch` with a Gleam `Request(FormData)` and `FetchOptions`,
150+
/// then convert the result back to Gleam.
151+
/// Request will be sent as a `multipart/form-data`, and should be
152+
/// decoded as-is on servers.
153+
///
154+
/// If you're looking for something more low-level, take a look at
155+
/// [`raw_send_with`](#raw_send_with).
156+
///
157+
/// ```gleam
158+
/// request.new()
159+
/// |> request.set_host("example.com")
160+
/// |> request.set_path("/example")
161+
/// |> request.set_body({
162+
/// form_data.new()
163+
/// |> form_data.append("key", "value")
164+
/// })
165+
/// |> fetch.send_form_data_with(fetch_options.new())
166+
/// ```
167+
pub fn send_form_data_with(
168+
request: Request(FormData),
169+
options: FetchOptions,
170+
) -> Promise(Result(Response(FetchBody), FetchError)) {
171+
request
172+
|> form_data_to_fetch_request
173+
|> raw_send_with(options)
174+
|> promise.try_await(fn(resp) {
175+
promise.resolve(Ok(from_fetch_response(resp)))
176+
})
177+
}
178+
100179
/// Call `fetch` with a Gleam `Request(FormData)`, and convert the result back
101180
/// to Gleam. Binary will be sent as-is, and you probably want a proper
102181
/// content-type added.
@@ -110,7 +189,7 @@ pub fn send_form_data(
110189
/// |> request.set_path("/example")
111190
/// |> request.set_body(<<"data">>)
112191
/// |> request.set_header("content-type", "application/octet-stream")
113-
/// |> fetch.send_form_data
192+
/// |> fetch.send_bits
114193
/// ```
115194
pub fn send_bits(
116195
request: Request(BitArray),
@@ -123,6 +202,33 @@ pub fn send_bits(
123202
})
124203
}
125204

205+
/// Call `fetch` with a Gleam `Request(FormData)` and `FetchOptions`,
206+
/// then convert the result back to Gleam. Binary will be sent as-is,
207+
/// and you probably want a proper content-type added.
208+
///
209+
/// If you're looking for something more low-level, take a look at
210+
/// [`raw_send_with`](#raw_send_with).
211+
///
212+
/// ```gleam
213+
/// request.new()
214+
/// |> request.set_host("example.com")
215+
/// |> request.set_path("/example")
216+
/// |> request.set_body(<<"data">>)
217+
/// |> request.set_header("content-type", "application/octet-stream")
218+
/// |> fetch.send_bits_with(fetch_options.new())
219+
/// ```
220+
pub fn send_bits_with(
221+
request: Request(BitArray),
222+
options: FetchOptions,
223+
) -> Promise(Result(Response(FetchBody), FetchError)) {
224+
request
225+
|> bitarray_request_to_fetch_request
226+
|> raw_send_with(options)
227+
|> promise.try_await(fn(resp) {
228+
promise.resolve(Ok(from_fetch_response(resp)))
229+
})
230+
}
231+
126232
/// Convert a Gleam `Request(String)` to a JavaScript
127233
/// [`Request`](https://developer.mozilla.org/docs/Web/API/Request), where
128234
/// `body` is a string.
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import gleam/dynamic.{type Dynamic}
2+
3+
/// Gleam equivalent of JavaScript [`RequestInit`](https://developer.mozilla.org/docs/Web/API/RequestInit).
4+
pub type FetchOptions
5+
6+
/// Cache options, for details see [`cache`](https://developer.mozilla.org/docs/Web/API/RequestInit#cache).
7+
pub type Cache {
8+
Default
9+
NoStore
10+
Reload
11+
NoCache
12+
ForceCache
13+
OnlyIfCached
14+
}
15+
16+
/// Credentials options, for details see [`credentials`](https://developer.mozilla.org/docs/Web/API/RequestInit#credentials).
17+
pub type Credentials {
18+
CredentialsOmit
19+
CredentialsSameOrigin
20+
CredentialsInclude
21+
}
22+
23+
/// Cors options, for details see [`mode`](https://developer.mozilla.org/docs/Web/API/RequestInit#mode).
24+
pub type Cors {
25+
SameOrigin
26+
Cors
27+
NoCors
28+
Navigate
29+
}
30+
31+
/// Priority options, for details see [`priority`](https://developer.mozilla.org/docs/Web/API/RequestInit#priority).
32+
pub type Priority {
33+
High
34+
Low
35+
Auto
36+
}
37+
38+
/// Redirect options, for details see [`redirect`](https://developer.mozilla.org/docs/Web/API/RequestInit#redirect).
39+
pub type Redirect {
40+
Follow
41+
Error
42+
Manual
43+
}
44+
45+
/// Creates new empty `FetchOptions` object.
46+
///
47+
/// Useful if more precise control over fetch is required, such as
48+
/// using signals, cache options and so on.
49+
///
50+
/// ```gleam
51+
/// let options = fetch_options.new()
52+
/// |> fetch_options.set_cache(fetch_options.NoStore)
53+
/// ```
54+
@external(javascript, "../../gleam_fetch_ffi.mjs", "newFetchOptions")
55+
pub fn new() -> FetchOptions
56+
57+
/// Sets the [`cache`](https://developer.mozilla.org/docs/Web/API/RequestInit#cache) option of `FetchOptions`.
58+
///
59+
/// ```gleam
60+
/// let options = fetch_options.new()
61+
/// |> fetch_options.set_cache(fetch_options.NoStore)
62+
/// ```
63+
pub fn set_cache(fetch_options: FetchOptions, cache: Cache) -> FetchOptions {
64+
set_key(
65+
fetch_options,
66+
"cache",
67+
dynamic.from(case cache {
68+
Default -> "default"
69+
NoStore -> "no-store"
70+
Reload -> "reload"
71+
NoCache -> "no-cache"
72+
ForceCache -> "force-cache"
73+
OnlyIfCached -> "only-if-cached"
74+
}),
75+
)
76+
}
77+
78+
/// Sets the [`credentials`](https://developer.mozilla.org/docs/Web/API/RequestInit#credentials) option of `FetchOptions`.
79+
///
80+
/// ```gleam
81+
/// let options = fetch_options.new()
82+
/// |> fetch_options.set_credentials(fetch_options.CredentialsOmit)
83+
/// ```
84+
pub fn set_credentials(
85+
fetch_options: FetchOptions,
86+
credentials: Credentials,
87+
) -> FetchOptions {
88+
set_key(
89+
fetch_options,
90+
"credentials",
91+
dynamic.from(case credentials {
92+
CredentialsOmit -> "omit"
93+
CredentialsSameOrigin -> "same-origin"
94+
CredentialsInclude -> "include"
95+
}),
96+
)
97+
}
98+
99+
/// Sets the [`keepalive`](https://developer.mozilla.org/docs/Web/API/RequestInit#keepalive) option of `FetchOptions`.
100+
///
101+
/// ```gleam
102+
/// let options = fetch_options.new()
103+
/// |> fetch_options.set_keepalive(True)
104+
/// ```
105+
pub fn set_keepalive(
106+
fetch_options: FetchOptions,
107+
keepalive: Bool,
108+
) -> FetchOptions {
109+
set_key(fetch_options, "keepalive", dynamic.from(keepalive))
110+
}
111+
112+
/// Sets the [`cors`](https://developer.mozilla.org/docs/Web/API/RequestInit#mode) option of `FetchOptions`.
113+
///
114+
/// ```gleam
115+
/// let options = fetch_options.new()
116+
/// |> fetch_options.set_cors(fetch_options.SameOrigin)
117+
/// ```
118+
pub fn set_cors(fetch_options: FetchOptions, cors: Cors) -> FetchOptions {
119+
set_key(
120+
fetch_options,
121+
"mode",
122+
dynamic.from(case cors {
123+
SameOrigin -> "same-origin"
124+
Cors -> "cors"
125+
NoCors -> "no-cors"
126+
Navigate -> "navigate"
127+
}),
128+
)
129+
}
130+
131+
/// Sets the [`priority`](https://developer.mozilla.org/docs/Web/API/RequestInit#priority) option of `FetchOptions`.
132+
///
133+
/// ```gleam
134+
/// let options = fetch_options.new()
135+
/// |> fetch_options.set_cors(fetch_options.High)
136+
/// ```
137+
pub fn set_priority(
138+
fetch_options: FetchOptions,
139+
priority: Priority,
140+
) -> FetchOptions {
141+
set_key(
142+
fetch_options,
143+
"priority",
144+
dynamic.from(case priority {
145+
High -> "high"
146+
Low -> "low"
147+
Auto -> "auto"
148+
}),
149+
)
150+
}
151+
152+
/// Sets the [`redirect`](https://developer.mozilla.org/docs/Web/API/RequestInit#redirect) option of `FetchOptions`.
153+
///
154+
/// ```gleam
155+
/// let options = fetch_options.new()
156+
/// |> fetch_options.set_redirect(fetch_options.Follow)
157+
/// ```
158+
pub fn set_redirect(
159+
fetch_options: FetchOptions,
160+
redirect: Redirect,
161+
) -> FetchOptions {
162+
set_key(
163+
fetch_options,
164+
"redirect",
165+
dynamic.from(case redirect {
166+
Follow -> "follow"
167+
Error -> "error"
168+
Manual -> "manual"
169+
}),
170+
)
171+
}
172+
173+
/// Generic function that sets specified option in the `FetchOptions` object.
174+
///
175+
/// In JavaScript, this object is simply represented as `{}` with no type-checking,
176+
/// so when implementing new features, you should consult
177+
/// [documentation](https://developer.mozilla.org/docs/Web/API/RequestInit)
178+
/// for valid and sensible keys and values.
179+
@external(javascript, "../../gleam_fetch_ffi.mjs", "setKeyFetchOptions")
180+
fn set_key(
181+
fetch_options: FetchOptions,
182+
key: String,
183+
value: Dynamic,
184+
) -> FetchOptions

src/gleam_fetch_ffi.mjs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import {
99
UnableToReadBody,
1010
} from "../gleam_fetch/gleam/fetch.mjs";
1111

12-
export async function raw_send(request) {
12+
export async function raw_send(request, options) {
1313
try {
14-
return new Ok(await fetch(request));
14+
return new Ok(await fetch(request, options));
1515
} catch (error) {
1616
return new Error(new NetworkError(error.toString()));
1717
}
@@ -165,3 +165,14 @@ export function keysFormData(formData) {
165165
}
166166
return toList([...result])
167167
}
168+
169+
// FetchOptions functions.
170+
171+
export function newFetchOptions() {
172+
return {};
173+
}
174+
175+
export function setKeyFetchOptions(fetchOptions, key, value) {
176+
fetchOptions[key] = value;
177+
return fetchOptions;
178+
}

test/gleam_fetch_test.gleam

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import gleam/fetch.{type FetchError}
2+
import gleam/fetch/fetch_options
23
import gleam/fetch/form_data
34
import gleam/http.{Get, Head, Options}
45
import gleam/http/request
@@ -194,3 +195,25 @@ fn setup_form_data() {
194195
|> form_data.append("second-key", "second-value")
195196
|> form_data.append_bits("second-key", <<"second-value-bits":utf8>>)
196197
}
198+
199+
pub fn complex_fetch_options_test() {
200+
let req =
201+
request.new()
202+
|> request.set_method(Get)
203+
|> request.set_host("test-api.service.hmrc.gov.uk")
204+
|> request.set_path("/hello/world")
205+
|> request.prepend_header("accept", "application/vnd.hmrc.1.0+json")
206+
207+
let options =
208+
fetch_options.new()
209+
|> fetch_options.set_cache(fetch_options.NoStore)
210+
|> fetch_options.set_cors(fetch_options.Cors)
211+
|> fetch_options.set_credentials(fetch_options.CredentialsOmit)
212+
|> fetch_options.set_keepalive(True)
213+
|> fetch_options.set_priority(fetch_options.High)
214+
|> fetch_options.set_redirect(fetch_options.Follow)
215+
216+
use result <- promise.await(fetch.send_with(req, options))
217+
let assert Ok(_) = result
218+
promise.resolve(Nil)
219+
}

0 commit comments

Comments
 (0)