Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 108 additions & 2 deletions src/gleam/fetch.gleam
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import gleam/dynamic.{type Dynamic}
import gleam/fetch/fetch_options.{type FetchOptions}
import gleam/fetch/form_data.{type FormData}
import gleam/http/request.{type Request}
import gleam/http/response.{type Response}
Expand Down Expand Up @@ -41,7 +42,27 @@ pub type FetchResponse
/// |> fetch.raw_send
/// ```
@external(javascript, "../gleam_fetch_ffi.mjs", "raw_send")
pub fn raw_send(a: FetchRequest) -> Promise(Result(FetchResponse, FetchError))
pub fn raw_send(
request: FetchRequest,
) -> Promise(Result(FetchResponse, FetchError))

/// Call directly `fetch` with a `Request` and `FetchOptions`,
/// then convert the result back to Gleam.
/// Let you get back a `FetchResponse` instead of the Gleam
/// `gleam/http/response.Response` data.
///
/// ```gleam
/// request.new()
/// |> request.set_host("example.com")
/// |> request.set_path("/example")
/// |> fetch.to_fetch_request
/// |> fetch.raw_send_with(fetch_options.new())
/// ```
@external(javascript, "../gleam_fetch_ffi.mjs", "raw_send")
pub fn raw_send_with(
request: FetchRequest,
options: FetchOptions,
) -> Promise(Result(FetchResponse, FetchError))

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

/// Call `fetch` with a Gleam `Request(String)` and `FetchOptions`,
/// then convert the result back to Gleam.
/// Use it to send strings or JSON stringified.
///
/// If you're looking for something more low-level, take a look at
/// [`raw_send_with`](#raw_send_with).
///
/// ```gleam
/// let my_data = json.object([#("field", "value")])
/// request.new()
/// |> request.set_host("example.com")
/// |> request.set_path("/example")
/// |> request.set_body(json.to_string(my_data))
/// |> request.set_header("content-type", "application/json")
/// |> fetch.send_with(fetch_options.new())
/// ```
pub fn send_with(
request: Request(String),
options: FetchOptions,
) -> Promise(Result(Response(FetchBody), FetchError)) {
request
|> to_fetch_request
|> raw_send_with(options)
|> promise.try_await(fn(resp) {
promise.resolve(Ok(from_fetch_response(resp)))
})
}

/// Call `fetch` with a Gleam `Request(FormData)`, and convert the result back
/// to Gleam. Request will be sent as a `multipart/form-data`, and should be
/// decoded as-is on servers.
Expand Down Expand Up @@ -97,6 +146,36 @@ pub fn send_form_data(
})
}

/// Call `fetch` with a Gleam `Request(FormData)` and `FetchOptions`,
/// then convert the result back to Gleam.
/// Request will be sent as a `multipart/form-data`, and should be
/// decoded as-is on servers.
///
/// If you're looking for something more low-level, take a look at
/// [`raw_send_with`](#raw_send_with).
///
/// ```gleam
/// request.new()
/// |> request.set_host("example.com")
/// |> request.set_path("/example")
/// |> request.set_body({
/// form_data.new()
/// |> form_data.append("key", "value")
/// })
/// |> fetch.send_form_data_with(fetch_options.new())
/// ```
pub fn send_form_data_with(
request: Request(FormData),
options: FetchOptions,
) -> Promise(Result(Response(FetchBody), FetchError)) {
request
|> form_data_to_fetch_request
|> raw_send_with(options)
|> promise.try_await(fn(resp) {
promise.resolve(Ok(from_fetch_response(resp)))
})
}

/// Call `fetch` with a Gleam `Request(FormData)`, and convert the result back
/// to Gleam. Binary will be sent as-is, and you probably want a proper
/// content-type added.
Expand All @@ -110,7 +189,7 @@ pub fn send_form_data(
/// |> request.set_path("/example")
/// |> request.set_body(<<"data">>)
/// |> request.set_header("content-type", "application/octet-stream")
/// |> fetch.send_form_data
/// |> fetch.send_bits
/// ```
pub fn send_bits(
request: Request(BitArray),
Expand All @@ -123,6 +202,33 @@ pub fn send_bits(
})
}

/// Call `fetch` with a Gleam `Request(FormData)` and `FetchOptions`,
/// then convert the result back to Gleam. Binary will be sent as-is,
/// and you probably want a proper content-type added.
///
/// If you're looking for something more low-level, take a look at
/// [`raw_send_with`](#raw_send_with).
///
/// ```gleam
/// request.new()
/// |> request.set_host("example.com")
/// |> request.set_path("/example")
/// |> request.set_body(<<"data">>)
/// |> request.set_header("content-type", "application/octet-stream")
/// |> fetch.send_bits_with(fetch_options.new())
/// ```
pub fn send_bits_with(
request: Request(BitArray),
options: FetchOptions,
) -> Promise(Result(Response(FetchBody), FetchError)) {
request
|> bitarray_request_to_fetch_request
|> raw_send_with(options)
|> promise.try_await(fn(resp) {
promise.resolve(Ok(from_fetch_response(resp)))
})
}

/// Convert a Gleam `Request(String)` to a JavaScript
/// [`Request`](https://developer.mozilla.org/docs/Web/API/Request), where
/// `body` is a string.
Expand Down
184 changes: 184 additions & 0 deletions src/gleam/fetch/fetch_options.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import gleam/dynamic.{type Dynamic}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dynamic is for data coming from the outside work, for data going to FFI one should use an accurate type.

Removing the dynamic use here will also fix the warnings that dynamic.from emits in current gleam_stdlib


/// Gleam equivalent of JavaScript [`RequestInit`](https://developer.mozilla.org/docs/Web/API/RequestInit).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap doc comments at 80 lines like regular Gleam code, and included documentation for each option rather solely linking to an external resource 🙏

pub type FetchOptions

/// Cache options, for details see [`cache`](https://developer.mozilla.org/docs/Web/API/RequestInit#cache).
pub type Cache {
Default
NoStore
Reload
NoCache
ForceCache
OnlyIfCached
}

/// Credentials options, for details see [`credentials`](https://developer.mozilla.org/docs/Web/API/RequestInit#credentials).
pub type Credentials {
CredentialsOmit
CredentialsSameOrigin
CredentialsInclude
}

/// Cors options, for details see [`mode`](https://developer.mozilla.org/docs/Web/API/RequestInit#mode).
pub type Cors {
SameOrigin
Cors
NoCors
Navigate
}

/// Priority options, for details see [`priority`](https://developer.mozilla.org/docs/Web/API/RequestInit#priority).
pub type Priority {
High
Low
Auto
}

/// Redirect options, for details see [`redirect`](https://developer.mozilla.org/docs/Web/API/RequestInit#redirect).
pub type Redirect {
Follow
Error
Manual
}

/// Creates new empty `FetchOptions` object.
///
/// Useful if more precise control over fetch is required, such as
/// using signals, cache options and so on.
///
/// ```gleam
/// let options = fetch_options.new()
/// |> fetch_options.set_cache(fetch_options.NoStore)
/// ```
@external(javascript, "../../gleam_fetch_ffi.mjs", "newFetchOptions")
pub fn new() -> FetchOptions

/// Sets the [`cache`](https://developer.mozilla.org/docs/Web/API/RequestInit#cache) option of `FetchOptions`.
///
/// ```gleam
/// let options = fetch_options.new()
/// |> fetch_options.set_cache(fetch_options.NoStore)
/// ```
pub fn set_cache(fetch_options: FetchOptions, cache: Cache) -> FetchOptions {
set_key(
fetch_options,
"cache",
dynamic.from(case cache {
Default -> "default"
NoStore -> "no-store"
Reload -> "reload"
NoCache -> "no-cache"
ForceCache -> "force-cache"
OnlyIfCached -> "only-if-cached"
}),
)
}

/// Sets the [`credentials`](https://developer.mozilla.org/docs/Web/API/RequestInit#credentials) option of `FetchOptions`.
///
/// ```gleam
/// let options = fetch_options.new()
/// |> fetch_options.set_credentials(fetch_options.CredentialsOmit)
/// ```
pub fn set_credentials(
fetch_options: FetchOptions,
credentials: Credentials,
) -> FetchOptions {
set_key(
fetch_options,
"credentials",
dynamic.from(case credentials {
CredentialsOmit -> "omit"
CredentialsSameOrigin -> "same-origin"
CredentialsInclude -> "include"
}),
)
}

/// Sets the [`keepalive`](https://developer.mozilla.org/docs/Web/API/RequestInit#keepalive) option of `FetchOptions`.
///
/// ```gleam
/// let options = fetch_options.new()
/// |> fetch_options.set_keepalive(True)
/// ```
pub fn set_keepalive(
fetch_options: FetchOptions,
keepalive: Bool,
) -> FetchOptions {
set_key(fetch_options, "keepalive", dynamic.from(keepalive))
}

/// Sets the [`cors`](https://developer.mozilla.org/docs/Web/API/RequestInit#mode) option of `FetchOptions`.
///
/// ```gleam
/// let options = fetch_options.new()
/// |> fetch_options.set_cors(fetch_options.SameOrigin)
/// ```
pub fn set_cors(fetch_options: FetchOptions, cors: Cors) -> FetchOptions {
set_key(
fetch_options,
"mode",
dynamic.from(case cors {
SameOrigin -> "same-origin"
Cors -> "cors"
NoCors -> "no-cors"
Navigate -> "navigate"
}),
)
}

/// Sets the [`priority`](https://developer.mozilla.org/docs/Web/API/RequestInit#priority) option of `FetchOptions`.
///
/// ```gleam
/// let options = fetch_options.new()
/// |> fetch_options.set_cors(fetch_options.High)
/// ```
pub fn set_priority(
fetch_options: FetchOptions,
priority: Priority,
) -> FetchOptions {
set_key(
fetch_options,
"priority",
dynamic.from(case priority {
High -> "high"
Low -> "low"
Auto -> "auto"
}),
)
}

/// Sets the [`redirect`](https://developer.mozilla.org/docs/Web/API/RequestInit#redirect) option of `FetchOptions`.
///
/// ```gleam
/// let options = fetch_options.new()
/// |> fetch_options.set_redirect(fetch_options.Follow)
/// ```
pub fn set_redirect(
fetch_options: FetchOptions,
redirect: Redirect,
) -> FetchOptions {
set_key(
fetch_options,
"redirect",
dynamic.from(case redirect {
Follow -> "follow"
Error -> "error"
Manual -> "manual"
}),
)
}

/// Generic function that sets specified option in the `FetchOptions` object.
///
/// In JavaScript, this object is simply represented as `{}` with no type-checking,
/// so when implementing new features, you should consult
/// [documentation](https://developer.mozilla.org/docs/Web/API/RequestInit)
/// for valid and sensible keys and values.
@external(javascript, "../../gleam_fetch_ffi.mjs", "setKeyFetchOptions")
fn set_key(
fetch_options: FetchOptions,
key: String,
value: Dynamic,
) -> FetchOptions
15 changes: 13 additions & 2 deletions src/gleam_fetch_ffi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
UnableToReadBody,
} from "../gleam_fetch/gleam/fetch.mjs";

export async function raw_send(request) {
export async function raw_send(request, options) {
try {
return new Ok(await fetch(request));
return new Ok(await fetch(request, options));
} catch (error) {
return new Error(new NetworkError(error.toString()));
}
Expand Down Expand Up @@ -165,3 +165,14 @@ export function keysFormData(formData) {
}
return toList([...result])
}

// FetchOptions functions.

export function newFetchOptions() {
return {};
}

export function setKeyFetchOptions(fetchOptions, key, value) {
fetchOptions[key] = value;
return fetchOptions;
}
23 changes: 23 additions & 0 deletions test/gleam_fetch_test.gleam
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import gleam/fetch.{type FetchError}
import gleam/fetch/fetch_options
import gleam/fetch/form_data
import gleam/http.{Get, Head, Options}
import gleam/http/request
Expand Down Expand Up @@ -194,3 +195,25 @@ fn setup_form_data() {
|> form_data.append("second-key", "second-value")
|> form_data.append_bits("second-key", <<"second-value-bits":utf8>>)
}

pub fn complex_fetch_options_test() {
let req =
request.new()
|> request.set_method(Get)
|> request.set_host("test-api.service.hmrc.gov.uk")
|> request.set_path("/hello/world")
|> request.prepend_header("accept", "application/vnd.hmrc.1.0+json")

let options =
fetch_options.new()
|> fetch_options.set_cache(fetch_options.NoStore)
|> fetch_options.set_cors(fetch_options.Cors)
|> fetch_options.set_credentials(fetch_options.CredentialsOmit)
|> fetch_options.set_keepalive(True)
|> fetch_options.set_priority(fetch_options.High)
|> fetch_options.set_redirect(fetch_options.Follow)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this test verify? Doesn't seem like there's assertions for any of these?


use result <- promise.await(fetch.send_with(req, options))
let assert Ok(_) = result
promise.resolve(Nil)
}