mirror of
https://github.com/adoyle0/leptos-use.git
synced 2025-01-22 16:49:22 -05:00
finished use_cookie
This commit is contained in:
parent
c9381abe26
commit
ee62623950
13 changed files with 776 additions and 87 deletions
|
@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- The trait `Codec` has been renamed to `StringCodec` and has been moved to `util::StringCodec`.
|
||||
- The struct `StringCodec` has been renamed to `FromToStringCodec` and has been moved to `util::FromToStringCodec`.
|
||||
- The structs `JsonCodec` and `ProstCodec` have been moved to `util` as well.
|
||||
- The function `use_storage` now requires type parameters for the stored type and the codec like all the other
|
||||
`...storage...` functions.
|
||||
|
||||
### Fixes 🍕
|
||||
|
||||
|
|
13
Cargo.toml
13
Cargo.toml
|
@ -28,6 +28,7 @@ js-sys = "0.3"
|
|||
lazy_static = "1"
|
||||
leptos = "0.6"
|
||||
leptos_axum = { version = "0.6", optional = true }
|
||||
leptos_actix = { version = "0.6", optional = true }
|
||||
num = { version = "0.4", optional = true }
|
||||
paste = "1"
|
||||
prost = { version = "0.12", optional = true }
|
||||
|
@ -44,7 +45,7 @@ features = [
|
|||
"AddEventListenerOptions",
|
||||
"BinaryType",
|
||||
"BroadcastChannel",
|
||||
"Coordinates",
|
||||
"Coordinates",
|
||||
"CloseEvent",
|
||||
"CssStyleDeclaration",
|
||||
"CustomEvent",
|
||||
|
@ -62,7 +63,7 @@ features = [
|
|||
"FileList",
|
||||
"Geolocation",
|
||||
"HtmlDocument",
|
||||
"HtmlElement",
|
||||
"HtmlElement",
|
||||
"HtmlLinkElement",
|
||||
"HtmlStyleElement",
|
||||
"IntersectionObserver",
|
||||
|
@ -72,7 +73,7 @@ features = [
|
|||
"MediaQueryList",
|
||||
"MediaStream",
|
||||
"MediaStreamTrack",
|
||||
"MessageEvent",
|
||||
"MessageEvent",
|
||||
"MouseEvent",
|
||||
"MutationObserver",
|
||||
"MutationObserverInit",
|
||||
|
@ -120,8 +121,12 @@ features = [
|
|||
"WritableStreamDefaultWriter",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
|
||||
[features]
|
||||
actix = ["dep:actix-web", "dep:http0_2"]
|
||||
actix = ["dep:actix-web", "dep:leptos_actix", "dep:http0_2"]
|
||||
axum = ["dep:leptos_axum", "dep:http1"]
|
||||
docs = []
|
||||
math = ["num"]
|
||||
|
|
|
@ -7,24 +7,24 @@ edition = "2021"
|
|||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.6.4", optional = true }
|
||||
axum = { version = "0.7", optional = true }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
leptos = { version = "0.6", features = ["nightly"] }
|
||||
leptos_axum = { version = "0.5", optional = true }
|
||||
leptos_meta = { version = "0.5", features = ["nightly"] }
|
||||
leptos_router = { version = "0.5", features = ["nightly"] }
|
||||
leptos_axum = { version = "0.6", optional = true }
|
||||
leptos_meta = { version = "0.6", features = ["nightly"] }
|
||||
leptos_router = { version = "0.6", features = ["nightly"] }
|
||||
leptos-use = { path = "../.." }
|
||||
log = "0.4"
|
||||
simple_logger = "4"
|
||||
tokio = { version = "1.25.0", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4.3", features = ["fs"], optional = true }
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2.88"
|
||||
thiserror = "1.0.38"
|
||||
tracing = { version = "0.1.37", optional = true }
|
||||
http = "0.2.8"
|
||||
http = "1"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
|
@ -38,7 +38,8 @@ ssr = [
|
|||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:tracing",
|
||||
"leptos-use/ssr"
|
||||
"leptos-use/ssr",
|
||||
"leptos-use/axum",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
|
|
@ -3,11 +3,12 @@ use leptos::ev::{keypress, KeyboardEvent};
|
|||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos_use::storage::{use_local_storage, StringCodec};
|
||||
use leptos_use::storage::use_local_storage;
|
||||
use leptos_use::utils::FromToStringCodec;
|
||||
use leptos_use::{
|
||||
use_color_mode, use_debounce_fn, use_event_listener, use_interval, use_intl_number_format,
|
||||
use_preferred_dark, use_timestamp, use_window, ColorMode, UseColorModeReturn,
|
||||
UseIntervalReturn, UseIntlNumberFormatOptions,
|
||||
use_color_mode, use_cookie_with_options, use_debounce_fn, use_event_listener, use_interval,
|
||||
use_intl_number_format, use_preferred_dark, use_timestamp, use_window, ColorMode,
|
||||
UseColorModeReturn, UseCookieOptions, UseIntervalReturn, UseIntlNumberFormatOptions,
|
||||
};
|
||||
|
||||
#[component]
|
||||
|
@ -15,6 +16,11 @@ pub fn App() -> impl IntoView {
|
|||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
expect_context::<http::request::Parts>();
|
||||
}
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/start-axum.css"/>
|
||||
|
||||
|
@ -38,7 +44,7 @@ pub fn App() -> impl IntoView {
|
|||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
// Creates a reactive value to update the button
|
||||
let (count, set_count, _) = use_local_storage::<i32, StringCodec>("count-state");
|
||||
let (count, set_count, _) = use_local_storage::<i32, FromToStringCodec>("count-state");
|
||||
let on_click = move |_| set_count.update(|count| *count += 1);
|
||||
|
||||
let nf = use_intl_number_format(
|
||||
|
@ -70,6 +76,13 @@ fn HomePage() -> impl IntoView {
|
|||
|
||||
let is_dark_preferred = use_preferred_dark();
|
||||
|
||||
let (session_cookie, _) = use_cookie_with_options::<String, FromToStringCodec>(
|
||||
"session",
|
||||
UseCookieOptions::<String, _>::default()
|
||||
.max_age(3600)
|
||||
.default_value(Some("Bogus session string".to_owned())),
|
||||
);
|
||||
|
||||
view! {
|
||||
<h1>Leptos-Use SSR Example</h1>
|
||||
<button on:click=on_click>Click Me: {count}</button>
|
||||
|
@ -83,6 +96,7 @@ fn HomePage() -> impl IntoView {
|
|||
<p>{timestamp}</p>
|
||||
<p>Dark preferred: {is_dark_preferred}</p>
|
||||
<LocalStorageTest/>
|
||||
<p>Session cookie: {session_cookie}</p>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,7 +104,7 @@ fn HomePage() -> impl IntoView {
|
|||
pub fn LocalStorageTest() -> impl IntoView {
|
||||
let UseIntervalReturn { counter, .. } = use_interval(1000);
|
||||
logging::log!("test log");
|
||||
let (state, set_state, ..) = use_local_storage::<String, StringCodec>("test-state");
|
||||
let (state, set_state, ..) = use_local_storage::<String, FromToStringCodec>("test-state");
|
||||
|
||||
view! {
|
||||
<p>{counter}</p>
|
||||
|
|
|
@ -2,7 +2,7 @@ use cfg_if::cfg_if;
|
|||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
body::Body,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
|
@ -25,12 +25,12 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
|||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
|
|
|
@ -27,11 +27,9 @@ async fn main() {
|
|||
.fallback(file_and_error_handler)
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ console_error_panic_hook = "0.1"
|
|||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos-use = { path = "../..", features = ["docs"] }
|
||||
rand = "0.8"
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
web-sys = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -1,15 +1,27 @@
|
|||
use leptos::*;
|
||||
use leptos_use::docs::demo_or_body;
|
||||
use leptos_use::use_cookie;
|
||||
use leptos_use::utils::FromToStringCodec;
|
||||
use rand::prelude::*;
|
||||
|
||||
#[component]
|
||||
fn Demo() -> impl IntoView {
|
||||
if let Some(cookie) = use_cookie("auth") {
|
||||
view! { <div>"'auth' cookie set to " <code>"`" {cookie.value().to_string()} "`"</code></div> }
|
||||
.into_view()
|
||||
} else {
|
||||
view! { <div>"No 'auth' cookie set"</div> }
|
||||
.into_view()
|
||||
let (counter, set_counter) = use_cookie::<u32, FromToStringCodec>("counter");
|
||||
|
||||
let reset = move || set_counter(Some(random()));
|
||||
|
||||
if counter().is_none() {
|
||||
reset();
|
||||
}
|
||||
|
||||
let increase = move || {
|
||||
set_counter(counter().map(|c| c + 1));
|
||||
};
|
||||
|
||||
view! {
|
||||
<p>Counter: {move || counter().map(|c| c.to_string()).unwrap_or("—".to_string())}</p>
|
||||
<button on:click=move |_| reset()>Reset</button>
|
||||
<button on:click=move |_| increase()>+</button>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ pub fn use_local_storage_with_options<T, C>(
|
|||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + PartialEq,
|
||||
C: StringCodec<T>,
|
||||
C: StringCodec<T> + Default,
|
||||
{
|
||||
use_storage_with_options(StorageType::Local, key, options)
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ pub fn use_session_storage_with_options<T, C>(
|
|||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + PartialEq,
|
||||
C: StringCodec<T>,
|
||||
C: StringCodec<T> + Default,
|
||||
{
|
||||
use_storage_with_options(StorageType::Session, key, options)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use crate::utils::FromToStringCodec;
|
||||
use crate::{
|
||||
core::{MaybeRwSignal, StorageType},
|
||||
utils::{FilterOptions, StringCodec},
|
||||
|
@ -29,7 +28,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
|
|||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions};
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage};
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// # use leptos_use::utils::{FromToStringCodec, JsonCodec, ProstCodec};
|
||||
/// #
|
||||
|
@ -46,7 +45,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
|
|||
/// let (count, set_count, _) = use_session_storage::<i32, JsonCodec>("my-count-kept-in-js");
|
||||
///
|
||||
/// // Bind string with SessionStorage stored in ProtoBuf format:
|
||||
/// let (id, set_id, _) = use_storage_with::<String, ProstCodec>(
|
||||
/// let (id, set_id, _) = use_storage::<String, ProstCodec>(
|
||||
/// StorageType::Session,
|
||||
/// "my-id",
|
||||
/// );
|
||||
|
@ -76,15 +75,15 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
|
|||
///
|
||||
/// All you need to do is to implement the [`StringCodec`] trait together with `Default` and `Clone`.
|
||||
#[inline(always)]
|
||||
pub fn use_storage(
|
||||
pub fn use_storage<T, C>(
|
||||
storage_type: StorageType,
|
||||
key: impl AsRef<str>,
|
||||
) -> (Signal<String>, WriteSignal<String>, impl Fn() + Clone) {
|
||||
use_storage_with_options::<String, FromToStringCodec>(
|
||||
storage_type,
|
||||
key,
|
||||
UseStorageOptions::default(),
|
||||
)
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Default + Clone + PartialEq,
|
||||
C: StringCodec<T> + Default,
|
||||
{
|
||||
use_storage_with_options::<T, C>(storage_type, key, UseStorageOptions::default())
|
||||
}
|
||||
|
||||
/// Version of [`use_storage`] that accepts [`UseStorageOptions`].
|
||||
|
@ -95,7 +94,7 @@ pub fn use_storage_with_options<T, C>(
|
|||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + PartialEq,
|
||||
C: StringCodec<T>,
|
||||
C: StringCodec<T> + Default,
|
||||
{
|
||||
let UseStorageOptions {
|
||||
codec,
|
||||
|
|
|
@ -65,6 +65,10 @@ use wasm_bindgen::JsValue;
|
|||
/// # view! { }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Create Your Own Custom Codec
|
||||
///
|
||||
/// All you need to do is to implement the [`StringCodec`] trait together with `Default` and `Clone`.
|
||||
pub fn use_broadcast_channel<T, C>(
|
||||
name: &str,
|
||||
) -> UseBroadcastChannelReturn<T, impl Fn(&T) + Clone, impl Fn() + Clone, C::Error>
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
use cookie::Cookie;
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
/// Get a cookie by name, for both SSR and CSR
|
||||
use crate::core::now;
|
||||
use crate::utils::StringCodec;
|
||||
use cookie::time::{Duration, OffsetDateTime};
|
||||
use cookie::{Cookie, CookieJar, SameSite};
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use leptos::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// SSR-friendly and reactive cookie access.
|
||||
///
|
||||
/// ## Demo
|
||||
///
|
||||
|
@ -9,34 +16,69 @@ use default_struct_builder::DefaultBuilder;
|
|||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// This provides you with the cookie that has been set. For more details on how to use the cookie provided, refer: https://docs.rs/cookie/0.18/cookie/struct.Cookie.html
|
||||
/// The example below creates a cookie called `counter`. If the cookie doesn't exist, it is initially set to a random value.
|
||||
/// Whenever we update the `counter` variable, the cookie will be updated accordingly.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::use_cookie;
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// # use rand::prelude::*;
|
||||
///
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
/// if let Some(cookie) = use_cookie("auth") {
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// format!("'auth' cookie set to `{}`", cookie.value())
|
||||
/// </div>
|
||||
/// }.into_view()
|
||||
/// } else {
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// "No 'auth' cookie set"
|
||||
/// </div>
|
||||
/// }.into_view()
|
||||
/// let (counter, set_counter) = use_cookie::<u32, FromToStringCodec>("counter");
|
||||
///
|
||||
/// let reset = move || set_counter.set(Some(random()));
|
||||
///
|
||||
/// if counter.get().is_none() {
|
||||
/// reset();
|
||||
/// }
|
||||
///
|
||||
/// let increase = move || {
|
||||
/// set_counter.set(counter.get().map(|c| c + 1));
|
||||
/// };
|
||||
///
|
||||
/// view! {
|
||||
/// <p>Counter: {move || counter.get().map(|c| c.to_string()).unwrap_or("—".to_string())}</p>
|
||||
/// <button on:click=move |_| reset()>Reset</button>
|
||||
/// <button on:click=move |_| increase()>+</button>
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// See [`StringCodec`] for details on how to handle versioning — dealing with data that can outlast your code.
|
||||
///
|
||||
/// ## Cookie attributes
|
||||
///
|
||||
/// As part of the options when you use `use_cookie_with_options` you can specify cookie attributes.
|
||||
///
|
||||
/// ```
|
||||
/// # use cookie::SameSite;
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
/// let (cookie, set_cookie) = use_cookie_with_options::<bool, FromToStringCodec>(
|
||||
/// "user_info",
|
||||
/// UseCookieOptions::default()
|
||||
/// .max_age(3600_000) // one hour
|
||||
/// .same_site(SameSite::Lax)
|
||||
/// );
|
||||
/// #
|
||||
/// # view! {}
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Server-Side Rendering
|
||||
///
|
||||
/// This works equally well on the server or the client.
|
||||
/// On the server this function gets the cookie from the HTTP request header.
|
||||
/// On the server this function reads the cookie from the HTTP request header and writes it back into
|
||||
/// the HTTP response header according to options (if provided).
|
||||
/// The returned `WriteSignal` will not affect the cookie headers on the server.
|
||||
///
|
||||
/// > If you're using `axum` you have to enable the `"axum"` feature in your Cargo.toml.
|
||||
/// > In case it's `actix-web` enable the feature `"actix"`.
|
||||
|
@ -44,62 +86,379 @@ use default_struct_builder::DefaultBuilder;
|
|||
/// ### Bring your own header
|
||||
///
|
||||
/// In case you're neither using Axum nor Actix, or the default implementation is not to your liking,
|
||||
/// you can provide your own way of reading the cookie header value.
|
||||
/// you can provide your own way of reading and writing the cookie header value.
|
||||
///
|
||||
/// ```
|
||||
/// # use cookie::Cookie;
|
||||
/// # use leptos::*;
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
|
||||
/// # use leptos_use::utils::JsonCodec;
|
||||
/// #
|
||||
/// # #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
/// # pub struct Auth {
|
||||
/// # pub username: String,
|
||||
/// # pub token: String,
|
||||
/// # }
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
/// use_cookie_with_options("auth", UseCookieOptions::default().ssr_cookies_header_getter(|| {
|
||||
/// #[cfg(feature = "ssr")]
|
||||
/// {
|
||||
/// "Somehow get the value of the cookie header as a string".to_owned()
|
||||
/// }
|
||||
/// }));
|
||||
/// use_cookie_with_options::<Auth, JsonCodec>(
|
||||
/// "auth",
|
||||
/// UseCookieOptions::default()
|
||||
/// .ssr_cookies_header_getter(|| {
|
||||
/// #[cfg(feature = "ssr")]
|
||||
/// {
|
||||
/// "Somehow get the value of the cookie header as a string".to_owned()
|
||||
/// }
|
||||
/// })
|
||||
/// .ssr_set_cookie(|cookie: &Cookie| {
|
||||
/// #[cfg(feature = "ssr")]
|
||||
/// {
|
||||
/// // somehow insert the Set-Cookie header for this cookie
|
||||
/// }
|
||||
/// }),
|
||||
/// );
|
||||
/// # view! {}
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn use_cookie(cookie_name: &str) -> Option<Cookie<'static>> {
|
||||
use_cookie_with_options(cookie_name, UseCookieOptions::default())
|
||||
///
|
||||
/// ## Create Your Own Custom Codec
|
||||
///
|
||||
/// All you need to do is to implement the [`StringCodec`] trait together with `Default` and `Clone`.
|
||||
pub fn use_cookie<T, C>(cookie_name: &str) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
|
||||
where
|
||||
C: StringCodec<T> + Default + Clone,
|
||||
T: Clone,
|
||||
{
|
||||
use_cookie_with_options::<T, C>(cookie_name, UseCookieOptions::default())
|
||||
}
|
||||
|
||||
/// Version of [`use_cookie`] that takes [`UseCookieOptions`].
|
||||
pub fn use_cookie_with_options(
|
||||
pub fn use_cookie_with_options<T, C>(
|
||||
cookie_name: &str,
|
||||
options: UseCookieOptions,
|
||||
) -> Option<Cookie<'static>> {
|
||||
options: UseCookieOptions<T, C::Error>,
|
||||
) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
|
||||
where
|
||||
C: StringCodec<T> + Default + Clone,
|
||||
T: Clone,
|
||||
{
|
||||
let UseCookieOptions {
|
||||
max_age,
|
||||
expires,
|
||||
http_only,
|
||||
secure,
|
||||
domain,
|
||||
path,
|
||||
same_site,
|
||||
ssr_cookies_header_getter,
|
||||
ssr_set_cookie,
|
||||
default_value,
|
||||
readonly,
|
||||
on_error,
|
||||
} = options;
|
||||
|
||||
let cookies = read_cookies_string(ssr_cookies_header_getter);
|
||||
let delay = if let Some(max_age) = max_age {
|
||||
Some(max_age * 1000)
|
||||
} else {
|
||||
expires.map(|expires| expires * 1000 - now() as i64)
|
||||
};
|
||||
|
||||
Cookie::split_parse_encoded(cookies)
|
||||
.filter_map(|cookie| cookie.ok())
|
||||
.find(|cookie| cookie.name() == cookie_name)
|
||||
.map(|cookie| cookie.into_owned())
|
||||
let has_expired = if let Some(delay) = delay {
|
||||
delay <= 0
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let (cookie, set_cookie) = create_signal(None::<T>);
|
||||
|
||||
let jar = store_value(CookieJar::new());
|
||||
let codec = C::default();
|
||||
|
||||
if !has_expired {
|
||||
let ssr_cookies_header_getter = Rc::clone(&ssr_cookies_header_getter);
|
||||
|
||||
jar.update_value(|jar| {
|
||||
*jar = load_and_parse_cookie_jar(ssr_cookies_header_getter);
|
||||
|
||||
set_cookie.set(
|
||||
jar.get(cookie_name)
|
||||
.and_then(|c| {
|
||||
codec
|
||||
.decode(c.value().to_string())
|
||||
.map_err(|err| on_error(err))
|
||||
.ok()
|
||||
})
|
||||
.or(default_value),
|
||||
);
|
||||
});
|
||||
|
||||
handle_expiration(delay, set_cookie);
|
||||
} else {
|
||||
logging::debug_warn!(
|
||||
"not setting cookie '{}' because it has already expired",
|
||||
cookie_name
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
use crate::{
|
||||
use_broadcast_channel, watch_pausable, UseBroadcastChannelReturn, WatchPausableReturn,
|
||||
};
|
||||
|
||||
let UseBroadcastChannelReturn { message, post, .. } =
|
||||
use_broadcast_channel::<Option<String>, OptionStringCodec>(&format!(
|
||||
"leptos-use:cookies:{cookie_name}"
|
||||
));
|
||||
|
||||
let on_cookie_change = {
|
||||
let cookie_name = cookie_name.to_owned();
|
||||
let ssr_cookies_header_getter = Rc::clone(&ssr_cookies_header_getter);
|
||||
let codec = codec.clone();
|
||||
let on_error = Rc::clone(&on_error);
|
||||
let domain = domain.clone();
|
||||
let path = path.clone();
|
||||
|
||||
move || {
|
||||
if readonly {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = cookie.with_untracked(|cookie| {
|
||||
cookie
|
||||
.as_ref()
|
||||
.and_then(|cookie| codec.encode(cookie).map_err(|err| on_error(err)).ok())
|
||||
});
|
||||
|
||||
if value
|
||||
== jar.with_value(|jar| jar.get(&cookie_name).map(|c| c.value().to_owned()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
jar.update_value(|jar| {
|
||||
write_client_cookie(
|
||||
&cookie_name,
|
||||
&value,
|
||||
jar,
|
||||
max_age,
|
||||
expires,
|
||||
&domain,
|
||||
&path,
|
||||
same_site,
|
||||
secure,
|
||||
http_only,
|
||||
Rc::clone(&ssr_cookies_header_getter),
|
||||
);
|
||||
});
|
||||
|
||||
post(&value);
|
||||
}
|
||||
};
|
||||
|
||||
let WatchPausableReturn {
|
||||
pause,
|
||||
resume,
|
||||
stop,
|
||||
..
|
||||
} = watch_pausable(move || cookie.get(), {
|
||||
let on_cookie_change = on_cookie_change.clone();
|
||||
|
||||
move |_, _, _| {
|
||||
on_cookie_change();
|
||||
}
|
||||
});
|
||||
|
||||
// listen to cookie changes from the broadcast channel
|
||||
create_effect({
|
||||
let ssr_cookies_header_getter = Rc::clone(&ssr_cookies_header_getter);
|
||||
let cookie_name = cookie_name.to_owned();
|
||||
|
||||
move |_| {
|
||||
if let Some(message) = message.get() {
|
||||
pause();
|
||||
|
||||
if let Some(message) = message {
|
||||
match codec.decode(message.clone()) {
|
||||
Ok(value) => {
|
||||
let ssr_cookies_header_getter =
|
||||
Rc::clone(&ssr_cookies_header_getter);
|
||||
|
||||
jar.update_value(|jar| {
|
||||
update_client_cookie_jar(
|
||||
&cookie_name,
|
||||
&Some(message),
|
||||
jar,
|
||||
max_age,
|
||||
expires,
|
||||
&domain,
|
||||
&path,
|
||||
same_site,
|
||||
secure,
|
||||
http_only,
|
||||
ssr_cookies_header_getter,
|
||||
);
|
||||
});
|
||||
|
||||
set_cookie.set(Some(value));
|
||||
}
|
||||
Err(err) => {
|
||||
on_error(err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let cookie_name = cookie_name.clone();
|
||||
|
||||
jar.update_value(|jar| {
|
||||
jar.force_remove(cookie_name);
|
||||
});
|
||||
|
||||
set_cookie.set(None);
|
||||
}
|
||||
|
||||
resume();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
on_cleanup(move || {
|
||||
stop();
|
||||
on_cookie_change();
|
||||
});
|
||||
|
||||
let _ = ssr_set_cookie;
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
if !readonly {
|
||||
let value = cookie
|
||||
.with_untracked(|cookie| {
|
||||
cookie
|
||||
.as_ref()
|
||||
.map(|cookie| codec.encode(&cookie).map_err(|err| on_error(err)).ok())
|
||||
})
|
||||
.flatten();
|
||||
jar.update_value(|jar| {
|
||||
write_server_cookie(
|
||||
cookie_name,
|
||||
value,
|
||||
jar,
|
||||
max_age,
|
||||
expires,
|
||||
domain,
|
||||
path,
|
||||
same_site,
|
||||
secure,
|
||||
http_only,
|
||||
ssr_set_cookie,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
(cookie.into(), set_cookie)
|
||||
}
|
||||
|
||||
/// Options for [`use_cookie_with_options`].
|
||||
#[derive(DefaultBuilder)]
|
||||
pub struct UseCookieOptions {
|
||||
pub struct UseCookieOptions<T, Err> {
|
||||
/// [`Max-Age` of the cookie](https://tools.ietf.org/html/rfc6265#section-5.2.2) in seconds. The returned signal will turn to `None` after the max age is reached.
|
||||
/// Default: `None`
|
||||
///
|
||||
/// > The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states
|
||||
/// > that if both `expires` and `max_age` is set, then `max_age` takes precedence,
|
||||
/// > but not all clients may obey this, so if both are set, they should point to the same date and time!
|
||||
///
|
||||
/// > If neither of `expires` and `max_age` is set, the cookie will be session-only and removed when the user closes their browser.
|
||||
#[builder(into)]
|
||||
max_age: Option<i64>,
|
||||
|
||||
/// [Expiration date-time of the cookie](https://tools.ietf.org/html/rfc6265#section-5.2.1) as UNIX timestamp in seconds.
|
||||
/// The signal will turn to `None` after the expiration date-time is reached.
|
||||
/// Default: `None`
|
||||
///
|
||||
/// > The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states
|
||||
/// > that if both `expires` and `max_age` is set, then `max_age` takes precedence,
|
||||
/// > but not all clients may obey this, so if both are set, they should point to the same date and time!
|
||||
///
|
||||
/// > If neither of `expires` and `max_age` is set, the cookie will be session-only and removed when the user closes their browser.
|
||||
#[builder(into)]
|
||||
expires: Option<i64>,
|
||||
|
||||
/// Specifies the [`HttpOnly` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6).
|
||||
/// When `true`, the `HttpOnly` attribute is set; otherwise it is not.
|
||||
/// By default, the `HttpOnly` attribute is not set.
|
||||
///
|
||||
/// > Be careful when setting this to `true`, as compliant clients will not allow client-side JavaScript to see the cookie in `document.cookie`.
|
||||
http_only: bool,
|
||||
|
||||
/// Specifies the value for the [`Secure` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5).
|
||||
/// When `true`, the `Secure` attribute is set; otherwise it is not.
|
||||
/// By default, the `Secure` attribute is not set.
|
||||
///
|
||||
/// > Be careful when setting this to `true`, as compliant clients will not send the cookie back to the
|
||||
/// > server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
|
||||
secure: bool,
|
||||
|
||||
/// Specifies the value for the [`Domain` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.3).
|
||||
/// By default, no domain is set, and most clients will consider applying the cookie only to the current domain.
|
||||
#[builder(into)]
|
||||
domain: Option<String>,
|
||||
|
||||
/// Specifies the value for the [`Path` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.4).
|
||||
/// By default, the path is considered the ["default path"](https://tools.ietf.org/html/rfc6265#section-5.1.4).
|
||||
#[builder(into)]
|
||||
path: Option<String>,
|
||||
|
||||
/// Specifies the value for the [`SameSite` cookie attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).
|
||||
///
|
||||
/// - `'Some(SameSite::Lax)'` will set the `SameSite` attribute to `Lax` for lax same-site enforcement.
|
||||
/// - `'Some(SameSite::None)'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
|
||||
/// - `'Some(SameSite::Strict)'` will set the `SameSite` attribute to `Strict` for strict same-site enforcement.
|
||||
/// - `None` will not set the `SameSite` attribute (default).
|
||||
///
|
||||
/// More information about the different enforcement levels can be found in [the specification](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).
|
||||
#[builder(into)]
|
||||
same_site: Option<SameSite>,
|
||||
|
||||
/// The default cookie value in case the cookie is not set.
|
||||
/// Defaults to `None`.
|
||||
default_value: Option<T>,
|
||||
|
||||
/// If `true` the returned `WriteSignal` will not affect the actual cookie.
|
||||
/// Default: `false`
|
||||
readonly: bool,
|
||||
|
||||
/// Getter function to return the string value of the cookie header.
|
||||
/// When you use one of the features "axum" or "actix" there's a valid default implementation provided.
|
||||
ssr_cookies_header_getter: Box<dyn Fn() -> String>,
|
||||
ssr_cookies_header_getter: Rc<dyn Fn() -> String>,
|
||||
|
||||
/// Function to add a set cookie header to the response on the server.
|
||||
/// When you use one of the features "axum" or "actix" there's a valid default implementation provided.
|
||||
ssr_set_cookie: Rc<dyn Fn(&Cookie)>,
|
||||
|
||||
/// Callback for encoding/decoding errors. Defaults to logging the error to the console.
|
||||
on_error: Rc<dyn Fn(Err)>,
|
||||
}
|
||||
|
||||
impl Default for UseCookieOptions {
|
||||
impl<T, Err> Default for UseCookieOptions<T, Err> {
|
||||
#[allow(dead_code)]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ssr_cookies_header_getter: Box::new(move || {
|
||||
max_age: None,
|
||||
expires: None,
|
||||
http_only: false,
|
||||
default_value: None,
|
||||
readonly: false,
|
||||
secure: false,
|
||||
domain: None,
|
||||
path: None,
|
||||
same_site: None,
|
||||
ssr_cookies_header_getter: Rc::new(move || {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
#[cfg(any(feature = "axum", feature = "actix"))]
|
||||
use leptos::expect_context;
|
||||
|
||||
#[cfg(all(feature = "actix", feature = "axum"))]
|
||||
compile_error!("You cannot enable only one of features \"actix\" and \"axum\" at the same time");
|
||||
|
||||
|
@ -142,11 +501,52 @@ impl Default for UseCookieOptions {
|
|||
#[cfg(not(feature = "ssr"))]
|
||||
"".to_owned()
|
||||
}),
|
||||
ssr_set_cookie: Rc::new(|cookie: &Cookie| {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
#[cfg(feature = "actix")]
|
||||
use leptos_actix::ResponseOptions;
|
||||
#[cfg(feature = "axum")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
const SET_COOKIE: http0_2::HeaderName = http0_2::header::SET_COOKIE;
|
||||
#[cfg(feature = "axum")]
|
||||
const SET_COOKIE: http1::HeaderName = http1::header::SET_COOKIE;
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
type HeaderValue = http0_2::HeaderValue;
|
||||
#[cfg(feature = "axum")]
|
||||
type HeaderValue = http1::HeaderValue;
|
||||
|
||||
#[cfg(all(not(feature = "axum"), not(feature = "actix")))]
|
||||
{
|
||||
let _ = cookie;
|
||||
leptos::logging::warn!("If you're using use_cookie without the feature `axum` or `actix` enabled, you should provide the option `ssr_set_cookie`");
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "axum", feature = "actix"))]
|
||||
{
|
||||
let response_options = expect_context::<ResponseOptions>();
|
||||
|
||||
if let Ok(header_value) =
|
||||
HeaderValue::from_str(&cookie.encoded().to_string())
|
||||
{
|
||||
response_options.insert_header(SET_COOKIE, header_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = cookie;
|
||||
}),
|
||||
on_error: Rc::new(|_| {
|
||||
logging::error!("cookie (de-/)serialization error");
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_cookies_string(ssr_cookies_header_getter: Box<dyn Fn() -> String>) -> String {
|
||||
fn read_cookies_string(ssr_cookies_header_getter: Rc<dyn Fn() -> String>) -> String {
|
||||
let cookies;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
|
@ -167,3 +567,255 @@ fn read_cookies_string(ssr_cookies_header_getter: Box<dyn Fn() -> String>) -> St
|
|||
|
||||
cookies
|
||||
}
|
||||
|
||||
fn handle_expiration<T>(delay: Option<i64>, set_cookie: WriteSignal<Option<T>>) {
|
||||
if let Some(delay) = delay {
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
use leptos::leptos_dom::helpers::TimeoutHandle;
|
||||
use std::cell::Cell;
|
||||
use std::cell::RefCell;
|
||||
|
||||
// The maximum value allowed on a timeout delay.
|
||||
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
|
||||
const MAX_TIMEOUT_DELAY: i64 = 2_147_483_647;
|
||||
|
||||
let timeout = Rc::new(Cell::new(None::<TimeoutHandle>));
|
||||
let elapsed = Rc::new(Cell::new(0));
|
||||
|
||||
on_cleanup({
|
||||
let timeout = Rc::clone(&timeout);
|
||||
move || {
|
||||
if let Some(timeout) = timeout.take() {
|
||||
timeout.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let create_expiration_timeout = Rc::new(RefCell::new(None::<Box<dyn Fn()>>));
|
||||
|
||||
create_expiration_timeout.replace(Some(Box::new({
|
||||
let timeout = Rc::clone(&timeout);
|
||||
let elapsed = Rc::clone(&elapsed);
|
||||
let create_expiration_timeout = Rc::clone(&create_expiration_timeout);
|
||||
|
||||
move || {
|
||||
if let Some(timeout) = timeout.take() {
|
||||
timeout.clear();
|
||||
}
|
||||
|
||||
let time_remaining = delay - elapsed.get();
|
||||
let timeout_length = time_remaining.min(MAX_TIMEOUT_DELAY);
|
||||
|
||||
let elapsed = Rc::clone(&elapsed);
|
||||
let create_expiration_timeout = Rc::clone(&create_expiration_timeout);
|
||||
|
||||
timeout.set(
|
||||
set_timeout_with_handle(
|
||||
move || {
|
||||
elapsed.set(elapsed.get() + timeout_length);
|
||||
if elapsed.get() < delay {
|
||||
if let Some(create_expiration_timeout) =
|
||||
&*create_expiration_timeout.borrow()
|
||||
{
|
||||
create_expiration_timeout();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
set_cookie.set(None);
|
||||
},
|
||||
std::time::Duration::from_millis(timeout_length as u64),
|
||||
)
|
||||
.ok(),
|
||||
);
|
||||
}
|
||||
})));
|
||||
|
||||
if let Some(create_expiration_timeout) = &*create_expiration_timeout.borrow() {
|
||||
create_expiration_timeout();
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let _ = set_cookie;
|
||||
let _ = delay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn write_client_cookie(
|
||||
name: &str,
|
||||
value: &Option<String>,
|
||||
jar: &mut CookieJar,
|
||||
max_age: Option<i64>,
|
||||
expires: Option<i64>,
|
||||
domain: &Option<String>,
|
||||
path: &Option<String>,
|
||||
same_site: Option<SameSite>,
|
||||
secure: bool,
|
||||
http_only: bool,
|
||||
ssr_cookies_header_getter: Rc<dyn Fn() -> String>,
|
||||
) {
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
update_client_cookie_jar(
|
||||
name,
|
||||
value,
|
||||
jar,
|
||||
max_age,
|
||||
expires,
|
||||
domain,
|
||||
path,
|
||||
same_site,
|
||||
secure,
|
||||
http_only,
|
||||
ssr_cookies_header_getter,
|
||||
);
|
||||
|
||||
let document = document();
|
||||
let document: &web_sys::HtmlDocument = document.unchecked_ref();
|
||||
|
||||
document.set_cookie(&cookie_jar_to_string(jar)).ok();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn update_client_cookie_jar(
|
||||
name: &str,
|
||||
value: &Option<String>,
|
||||
jar: &mut CookieJar,
|
||||
max_age: Option<i64>,
|
||||
expires: Option<i64>,
|
||||
domain: &Option<String>,
|
||||
path: &Option<String>,
|
||||
same_site: Option<SameSite>,
|
||||
secure: bool,
|
||||
http_only: bool,
|
||||
ssr_cookies_header_getter: Rc<dyn Fn() -> String>,
|
||||
) {
|
||||
*jar = load_and_parse_cookie_jar(ssr_cookies_header_getter);
|
||||
|
||||
if let Some(value) = value {
|
||||
let cookie = build_cookie_from_options(
|
||||
name, max_age, expires, http_only, secure, path, same_site, domain, value,
|
||||
);
|
||||
|
||||
jar.add_original(cookie);
|
||||
} else {
|
||||
jar.force_remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn cookie_jar_to_string(jar: &CookieJar) -> String {
|
||||
jar.iter()
|
||||
.map(|c| c.encoded().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ")
|
||||
}
|
||||
|
||||
fn build_cookie_from_options(
|
||||
name: &str,
|
||||
max_age: Option<i64>,
|
||||
expires: Option<i64>,
|
||||
http_only: bool,
|
||||
secure: bool,
|
||||
path: &Option<String>,
|
||||
same_site: Option<SameSite>,
|
||||
domain: &Option<String>,
|
||||
value: &str,
|
||||
) -> Cookie<'static> {
|
||||
let mut cookie = Cookie::build((name, value));
|
||||
if let Some(max_age) = max_age {
|
||||
cookie = cookie.max_age(Duration::milliseconds(max_age));
|
||||
}
|
||||
if let Some(expires) = expires {
|
||||
match OffsetDateTime::from_unix_timestamp(expires) {
|
||||
Ok(expires) => {
|
||||
cookie = cookie.expires(expires);
|
||||
}
|
||||
Err(err) => {
|
||||
logging::debug_warn!("failed to set cookie expiration: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
if http_only {
|
||||
cookie = cookie.http_only(true);
|
||||
}
|
||||
if secure {
|
||||
cookie = cookie.secure(true);
|
||||
}
|
||||
if let Some(domain) = domain {
|
||||
cookie = cookie.domain(domain);
|
||||
}
|
||||
if let Some(path) = path {
|
||||
cookie = cookie.path(path);
|
||||
}
|
||||
if let Some(same_site) = same_site {
|
||||
cookie = cookie.same_site(same_site);
|
||||
}
|
||||
|
||||
let cookie: Cookie = cookie.into();
|
||||
cookie.into_owned()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn write_server_cookie(
|
||||
name: &str,
|
||||
value: Option<String>,
|
||||
jar: &mut CookieJar,
|
||||
max_age: Option<i64>,
|
||||
expires: Option<i64>,
|
||||
domain: Option<String>,
|
||||
path: Option<String>,
|
||||
same_site: Option<SameSite>,
|
||||
secure: bool,
|
||||
http_only: bool,
|
||||
ssr_set_cookie: Rc<dyn Fn(&Cookie)>,
|
||||
) {
|
||||
if let Some(value) = value {
|
||||
let cookie: Cookie = build_cookie_from_options(
|
||||
name, max_age, expires, http_only, secure, &path, same_site, &domain, &value,
|
||||
)
|
||||
.into();
|
||||
|
||||
jar.add(cookie.into_owned());
|
||||
} else {
|
||||
jar.remove(name.to_owned());
|
||||
}
|
||||
|
||||
for cookie in jar.delta() {
|
||||
ssr_set_cookie(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
fn load_and_parse_cookie_jar(ssr_cookies_header_getter: Rc<dyn Fn() -> String>) -> CookieJar {
|
||||
let mut jar = CookieJar::new();
|
||||
let cookies = read_cookies_string(ssr_cookies_header_getter);
|
||||
|
||||
for cookie in Cookie::split_parse_encoded(cookies).flatten() {
|
||||
jar.add_original(cookie);
|
||||
}
|
||||
|
||||
jar
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone)]
|
||||
struct OptionStringCodec;
|
||||
|
||||
impl StringCodec<Option<String>> for OptionStringCodec {
|
||||
type Error = ();
|
||||
|
||||
fn encode(&self, val: &Option<String>) -> Result<String, Self::Error> {
|
||||
match val {
|
||||
Some(val) => Ok(format!("~<|Some|>~{val}")),
|
||||
None => Ok("~<|None|>~".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode(&self, str: String) -> Result<Option<String>, Self::Error> {
|
||||
Ok(str.strip_prefix("~<|Some|>~").map(|v| v.to_owned()))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue