From cd8c885a3e2b66bb2e8083adcd1a141fad0147e9 Mon Sep 17 00:00:00 2001 From: duzda <25201406+duzda@users.noreply.github.com> Date: Sun, 31 Aug 2025 17:03:07 +0200 Subject: [PATCH] Implement RequestInit via FetchOptions Does not fully implement RequestInit, only what seemed useful. Closes: #4 --- src/gleam/fetch.gleam | 110 ++++++++++++++++- src/gleam/fetch/fetch_options.gleam | 184 ++++++++++++++++++++++++++++ src/gleam_fetch_ffi.mjs | 15 ++- test/gleam_fetch_test.gleam | 23 ++++ 4 files changed, 328 insertions(+), 4 deletions(-) create mode 100644 src/gleam/fetch/fetch_options.gleam diff --git a/src/gleam/fetch.gleam b/src/gleam/fetch.gleam index 2bf80c0..7223521 100644 --- a/src/gleam/fetch.gleam +++ b/src/gleam/fetch.gleam @@ -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} @@ -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. @@ -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. @@ -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. @@ -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), @@ -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. diff --git a/src/gleam/fetch/fetch_options.gleam b/src/gleam/fetch/fetch_options.gleam new file mode 100644 index 0000000..a4a9093 --- /dev/null +++ b/src/gleam/fetch/fetch_options.gleam @@ -0,0 +1,184 @@ +import gleam/dynamic.{type Dynamic} + +/// Gleam equivalent of JavaScript [`RequestInit`](https://developer.mozilla.org/docs/Web/API/RequestInit). +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 diff --git a/src/gleam_fetch_ffi.mjs b/src/gleam_fetch_ffi.mjs index 0bc8944..941cb1e 100644 --- a/src/gleam_fetch_ffi.mjs +++ b/src/gleam_fetch_ffi.mjs @@ -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())); } @@ -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; +} diff --git a/test/gleam_fetch_test.gleam b/test/gleam_fetch_test.gleam index 68a6d30..8b7f06e 100644 --- a/test/gleam_fetch_test.gleam +++ b/test/gleam_fetch_test.gleam @@ -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 @@ -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) + + use result <- promise.await(fetch.send_with(req, options)) + let assert Ok(_) = result + promise.resolve(Nil) +}