From b1367a9f0b81d93d47105c29fef5ea0c59241dbc Mon Sep 17 00:00:00 2001 From: Maccesch Date: Tue, 13 Aug 2024 13:56:13 +0100 Subject: [PATCH 1/4] chore: clippy --- src/use_locale.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/use_locale.rs b/src/use_locale.rs index be781a4..ff69e7f 100644 --- a/src/use_locale.rs +++ b/src/use_locale.rs @@ -58,13 +58,13 @@ where .map(|l| l.as_ref().clone()) .collect::>(); - const EMPTY_ERR_MSG: &'static str = "Empty supported list. You have to provide at least one locale in the `supported` parameter"; - assert!(supported.len() > 0, "{}", EMPTY_ERR_MSG); + const EMPTY_ERR_MSG: & str = "Empty supported list. You have to provide at least one locale in the `supported` parameter"; + assert!(!supported.is_empty(), "{}", EMPTY_ERR_MSG); Signal::derive(move || { let supported = supported.clone(); - client_locales.with(|clienht_locales| { + client_locales.with(|client_locales| { let mut first_supported = None; for s in supported { @@ -72,7 +72,7 @@ where first_supported = Some(s.clone()); } - for client_locale in clienht_locales { + for client_locale in client_locales { let client_locale: LanguageIdentifier = client_locale .parse() .expect("Client should provide a list of valid unicode locales"); From 26a2dc0e4b9f267b2062215184ed3022883979a7 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Tue, 13 Aug 2024 16:19:19 +0100 Subject: [PATCH 2/4] added support for Sec-CH-Prefers-Color-Scheme header. Fixes #101 Relates to #121 --- CHANGELOG.md | 1 + Cargo.toml | 4 +-- examples/ssr/Cargo.toml | 3 +- examples/ssr/src/main.rs | 11 ++++++- src/use_color_mode.rs | 47 +++++++++++++++++++++++++---- src/use_locales.rs | 2 +- src/use_preferred_dark.rs | 62 +++++++++++++++++++++++++++++++++++++-- src/utils/header_macro.rs | 15 ++++++---- 8 files changed, 127 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e064c5..8cae4d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated to web_sys 0.3.70 which unfortunately is breaking some things. - `use_clipboard` doesn't need the unstable flags anymore. - `use_locale` now uses `unic_langid::LanguageIdentifier` and proper locale matching (thanks to @mondeja). +- `use_preferred_dark` and `use_color_mode` now try to read the `Sec-CH-Prefers-Color-Scheme` header in SSR. ## [0.11.4] - 2024-08-12 diff --git a/Cargo.toml b/Cargo.toml index cf8fd6e..2fd0ca0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,11 +34,11 @@ num = { version = "0.4", optional = true } paste = "1" thiserror = "1" unic-langid = "0.9" -wasm-bindgen = ">=0.2.93" +wasm-bindgen = "=0.2.93" wasm-bindgen-futures = "0.4" [dependencies.web-sys] -version = ">=0.3.70" +version = "=0.3.70" features = [ "AddEventListenerOptions", "BinaryType", diff --git a/examples/ssr/Cargo.toml b/examples/ssr/Cargo.toml index 5d71626..533ce90 100644 --- a/examples/ssr/Cargo.toml +++ b/examples/ssr/Cargo.toml @@ -21,8 +21,9 @@ log = "0.4" simple_logger = "4" tokio = { version = "1", features = ["full"], optional = true } tower = { version = "0.4", optional = true } +tower-default-headers = { git = "https://github.com/banool/tower-default-headers-rs" } tower-http = { version = "0.5", features = ["fs"], optional = true } -wasm-bindgen = "0.2.92" +wasm-bindgen = "=0.2.93" thiserror = "1.0.38" tracing = { version = "0.1.37", optional = true } http = "1" diff --git a/examples/ssr/src/main.rs b/examples/ssr/src/main.rs index 4d92392..ff4f4bb 100644 --- a/examples/ssr/src/main.rs +++ b/examples/ssr/src/main.rs @@ -2,11 +2,13 @@ #[tokio::main] async fn main() { use axum::{routing::post, Router}; + use http::{HeaderMap, HeaderName, HeaderValue}; use leptos::logging::log; use leptos::*; use leptos_axum::{generate_route_list, LeptosRoutes}; use leptos_use_ssr::app::*; use leptos_use_ssr::fileserv::file_and_error_handler; + use tower_default_headers::DefaultHeadersLayer; simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging"); @@ -20,12 +22,19 @@ async fn main() { let addr = leptos_options.site_addr; let routes = generate_route_list(|| view! { }); + let mut default_headers = HeaderMap::new(); + let color_header = HeaderValue::from_static("Sec-CH-Prefers-Color-Scheme"); + default_headers.insert(HeaderName::from_static("accept-ch"), color_header.clone()); + default_headers.insert(HeaderName::from_static("vary"), color_header.clone()); + default_headers.insert(HeaderName::from_static("critical-ch"), color_header); + // build our application with a route let app = Router::new() .route("/api/*fn_name", post(leptos_axum::handle_server_fns)) .leptos_routes(&leptos_options, routes, || view! { }) .fallback(file_and_error_handler) - .with_state(leptos_options); + .with_state(leptos_options) + .layer(DefaultHeadersLayer::new(default_headers)); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); log!("listening on http://{}", &addr); diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index 1df636b..99a688e 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -2,7 +2,11 @@ use crate::core::url; use crate::core::StorageType; use crate::core::{ElementMaybeSignal, MaybeRwSignal}; use crate::storage::{use_storage_with_options, UseStorageOptions}; -use crate::{sync_signal_with_options, use_cookie, use_preferred_dark, SyncSignalOptions}; +use crate::utils::get_header; +use crate::{ + sync_signal_with_options, use_cookie, use_preferred_dark_with_options, + SyncSignalOptions, UsePreferredDarkOptions, +}; use codee::string::FromToStringCodec; use default_struct_builder::DefaultBuilder; use leptos::*; @@ -83,7 +87,7 @@ use wasm_bindgen::JsCast; /// # } /// ``` /// -/// ### Cookies +/// ### Cookie /// /// To persist color mode in a cookie, use `use_cookie_with_options` and specify `.cookie_enabled(true)`. /// @@ -112,9 +116,24 @@ use wasm_bindgen::JsCast; /// /// ## Server-Side Rendering /// -/// On the server this will by default return `ColorMode::Light`. Persistence with storage is disabled. +/// On the server this will try to read the +/// [`Sec-CH-Prefers-Color-Scheme` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme) +/// to determine the color mode. If the header is not present it will return `ColorMode::Light`. +/// Please have a look at the linked documentation above for that header to see browser support +/// as well as potential server requirements. /// -/// If `cookie_enabled` is set to `true`, cookies will be used and if present this value will be used +/// > 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"`, for `spin` enable `"spin"`. +/// +/// ### Bring your own header +/// +/// In case you're neither using Axum, Actix nor Spin, or the default implementation is not to your liking, +/// you can provide your own way of reading the color scheme header value using the option +/// [`crate::UseColorModeOptions::ssr_color_header_getter`]. +/// +/// ### Cookie +/// +/// If `cookie_enabled` is set to `true`, a cookie will be used and if present this value will be used /// on the server as well as on the client. Please note that you have to add the `axum` or `actix` /// feature as described in [`fn@crate::use_cookie`]. /// @@ -151,6 +170,7 @@ where emit_auto, transition_enabled, listen_to_storage_changes, + ssr_color_header_getter, _marker, } = options; @@ -162,7 +182,9 @@ where ]) .collect(); - let preferred_dark = use_preferred_dark(); + let preferred_dark = use_preferred_dark_with_options(UsePreferredDarkOptions { + ssr_color_header_getter, + }); let system = Signal::derive(move || { if preferred_dark.get() { @@ -471,6 +493,14 @@ where /// Defaults to true. listen_to_storage_changes: bool, + /// Getter function to return the string value of the + /// [`Sec-CH-Prefers-Color-Scheme`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme) + /// header. + /// When you use one of the features `"axum"`, `"actix"` or `"spin"` there's a valid default + /// implementation provided. + #[allow(dead_code)] + ssr_color_header_getter: Rc Option>, + #[builder(skip)] _marker: PhantomData, } @@ -496,6 +526,13 @@ impl Default for UseColorModeOptions<&'static str, web_sys::Element> { emit_auto: false, transition_enabled: false, listen_to_storage_changes: true, + ssr_color_header_getter: Rc::new(move || { + get_header!( + HeaderName::from_static("sec-ch-prefers-color-scheme"), + use_locale, + ssr_color_header_getter + ) + }), _marker: PhantomData, } } diff --git a/src/use_locales.rs b/src/use_locales.rs index 4e4f567..8762dc5 100644 --- a/src/use_locales.rs +++ b/src/use_locales.rs @@ -39,7 +39,7 @@ use std::rc::Rc; /// ### Bring your own header /// /// In case you're neither using Axum, Actix nor Spin, or the default implementation is not to your liking, -/// you can provide your own way of reading and writing the cookie header value using the option +/// you can provide your own way of reading the language header value using the option /// [`crate::UseLocalesOptions::ssr_lang_header_getter`]. pub fn use_locales() -> Signal> { use_locales_with_options(UseLocalesOptions::default()) diff --git a/src/use_preferred_dark.rs b/src/use_preferred_dark.rs index 18d8043..ad64f13 100644 --- a/src/use_preferred_dark.rs +++ b/src/use_preferred_dark.rs @@ -1,5 +1,7 @@ -use crate::use_media_query; +use crate::utils::get_header; +use default_struct_builder::DefaultBuilder; use leptos::*; +use std::rc::Rc; /// Reactive [dark theme preference](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme). /// @@ -20,12 +22,66 @@ use leptos::*; /// /// ## Server-Side Rendering /// -/// On the server this functions returns a Signal that is always `false`. +/// On the server this will try to read the +/// [`Sec-CH-Prefers-Color-Scheme` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme) +/// to determine the color mode. If the header is not present it will return `ColorMode::Light`. +/// Please have a look at the linked documentation above for that header to see browser support +/// as well as potential server requirements. +/// +/// > 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"`, for `spin` enable `"spin"`. +/// +/// ### Bring your own header +/// +/// In case you're neither using Axum, Actix nor Spin, or the default implementation is not to your liking, +/// you can provide your own way of reading the color scheme header value using the option +/// [`crate::UsePreferredDarkOptions::ssr_color_header_getter`]. /// /// ## See also /// /// * [`fn@crate::use_media_query`] /// * [`fn@crate::use_preferred_contrast`] pub fn use_preferred_dark() -> Signal { - use_media_query("(prefers-color-scheme: dark)") + use_preferred_dark_with_options(Default::default()) +} + +/// Version of [`fn@crate::use_preferred_dark`] that accepts a `UsePreferredDarkOptions`. +pub fn use_preferred_dark_with_options(options: UsePreferredDarkOptions) -> Signal { + #[cfg(not(feature = "ssr"))] + { + let _ = options; + + crate::use_media_query("(prefers-color-scheme: dark)") + } + + #[cfg(feature = "ssr")] + { + Signal::derive(move || (options.ssr_color_header_getter)() == Some("dark".to_string())) + } +} + +/// Options for [`fn@crate::use_preferred_dark_with_options`]. +#[derive(DefaultBuilder)] +pub struct UsePreferredDarkOptions { + /// Getter function to return the string value of the + /// [`Sec-CH-Prefers-Color-Scheme`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme) + /// header. + /// When you use one of the features `"axum"`, `"actix"` or `"spin"` there's a valid default + /// implementation provided. + #[allow(dead_code)] + pub(crate) ssr_color_header_getter: Rc Option>, +} + +impl Default for UsePreferredDarkOptions { + fn default() -> Self { + Self { + ssr_color_header_getter: Rc::new(move || { + get_header!( + HeaderName::from_static("sec-ch-prefers-color-scheme"), + use_locale, + ssr_color_header_getter + ) + }), + } + } } diff --git a/src/utils/header_macro.rs b/src/utils/header_macro.rs index 2cb0b52..59dab10 100644 --- a/src/utils/header_macro.rs +++ b/src/utils/header_macro.rs @@ -1,6 +1,6 @@ macro_rules! get_header { ( - $header_name:ident, + $header_name:expr, $function_name:ident, $option_name:ident $(,)? @@ -19,14 +19,19 @@ macro_rules! get_header { ); return None; } - + #[cfg(feature = "actix")] - const $header_name: http0_2::HeaderName = http0_2::header::$header_name; + #[allow(unused_imports)] + use http0_2::{HeaderName, header::*}; #[cfg(any(feature = "axum", feature = "spin"))] - const $header_name: http1::HeaderName = http1::header::$header_name; + #[allow(unused_imports)] + use http1::{HeaderName, header::*}; #[cfg(any(feature = "axum", feature = "actix", feature = "spin"))] - crate::utils::header($header_name) + { + let header_name = $header_name; + crate::utils::header(header_name) + } } else { None } From 16a65b100d9adb8f568fcc785bbe9fa43e1ac8d8 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Tue, 13 Aug 2024 18:32:50 +0100 Subject: [PATCH 3/4] fixed the codecs chapter in the book to refer to crate codee --- docs/book/src/codecs.md | 130 ++++------------------------------------ 1 file changed, 13 insertions(+), 117 deletions(-) diff --git a/docs/book/src/codecs.md b/docs/book/src/codecs.md index 2e54fa2..45b2219 100644 --- a/docs/book/src/codecs.md +++ b/docs/book/src/codecs.md @@ -1,9 +1,9 @@ # Encoding and Decoding Data Several functions encode and decode data for storing it and/or sending it over the network. To do this, codecs -located at [`src/utils/codecs`](https://github.com/Synphonyte/leptos-use/tree/main/src/utils/codecs) are used. They -implement the traits [`Encoder`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/mod.rs#L9) with the -method `encode` and [`Decoder`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/mod.rs#L17) with the +from the crate [`codee`](https://docs.rs/codee/latest/codee/) are used. They +implement the traits [`Encoder`](https://docs.rs/codee/latest/codee/trait.Encoder.html) with the +method `encode` and [`Decoder`](https://docs.rs/codee/latest/codee/trait.Decoder.html) with the method `decode`. There are two types of codecs: One that encodes as binary data (`Vec[u8]`) and another type that encodes as @@ -11,26 +11,8 @@ strings (`String`). There is also an adapter [`Base64`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/base64.rs) that can be used to wrap a binary codec and make it a string codec by representing the binary data as a base64 string. -## Available Codecs - -### String Codecs - -- [**`FromToStringCodec` - **](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/from_to_string.rs) -- [**`JsonSerdeCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/json_serde.rs)** - -### Binary Codecs - -- [**`FromToBytesCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/binary/from_to_bytes.rs) -- [**`BincodeSerdeCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/binary/bincode_serde.rs) -- [**`MsgpackSerdeCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/binary/msgpack_serde.rs) - -### Adapters - -- [**`Base64`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/base64.rs) — - Wraps a binary codec and make it a string codec by representing the binary data as a base64 string. -- [**`OptionCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/option.rs) — - Wraps a string codec that encodes `T` to create a codec that encodes `Option`. +Please check the documentation of [`codee`](https://docs.rs/codee/latest/codee/) for more details and a list of all +available codecs. ## Example @@ -41,6 +23,7 @@ format. Since cookies can only store strings, we have to use string codecs here. # use leptos::*; # use leptos_use::use_cookie; # use serde::{Deserialize, Serialize}; +# use codee::string::JsonCodec; # #[component] # pub fn App(cx: Scope) -> impl IntoView { @@ -57,100 +40,13 @@ let (cookie, set_cookie) = use_cookie::("my-state-cookie"); ## Custom Codecs -If you don't find a suitable codecs for your needs, you can implement your own; it's straightforward! If you want to -create a string codec, you can look -at [`JsonSerdeCodec`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/json_serde.rs). -In case it's a binary codec, have a look -at [`BincodeSerdeCodec`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/binary/bincode_serde.rs). +If you don't find a suitable codec for your needs, you can implement your own; it's straightforward! +If you want to create a string codec, you can look at +[`JsonSerdeCodec`](https://docs.rs/codee/latest/src/codee/string/json_serde.rs.html). +In case it's a binary codec, have a look at +[`BincodeSerdeCodec`](https://docs.rs/codee/latest/src/codee/binary/bincode_serde.rs.html). ## Versioning -Versioning is the process of handling long-term data that can outlive our code. - -For example, we could have a settings struct whose members change over time. We might eventually -add timezone support, and we might then remove support for a thousands separator for numbers. -Each change results in a new possible version of the stored data. If we stored these settings -in browser storage, we would need to handle all possible versions of the data format that can -occur. If we don't offer versioning, then all settings could revert to the default every time we -encounter an old format. - -How best to handle versioning depends on the codec involved: - -- The `FromToStringCodec` can avoid versioning entirely by keeping - to primitive types. In our example above, we could have decomposed the settings struct into - separate timezone and number separator fields. These would be encoded as strings and stored as - two separate key-value fields in the browser rather than a single field. If a field is missing, - then the value intentionally would fall back to the default without interfering with the other - field. - -- The `ProstCodec` uses [Protocol buffers](https://protobuf.dev/overview/) - designed to solve the problem of long-term storage. It provides semantics for versioning that - are not present in JSON or other formats. - -- The codecs that use serde under the hood can rely on serde or by - providing their own manual version handling. See the next sections for more details. - -### Rely on `serde` - -A simple way to avoid complex versioning is to rely on serde's [field attributes](https://serde.rs/field-attrs.html) -such as [`serde(default)`](https://serde.rs/field-attrs.html#default) -and [`serde(rename = "...")`](https://serde.rs/field-attrs.html#rename). - -### Manual Version Handling - -We look at the example of the `JsonSerdeCodec` in this section. - -To implement version handling, we parse the JSON generically then transform the -resulting `JsValue` before decoding it into our struct again. - -Let's look at an example. - - ```rust,noplayground - # use leptos::*; - # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions}; - # use serde::{Deserialize, Serialize}; - # use serde_json::json; - # use leptos_use::utils::{Encoder, Decoder}; - # - # pub fn Demo() -> impl IntoView { - #[derive(Serialize, Deserialize, Clone, Default, PartialEq)] - pub struct MyState { - pub hello: String, - // This field was added in a later version - pub greeting: String, - } - - pub struct MyStateCodec; - - impl Encoder for MyStateCodec { - type Error = serde_json::Error; - type Encoded = String; - - fn encode(val: &MyState) -> Result { - serde_json::to_string(val) - } - } - - impl Decoder for MyStateCodec { - type Error = serde_json::Error; - type Encoded = str; - - fn decode(stored_value: &Self::Encoded) -> Result { - let mut val: serde_json::Value = serde_json::from_str(stored_value)?; - // add "greeting": "Hello" to the object if it's missing - if let Some(obj) = val.as_object_mut() { - if !obj.contains_key("greeting") { - obj.insert("greeting".to_string(), json!("Hello")); - } - serde_json::from_value(val) - } else { - Ok(MyState::default()) - } - } - } - - // Then use it like the following just as any other codec. - let (get, set, remove) = use_local_storage::("my-struct-key"); - # view! { } - # } - ``` +For a discussion on how to implement versioning please refer to the +[relevant section in the docs for `codee`](https://docs.rs/codee/latest/codee/index.html#versioning). \ No newline at end of file From d2e968249b370f3fc8e3d4ae2a09f401390c596c Mon Sep 17 00:00:00 2001 From: Maccesch Date: Tue, 13 Aug 2024 18:36:18 +0100 Subject: [PATCH 4/4] updated changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cae4d0..fa0ca13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `use_locale` now uses `unic_langid::LanguageIdentifier` and proper locale matching (thanks to @mondeja). - `use_preferred_dark` and `use_color_mode` now try to read the `Sec-CH-Prefers-Color-Scheme` header in SSR. +### Fixes 🍕 + +- Fixed the codec chapter in the book to refer to crate `codee`. + ## [0.11.4] - 2024-08-12 ### New Features 🚀