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 1/2] 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) +} From dc8783df695eb80270060ca0ce7847a6a8c48fde Mon Sep 17 00:00:00 2001 From: duzda <25201406+duzda@users.noreply.github.com> Date: Sun, 14 Sep 2025 17:43:05 +0200 Subject: [PATCH 2/2] Move fetch_options, expand tests, fix inline notes --- src/gleam/fetch.gleam | 298 ++++++++++++++++++++++++++-- src/gleam/fetch/fetch_options.gleam | 184 ----------------- src/gleam_fetch_ffi.mjs | 11 - test/gleam_fetch_test.gleam | 277 +++++++++++++++++++++++++- 4 files changed, 547 insertions(+), 223 deletions(-) delete mode 100644 src/gleam/fetch/fetch_options.gleam diff --git a/src/gleam/fetch.gleam b/src/gleam/fetch.gleam index 7223521..956a49c 100644 --- a/src/gleam/fetch.gleam +++ b/src/gleam/fetch.gleam @@ -1,5 +1,4 @@ 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} @@ -46,6 +45,13 @@ pub fn raw_send( request: FetchRequest, ) -> Promise(Result(FetchResponse, FetchError)) +/// Bridge for Request with FetchOptions between Gleam and JavaScript. +@external(javascript, "../gleam_fetch_ffi.mjs", "raw_send") +fn raw_send_converted_options( + request: FetchRequest, + options: FetchOptionsNative, +) -> 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 @@ -56,13 +62,24 @@ pub fn raw_send( /// |> request.set_host("example.com") /// |> request.set_path("/example") /// |> fetch.to_fetch_request -/// |> fetch.raw_send_with(fetch_options.new()) +/// |> fetch.raw_send_options(fetch_options.new()) /// ``` -@external(javascript, "../gleam_fetch_ffi.mjs", "raw_send") -pub fn raw_send_with( +pub fn raw_send_options( request: FetchRequest, options: FetchOptions, -) -> Promise(Result(FetchResponse, FetchError)) +) -> Promise(Result(FetchResponse, FetchError)) { + raw_send_converted_options( + request, + FetchOptionsNative( + cache: cache_to_string(options.cache), + credentials: credentials_to_string(options.credentials), + keepalive: options.keepalive, + mode: cors_to_string(options.mode), + priority: priority_to_string(options.priority), + redirect: redirect_to_string(options.redirect), + ), + ) +} /// Call `fetch` with a Gleam `Request(String)`, and convert the result back /// to Gleam. Use it to send strings or JSON stringified. @@ -95,7 +112,7 @@ pub fn send( /// 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). +/// [`raw_send_options`](#raw_send_options). /// /// ```gleam /// let my_data = json.object([#("field", "value")]) @@ -104,15 +121,15 @@ pub fn send( /// |> 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()) +/// |> fetch.send_options(fetch_options.new()) /// ``` -pub fn send_with( +pub fn send_options( request: Request(String), options: FetchOptions, ) -> Promise(Result(Response(FetchBody), FetchError)) { request |> to_fetch_request - |> raw_send_with(options) + |> raw_send_options(options) |> promise.try_await(fn(resp) { promise.resolve(Ok(from_fetch_response(resp))) }) @@ -152,7 +169,7 @@ pub fn send_form_data( /// decoded as-is on servers. /// /// If you're looking for something more low-level, take a look at -/// [`raw_send_with`](#raw_send_with). +/// [`raw_send_options`](#raw_send_options). /// /// ```gleam /// request.new() @@ -162,15 +179,15 @@ pub fn send_form_data( /// form_data.new() /// |> form_data.append("key", "value") /// }) -/// |> fetch.send_form_data_with(fetch_options.new()) +/// |> fetch.send_form_data_options(fetch_options.new()) /// ``` -pub fn send_form_data_with( +pub fn send_form_data_options( request: Request(FormData), options: FetchOptions, ) -> Promise(Result(Response(FetchBody), FetchError)) { request |> form_data_to_fetch_request - |> raw_send_with(options) + |> raw_send_options(options) |> promise.try_await(fn(resp) { promise.resolve(Ok(from_fetch_response(resp))) }) @@ -207,7 +224,7 @@ pub fn send_bits( /// 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). +/// [`raw_send_options`](#raw_send_options). /// /// ```gleam /// request.new() @@ -215,15 +232,15 @@ pub fn send_bits( /// |> request.set_path("/example") /// |> request.set_body(<<"data">>) /// |> request.set_header("content-type", "application/octet-stream") -/// |> fetch.send_bits_with(fetch_options.new()) +/// |> fetch.send_bits_options(fetch_options.new()) /// ``` -pub fn send_bits_with( +pub fn send_bits_options( request: Request(BitArray), options: FetchOptions, ) -> Promise(Result(Response(FetchBody), FetchError)) { request |> bitarray_request_to_fetch_request - |> raw_send_with(options) + |> raw_send_options(options) |> promise.try_await(fn(resp) { promise.resolve(Ok(from_fetch_response(resp))) }) @@ -363,3 +380,250 @@ pub fn read_text_body( pub fn read_json_body( a: Response(FetchBody), ) -> Promise(Result(Response(Dynamic), FetchError)) + +/// Gleam equivalent of JavaScript +/// [`RequestInit`](https://developer.mozilla.org/docs/Web/API/RequestInit). +/// +/// The Node target supports only the `redirect` and `priority` options. +pub opaque type FetchOptions { + Builder( + cache: Cache, + credentials: Credentials, + keepalive: Bool, + mode: Cors, + priority: Priority, + redirect: Redirect, + ) +} + +// Converted internal FetchOptions to be send via JavaScript. +type FetchOptionsNative { + FetchOptionsNative( + cache: String, + credentials: String, + keepalive: Bool, + mode: String, + priority: String, + redirect: String, + ) +} + +/// Cache options, for details see +/// [`cache`](https://developer.mozilla.org/docs/Web/API/RequestInit#cache). +/// +/// Change how responses are stored and retrieved from cache. +pub type Cache { + /// Default cache behaviour. + /// + /// Fresh record will be returned from the cache. + /// If the record in cache is stale and server responds with not + /// changed, then the value from cache is used. Otherwise makes normal + /// request and updates the cache. + Default + /// Response is not fetched from the cache and not stored in the cache. + NoStore + /// Response is not fetched from the cache but gets stored. + Reload + /// If the record in cache is fresh or stale and server responds with not + /// changed, then the value from cache is used. Otherwise makes normal + /// request and updates the cache. + NoCache + /// If record is in cache, it is always used. Otherwise makes normal + /// request. + ForceCache +} + +fn cache_to_string(cache: Cache) -> String { + case cache { + Default -> "default" + NoStore -> "no-store" + Reload -> "reload" + NoCache -> "no-cache" + ForceCache -> "force-cache" + } +} + +/// Credentials options, for details see +/// [`credentials`](https://developer.mozilla.org/docs/Web/API/RequestInit#credentials). +/// +/// Control whether browser sends credentials with the request and whether +/// Set-Cookie response headers are respected. +pub type Credentials { + /// Never send credentials or include credentials in the response. + CredentialsOmit + /// Only send and include credentials for same-origin requests. + CredentialsSameOrigin + /// Always include credentials. + CredentialsInclude +} + +fn credentials_to_string(credentials: Credentials) -> String { + case credentials { + CredentialsOmit -> "omit" + CredentialsSameOrigin -> "same-origin" + CredentialsInclude -> "include" + } +} + +/// CORS options, for details see +/// [`mode`](https://developer.mozilla.org/docs/Web/API/RequestInit#mode). +/// +/// Set cross-origin behaviour of a request. +pub type Cors { + /// Disallows cross-origin requests. + SameOrigin + /// Defaults to + /// [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) + /// mechanism. + Cors + /// Disables CORS for cross-origin requests. + NoCors + /// Used only by HTML navigation. + Navigate +} + +fn cors_to_string(cors: Cors) -> String { + case cors { + SameOrigin -> "same-origin" + Cors -> "cors" + NoCors -> "no-cors" + Navigate -> "navigate" + } +} + +/// Priority options, for details see +/// [`priority`](https://developer.mozilla.org/docs/Web/API/RequestInit#priority). +/// +/// Increase priority of a request relative to other requests. +pub type Priority { + /// Higher priority. + High + /// Lower priority. + Low + /// No preference of priority. + Auto +} + +fn priority_to_string(priority: Priority) -> String { + case priority { + High -> "high" + Low -> "low" + Auto -> "auto" + } +} + +/// Redirect options, for details see +/// [`redirect`](https://developer.mozilla.org/docs/Web/API/RequestInit#redirect). +/// +/// Change the redirect behaviour of a request. +pub type Redirect { + /// Automatically redirects request. + Follow + /// Errors out on redirect. + Error + /// Expects user to handle redirects manually. + Manual +} + +fn redirect_to_string(redirect: Redirect) -> String { + case redirect { + Follow -> "follow" + Error -> "error" + Manual -> "manual" + } +} + +/// Creates new `FetchOptions` object with default values. +/// +/// 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.cache(fetch_options.NoStore) +/// ``` +pub fn fetch_options() -> FetchOptions { + Builder( + cache: Default, + credentials: CredentialsSameOrigin, + keepalive: False, + mode: Cors, + priority: Auto, + redirect: Follow, + ) +} + +/// Set the +/// [`cache`](https://developer.mozilla.org/docs/Web/API/RequestInit#cache) +/// option of `FetchOptions`. +/// +/// ```gleam +/// let options = fetch_options.new() +/// |> fetch_options.cache(fetch_options.NoStore) +/// ``` +pub fn cache(fetch_options: FetchOptions, which: Cache) -> FetchOptions { + Builder(..fetch_options, cache: which) +} + +/// Set the +/// [`credentials`](https://developer.mozilla.org/docs/Web/API/RequestInit#credentials) +/// option of `FetchOptions`. +/// +/// ```gleam +/// let options = fetch_options.new() +/// |> fetch_options.credentials(fetch_options.CredentialsOmit) +/// ``` +pub fn credentials( + fetch_options: FetchOptions, + which: Credentials, +) -> FetchOptions { + Builder(..fetch_options, credentials: which) +} + +/// Set the +/// [`keepalive`](https://developer.mozilla.org/docs/Web/API/RequestInit#keepalive) +/// option of `FetchOptions`. +/// +/// ```gleam +/// let options = fetch_options.new() +/// |> fetch_options.keepalive(True) +/// ``` +pub fn keepalive(fetch_options: FetchOptions, keepalive: Bool) -> FetchOptions { + Builder(..fetch_options, keepalive: keepalive) +} + +/// Set the +/// [`cors`](https://developer.mozilla.org/docs/Web/API/RequestInit#mode) +/// option of `FetchOptions`. +/// +/// ```gleam +/// let options = fetch_options.new() +/// |> fetch_options.cors(fetch_options.SameOrigin) +/// ``` +pub fn cors(fetch_options: FetchOptions, which: Cors) -> FetchOptions { + Builder(..fetch_options, mode: which) +} + +/// Set the +/// [`priority`](https://developer.mozilla.org/docs/Web/API/RequestInit#priority) +/// option of `FetchOptions`. +/// +/// ```gleam +/// let options = fetch_options.new() +/// |> fetch_options.cors(fetch_options.High) +/// ``` +pub fn priority(fetch_options: FetchOptions, which: Priority) -> FetchOptions { + Builder(..fetch_options, priority: which) +} + +/// Set the +/// [`redirect`](https://developer.mozilla.org/docs/Web/API/RequestInit#redirect) +/// option of `FetchOptions`. +/// +/// ```gleam +/// let options = fetch_options.new() +/// |> fetch_options.redirect(fetch_options.Follow) +/// ``` +pub fn redirect(fetch_options: FetchOptions, which: Redirect) -> FetchOptions { + Builder(..fetch_options, redirect: which) +} diff --git a/src/gleam/fetch/fetch_options.gleam b/src/gleam/fetch/fetch_options.gleam deleted file mode 100644 index a4a9093..0000000 --- a/src/gleam/fetch/fetch_options.gleam +++ /dev/null @@ -1,184 +0,0 @@ -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 941cb1e..0f7287b 100644 --- a/src/gleam_fetch_ffi.mjs +++ b/src/gleam_fetch_ffi.mjs @@ -165,14 +165,3 @@ 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 8b7f06e..495b6a6 100644 --- a/test/gleam_fetch_test.gleam +++ b/test/gleam_fetch_test.gleam @@ -1,5 +1,4 @@ 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 @@ -196,6 +195,7 @@ fn setup_form_data() { |> form_data.append_bits("second-key", <<"second-value-bits":utf8>>) } +// Node only supports `redirect` option. pub fn complex_fetch_options_test() { let req = request.new() @@ -205,15 +205,270 @@ pub fn complex_fetch_options_test() { |> 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 + fetch.fetch_options() + |> fetch.cache(fetch.NoStore) + |> fetch.cors(fetch.Cors) + |> fetch.credentials(fetch.CredentialsOmit) + |> fetch.keepalive(True) + |> fetch.priority(fetch.High) + |> fetch.redirect(fetch.Follow) + + use result <- promise.await(fetch.send_options(req, options)) + + let assert Ok(resp) = result + let assert 200 = resp.status + + promise.resolve(Nil) +} + +// Node doesn't support `cache` option, let's check that it passes at least. +// For further reference see: +// https://github.com/node-fetch/node-fetch#class-request +pub fn fetch_cache_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_default = + fetch.fetch_options() + |> fetch.cache(fetch.Default) + + let options_no_store = + fetch.fetch_options() + |> fetch.cache(fetch.NoStore) + + let options_reload = + fetch.fetch_options() + |> fetch.cache(fetch.Reload) + + let options_no_cache = + fetch.fetch_options() + |> fetch.cache(fetch.NoCache) + + let options_force_cache = + fetch.fetch_options() + |> fetch.cache(fetch.ForceCache) + + use result_default <- promise.await(fetch.send_options(req, options_default)) + use result_no_store <- promise.await(fetch.send_options(req, options_no_store)) + use result_reload <- promise.await(fetch.send_options(req, options_reload)) + use result_no_cache <- promise.await(fetch.send_options(req, options_no_cache)) + use result_force_cache <- promise.await(fetch.send_options( + req, + options_force_cache, + )) + + let assert Ok(resp) = result_default + let assert 200 = resp.status + let assert Ok(resp) = result_no_store + let assert 200 = resp.status + let assert Ok(resp) = result_reload + let assert 200 = resp.status + let assert Ok(resp) = result_no_cache + let assert 200 = resp.status + let assert Ok(resp) = result_force_cache + let assert 200 = resp.status + + promise.resolve(Nil) +} + +// Node doesn't support `credentials` option, let's check that it passes at least. +// For further reference see: +// https://github.com/node-fetch/node-fetch#class-request +pub fn fetch_credentials_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_omit = + fetch.fetch_options() + |> fetch.credentials(fetch.CredentialsOmit) + + let options_same_origin = + fetch.fetch_options() + |> fetch.credentials(fetch.CredentialsSameOrigin) + + let options_include = + fetch.fetch_options() + |> fetch.credentials(fetch.CredentialsInclude) + + use result_omit <- promise.await(fetch.send_options(req, options_omit)) + use result_same_origin <- promise.await(fetch.send_options( + req, + options_same_origin, + )) + use result_include <- promise.await(fetch.send_options(req, options_include)) + + let assert Ok(resp) = result_omit + let assert 200 = resp.status + let assert Ok(resp) = result_same_origin + let assert 200 = resp.status + let assert Ok(resp) = result_include + let assert 200 = resp.status + + promise.resolve(Nil) +} + +// Node doesn't support `keepalive` option, let's check that it passes at least. +// For further reference see: +// https://github.com/node-fetch/node-fetch#class-request +pub fn fetch_keepalive_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_keepalive = + fetch.fetch_options() + |> fetch.keepalive(True) + + let options_no_keepalive = + fetch.fetch_options() + |> fetch.keepalive(False) + + use result_keepalive <- promise.await(fetch.send_options( + req, + options_keepalive, + )) + use result_no_keepalive <- promise.await(fetch.send_options( + req, + options_no_keepalive, + )) + + let assert Ok(resp) = result_keepalive + let assert 200 = resp.status + let assert Ok(resp) = result_no_keepalive + let assert 200 = resp.status + + promise.resolve(Nil) +} + +// Node doesn't support `mode` option, let's check that it passes at least. +// For further reference see: +// https://github.com/node-fetch/node-fetch#class-request +pub fn fetch_mods_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_default = + fetch.fetch_options() + |> fetch.cache(fetch.Default) + + let options_no_store = + fetch.fetch_options() + |> fetch.cache(fetch.NoStore) + + let options_reload = + fetch.fetch_options() + |> fetch.cache(fetch.Reload) + + let options_no_cache = + fetch.fetch_options() + |> fetch.cache(fetch.NoCache) + + let options_force_cache = + fetch.fetch_options() + |> fetch.cache(fetch.ForceCache) + + use result_default <- promise.await(fetch.send_options(req, options_default)) + use result_no_store <- promise.await(fetch.send_options(req, options_no_store)) + use result_reload <- promise.await(fetch.send_options(req, options_reload)) + use result_no_cache <- promise.await(fetch.send_options(req, options_no_cache)) + use result_force_cache <- promise.await(fetch.send_options( + req, + options_force_cache, + )) + + let assert Ok(resp) = result_default + let assert 200 = resp.status + let assert Ok(resp) = result_no_store + let assert 200 = resp.status + let assert Ok(resp) = result_reload + let assert 200 = resp.status + let assert Ok(resp) = result_no_cache + let assert 200 = resp.status + let assert Ok(resp) = result_force_cache + let assert 200 = resp.status + + promise.resolve(Nil) +} + +// It's difficult to test `priority` as it just increases the probability +// of finishing first +pub fn fetch_priority_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_high = + fetch.fetch_options() + |> fetch.priority(fetch.High) + + let options_low = + fetch.fetch_options() + |> fetch.priority(fetch.Low) + + let options_auto = + fetch.fetch_options() + |> fetch.priority(fetch.Auto) + + use result_high <- promise.await(fetch.send_options(req, options_high)) + use result_low <- promise.await(fetch.send_options(req, options_low)) + use result_auto <- promise.await(fetch.send_options(req, options_auto)) + + let assert Ok(resp) = result_high + let assert 200 = resp.status + let assert Ok(resp) = result_low + let assert 200 = resp.status + let assert Ok(resp) = result_auto + let assert 200 = resp.status + + promise.resolve(Nil) +} + +pub fn fetch_redirect_test() { + // Initiates redirect + let req = + request.new() + |> request.set_method(Get) + |> request.set_host("postman-echo.com") + + let options_follow = + fetch.fetch_options() + |> fetch.redirect(fetch.Follow) + + let options_error = + fetch.fetch_options() + |> fetch.redirect(fetch.Error) + + let options_manual = + fetch.fetch_options() + |> fetch.redirect(fetch.Manual) + + use result_follow <- promise.await(fetch.send_options(req, options_follow)) + use result_error <- promise.await(fetch.send_options(req, options_error)) + use result_manual <- promise.await(fetch.send_options(req, options_manual)) + + let assert Ok(resp) = result_follow + let assert 200 = resp.status + let assert Error(_) = result_error + let assert Ok(resp) = result_manual + let assert 302 = resp.status + promise.resolve(Nil) }