refactored codecs and implemented msgpack and bincode as binary codecs. prost is now a binary codec as well and base64 is available as an adapter.

This commit is contained in:
Maccesch 2024-06-07 02:19:07 +02:00
parent ae019b4734
commit a8de6a96dd
24 changed files with 826 additions and 504 deletions

View file

@ -32,11 +32,11 @@ jobs:
- name: Clippy
run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings
- name: Run tests (general)
run: cargo test --features math,docs,ssr,prost,serde
run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde
- name: Run tests (axum)
run: cargo test --features math,docs,ssr,prost,serde,axum --doc use_cookie::use_cookie
run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,axum --doc use_cookie::use_cookie
- name: Run tests (actix)
run: cargo test --features math,docs,ssr,prost,serde,actix --doc use_cookie::use_cookie
run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,actix --doc use_cookie::use_cookie
#### mdbook
- name: Install mdbook I

View file

@ -25,8 +25,8 @@ jobs:
- name: Clippy
run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings
- name: Run tests (general)
run: cargo test --features math,docs,ssr,prost,serde
run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde
- name: Run tests (axum)
run: cargo test --features math,docs,ssr,prost,serde,axum --doc use_cookie::use_cookie
run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,axum --doc use_cookie::use_cookie
- name: Run tests (actix)
run: cargo test --features math,docs,ssr,prost,serde,actix --doc use_cookie::use_cookie
run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,actix --doc use_cookie::use_cookie

View file

@ -11,13 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `UseStorageOptions` now has `delay_during_hydration` which has to be used when you conditionally show parts of
the DOM controlled by a value from storage. This leads to hydration errors which can be fixed by setting this new
option to `true`.
- `cookie::SameSite` is no re-exported
- Fixed typo in compiler error messages in `use_cookie`.
- `cookie::SameSite` is now re-exported
- Fixed typo in compiler error messages in `use_cookie` (thanks to @SleeplessOne1917).
### Breaking Changes 🛠
- `UseStorageOptions` no longer accepts a `codec` value because this is already provided as a generic parameter to
the respective function calls.
- `UseWebsocketOptions::reconnect_limit` is now `ReconnectLimit` instead of `u64`. Use `ReconnectLimit::Infinite` for
infinite retries or `ReconnectLimit::Limited(...)` for limited retries.
- `StringCodec::decode` now takes a `&str` instead of a `String`.
## [0.10.10] - 2024-05-10
@ -133,7 +136,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- You can now convert `leptos::html::HtmlElement<T>` into `Element(s)MaybeSignal`. This should make functions a lot
easier to use in directives.
- There's now a chapter in the book especially for `Element(s)MaybeSignal`.
- Throttled or debounced callbacks (in watch\__ or _\_fn) no longer are called after the containing scope was cleaned up.
- Throttled or debounced callbacks (in watch\__ or _\_fn) no longer are called after the containing scope was cleaned
up.
- The document returned from `use_document` now supports the methods `query_selector` and `query_selector_all`.
## [0.9.0] - 2023-12-06

View file

@ -17,6 +17,7 @@ actix-web = { version = "4", optional = true, default-features = false }
async-trait = "0.1"
base64 = { version = "0.21", optional = true }
cfg-if = "1"
bincode = { version = "1", optional = true }
cookie = { version = "0.18", features = ["percent-encode"] }
default-struct-builder = "0.5"
futures-util = "0.3"
@ -141,12 +142,13 @@ actix = ["dep:actix-web", "dep:leptos_actix", "dep:http0_2"]
axum = ["dep:leptos_axum", "dep:http1"]
docs = []
math = ["num"]
prost = ["base64", "dep:prost"]
serde = ["dep:serde", "serde_json"]
prost = ["dep:prost"]
json_serde = ["dep:serde_json", "dep:serde"]
spin = ["dep:leptos-spin", "dep:http1"]
ssr = []
msgpack = ["dep:rmp-serde", "dep:serde"]
msgpack_serde = ["dep:rmp-serde", "dep:serde"]
bincode_serde = ["dep:bincode", "dep:serde"]
wasm_ssr = []
[package.metadata.docs.rs]
features = ["math", "docs", "ssr", "prost", "serde"]
features = ["math", "docs", "ssr", "prost", "json_serde", "msgpack_serde", "bincode_serde"]

156
docs/book/src/codecs.md Normal file
View file

@ -0,0 +1,156 @@
# 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
method `decode`.
There are two types of codecs: One that encodes as binary data (`Vec[u8]`) and another type that encodes as
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<T>`.
## Example
In this example, a codec is given to [`use_cookie`](browser/use_cookie.md) that stores data as a string in the JSON
format. Since cookies can only store strings, we have to use string codecs here.
```rust,noplayground
# use leptos::*;
# use leptos_use::use_cookie;
# use serde::{Deserialize, Serialize};
# #[component]
# pub fn App(cx: Scope) -> impl IntoView {
#[derive(Serialize, Deserialize, Clone)]
struct MyState {
chicken_count: i32,
egg_count: i32,
}
let (cookie, set_cookie) = use_cookie::<MyState, JsonCodec>("my-state-cookie");
# view! {}
# }
```
## 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).
## 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<MyState> for MyStateCodec {
type Error = serde_json::Error;
type Encoded = String;
fn encode(val: &MyState) -> Result<Self::Encoded, Self::Error> {
serde_json::to_string(val)
}
}
impl Decoder<MyState> for MyStateCodec {
type Error = serde_json::Error;
type Encoded = str;
fn decode(stored_value: &Self::Encoded) -> Result<MyState, Self::Error> {
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::<MyState, MyStateCodec>("my-struct-key");
# view! { }
# }
```

View file

@ -1,5 +1,5 @@
use super::{use_storage_with_options, StorageType, UseStorageOptions};
use crate::utils::StringCodec;
use crate::utils::{Decoder, Encoder};
use leptos::signal_prelude::*;
/// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
@ -15,23 +15,23 @@ pub fn use_local_storage<T, C>(
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
where
T: Clone + Default + PartialEq,
C: StringCodec<T> + Default,
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
{
use_storage_with_options::<T, C>(
StorageType::Local,
key,
UseStorageOptions::<T, C::Error>::default(),
UseStorageOptions::<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>::default(),
)
}
/// Accepts [`UseStorageOptions`]. See [`use_local_storage`] for details.
pub fn use_local_storage_with_options<T, C>(
key: impl AsRef<str>,
options: UseStorageOptions<T, C::Error>,
options: UseStorageOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
where
T: Clone + PartialEq,
C: StringCodec<T> + Default,
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
{
use_storage_with_options::<T, C>(StorageType::Local, key, options)
}

View file

@ -1,5 +1,5 @@
use super::{use_storage_with_options, StorageType, UseStorageOptions};
use crate::utils::StringCodec;
use crate::utils::{Decoder, Encoder};
use leptos::signal_prelude::*;
/// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage).
@ -15,23 +15,23 @@ pub fn use_session_storage<T, C>(
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
where
T: Clone + Default + PartialEq,
C: StringCodec<T> + Default,
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
{
use_storage_with_options::<T, C>(
StorageType::Session,
key,
UseStorageOptions::<T, C::Error>::default(),
UseStorageOptions::<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>::default(),
)
}
/// Accepts [`UseStorageOptions`]. See [`use_session_storage`] for details.
pub fn use_session_storage_with_options<T, C>(
key: impl AsRef<str>,
options: UseStorageOptions<T, C::Error>,
options: UseStorageOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
where
T: Clone + PartialEq,
C: StringCodec<T> + Default,
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
{
use_storage_with_options::<T, C>(StorageType::Session, key, options)
}

View file

@ -1,9 +1,9 @@
use crate::utils::{CodecError, Decoder, Encoder};
use crate::{
core::{MaybeRwSignal, StorageType},
utils::{FilterOptions, StringCodec},
utils::FilterOptions,
};
use default_struct_builder::DefaultBuilder;
use leptos::leptos_dom::HydrationCtx;
use leptos::*;
use std::rc::Rc;
use thiserror::Error;
@ -25,12 +25,13 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
/// The specified key is where data is stored. All values are stored as UTF-16 strings which
/// is then encoded and decoded via the given [`Codec`]. This value is synced with other calls using
/// the same key on the smae page and across tabs for local storage.
/// See [`UseStorageOptions`] to see how behaviour can be further customised.
/// See [`UseStorageOptions`] to see how behavior can be further customised.
///
/// See [`StringCodec`] for more details on how to handle versioning — dealing with data that can outlast your code.
/// Values are (en)decoded via the given codec. You can use any of the string codecs or a
/// binary codec wrapped in [`Base64`].
///
/// > To use the [`JsonCodec`], you will need to add the `"serde"` feature to your project's `Cargo.toml`.
/// > To use [`ProstCodec`], add the feature `"prost"`.
/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
/// available and what feature flags they require.
///
/// ## Example
///
@ -38,12 +39,12 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
/// # use leptos::*;
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage};
/// # use serde::{Deserialize, Serialize};
/// # use leptos_use::utils::{FromToStringCodec, JsonCodec, ProstCodec};
/// # use leptos_use::utils::{FromToStringCodec, JsonSerdeCodec, ProstCodec, Base64};
/// #
/// # #[component]
/// # pub fn Demo() -> impl IntoView {
/// // Binds a struct:
/// let (state, set_state, _) = use_local_storage::<MyState, JsonCodec>("my-state");
/// let (state, set_state, _) = use_local_storage::<MyState, JsonSerdeCodec>("my-state");
///
/// // Binds a bool, stored as a string:
/// let (flag, set_flag, remove_flag) = use_session_storage::<bool, FromToStringCodec>("my-flag");
@ -51,10 +52,10 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
/// // Binds a number, stored as a string:
/// let (count, set_count, _) = use_session_storage::<i32, FromToStringCodec>("my-count");
/// // Binds a number, stored in JSON:
/// let (count, set_count, _) = use_session_storage::<i32, JsonCodec>("my-count-kept-in-js");
/// let (count, set_count, _) = use_session_storage::<i32, JsonSerdeCodec>("my-count-kept-in-js");
///
/// // Bind string with SessionStorage stored in ProtoBuf format:
/// let (id, set_id, _) = use_storage::<String, ProstCodec>(
/// let (id, set_id, _) = use_storage::<String, Base64<ProstCodec>>(
/// StorageType::Session,
/// "my-id",
/// );
@ -81,10 +82,6 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
/// }
/// ```
///
/// ## Create Your Own Custom Codec
///
/// All you need to do is to implement the [`StringCodec`] trait together with `Default` and `Clone`.
///
/// ## Server-Side Rendering
///
/// On the server the returned signals will just read/manipulate the `initial_value` without persistence.
@ -96,7 +93,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
///
/// ```
/// # use leptos::*;
/// # use leptos_use::storage::use_local_storage;
/// # use leptos_use::storage::use_session_storage;
/// # use leptos_use::utils::FromToStringCodec;
/// #
/// # #[component]
@ -104,7 +101,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
/// let (flag, set_flag, _) = use_session_storage::<bool, FromToStringCodec>("my-flag");
///
/// view! {
/// <Show when=move || flag()>
/// <Show when=move || flag.get()>
/// <div>Some conditional content</div>
/// </Show>
/// }
@ -134,13 +131,13 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
/// #
/// # #[component]
/// # pub fn Example() -> impl IntoView {
/// let (flag, set_flag, _) = use_session_storage_with_options::<bool, FromToStringCodec>(
/// let (flag, set_flag, _) = use_local_storage_with_options::<bool, FromToStringCodec>(
/// "my-flag",
/// UseStorageOptions::default().delay_during_hydration(true),
/// );
///
/// view! {
/// <Show when=move || flag()>
/// <Show when=move || flag.get()>
/// <div>Some conditional content</div>
/// </Show>
/// }
@ -153,7 +150,7 @@ pub fn use_storage<T, C>(
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
where
T: Default + Clone + PartialEq,
C: StringCodec<T> + Default,
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
{
use_storage_with_options::<T, C>(storage_type, key, UseStorageOptions::default())
}
@ -162,11 +159,11 @@ where
pub fn use_storage_with_options<T, C>(
storage_type: StorageType,
key: impl AsRef<str>,
options: UseStorageOptions<T, C::Error>,
options: UseStorageOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
where
T: Clone + PartialEq,
C: StringCodec<T> + Default,
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
{
let UseStorageOptions {
on_error,
@ -176,17 +173,15 @@ where
delay_during_hydration,
} = options;
let codec = C::default();
let (data, set_data) = initial_value.into_signal();
let default = data.get_untracked();
#[cfg(feature = "ssr")]
{
let _ = codec;
let _ = on_error;
let _ = listen_to_storage_changes;
let _ = filter;
let _ = delay_during_hydration;
let _ = storage_type;
let _ = key;
let _ = INTERNAL_STORAGE_EVENT;
@ -278,7 +273,7 @@ where
};
// Fetch initial value
if delay_during_hydration && HydrationCtx::is_hydrating() {
if delay_during_hydration && leptos::leptos_dom::HydrationCtx::is_hydrating() {
request_animation_frame(fetch_from_storage.clone());
} else {
fetch_from_storage();
@ -381,7 +376,7 @@ where
/// Session handling errors returned by [`use_storage_with_options`].
#[derive(Error, Debug)]
pub enum UseStorageError<Err> {
pub enum UseStorageError<E, D> {
#[error("storage not available")]
StorageNotAvailable(JsValue),
#[error("storage not returned from window")]
@ -395,18 +390,18 @@ pub enum UseStorageError<Err> {
#[error("failed to notify item changed")]
NotifyItemChangedFailed(JsValue),
#[error("failed to encode / decode item value")]
ItemCodecError(Err),
ItemCodecError(CodecError<E, D>),
}
/// Options for use with [`use_local_storage_with_options`], [`use_session_storage_with_options`] and [`use_storage_with_options`].
#[derive(DefaultBuilder)]
pub struct UseStorageOptions<T, Err>
pub struct UseStorageOptions<T, E, D>
where
T: 'static,
{
// Callback for when an error occurs
#[builder(skip)]
on_error: Rc<dyn Fn(UseStorageError<Err>)>,
on_error: Rc<dyn Fn(UseStorageError<E, D>)>,
// Whether to continuously listen to changes from browser storage
listen_to_storage_changes: bool,
// Initial value to use when the storage key is not set
@ -430,7 +425,7 @@ fn handle_error<T, Err>(
result.map_err(|err| (on_error)(err))
}
impl<T: Default, Err> Default for UseStorageOptions<T, Err> {
impl<T: Default, E, D> Default for UseStorageOptions<T, E, D> {
fn default() -> Self {
Self {
on_error: Rc::new(|_err| ()),
@ -442,9 +437,9 @@ impl<T: Default, Err> Default for UseStorageOptions<T, Err> {
}
}
impl<T: Default, Err> UseStorageOptions<T, Err> {
impl<T: Default, E, D> UseStorageOptions<T, E, D> {
/// Optional callback whenever an error occurs.
pub fn on_error(self, on_error: impl Fn(UseStorageError<Err>) + 'static) -> Self {
pub fn on_error(self, on_error: impl Fn(UseStorageError<E, D>) + 'static) -> Self {
Self {
on_error: Rc::new(on_error),
..self

View file

@ -1,4 +1,4 @@
use crate::utils::StringCodec;
use crate::utils::{CodecError, Decoder, Encoder};
use crate::{
js, use_event_listener, use_event_listener_with_options, use_supported, UseEventListenerOptions,
};
@ -44,13 +44,17 @@ use wasm_bindgen::JsValue;
/// # }
/// ```
///
/// Just like with [`use_storage`] you can use different codecs for encoding and decoding.
/// Values are (en)decoded via the given codec. You can use any of the string codecs or a
/// binary codec wrapped in [`Base64`].
///
/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
/// available and what feature flags they require.
///
/// ```
/// # use leptos::*;
/// # use serde::{Deserialize, Serialize};
/// # use leptos_use::use_broadcast_channel;
/// # use leptos_use::utils::JsonCodec;
/// # use leptos_use::utils::JsonSerdeCodec;
/// #
/// // Data sent in JSON must implement Serialize, Deserialize:
/// #[derive(Serialize, Deserialize, Clone, PartialEq)]
@ -61,35 +65,35 @@ use wasm_bindgen::JsValue;
///
/// # #[component]
/// # fn Demo() -> impl IntoView {
/// use_broadcast_channel::<MyState, JsonCodec>("everyting-is-awesome");
/// use_broadcast_channel::<MyState, JsonSerdeCodec>("everyting-is-awesome");
/// # 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>
) -> UseBroadcastChannelReturn<
T,
impl Fn(&T) + Clone,
impl Fn() + Clone,
<C as Encoder<T>>::Error,
<C as Decoder<T>>::Error,
>
where
C: StringCodec<T> + Default + Clone,
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
{
let is_supported = use_supported(|| js!("BroadcastChannel" in &window()));
let (is_closed, set_closed) = create_signal(false);
let (channel, set_channel) = create_signal(None::<web_sys::BroadcastChannel>);
let (message, set_message) = create_signal(None::<T>);
let (error, set_error) = create_signal(None::<UseBroadcastChannelError<C::Error>>);
let codec = C::default();
let (error, set_error) = create_signal(
None::<UseBroadcastChannelError<<C as Encoder<T>>::Error, <C as Decoder<T>>::Error>>,
);
let post = {
let codec = codec.clone();
move |data: &T| {
if let Some(channel) = channel.get_untracked() {
match codec.encode(data) {
match C::encode(data) {
Ok(msg) => {
channel
.post_message(&msg.into())
@ -99,7 +103,9 @@ where
.ok();
}
Err(err) => {
set_error.set(Some(UseBroadcastChannelError::Encode(err)));
set_error.set(Some(UseBroadcastChannelError::Codec(CodecError::Encode(
err,
))));
}
}
}
@ -125,11 +131,13 @@ where
ev::message,
move |event| {
if let Some(data) = event.data().as_string() {
match codec.decode(data) {
match C::decode(&data) {
Ok(msg) => {
set_message.set(Some(msg));
}
Err(err) => set_error.set(Some(UseBroadcastChannelError::Decode(err))),
Err(err) => set_error.set(Some(UseBroadcastChannelError::Codec(
CodecError::Decode(err),
))),
}
} else {
set_error.set(Some(UseBroadcastChannelError::ValueNotString));
@ -167,12 +175,13 @@ where
}
/// Return type of [`use_broadcast_channel`].
pub struct UseBroadcastChannelReturn<T, PFn, CFn, Err>
pub struct UseBroadcastChannelReturn<T, PFn, CFn, E, D>
where
T: 'static,
PFn: Fn(&T) + Clone,
CFn: Fn() + Clone,
Err: 'static,
E: 'static,
D: 'static,
{
/// `true` if this browser supports `BroadcastChannel`s.
pub is_supported: Signal<bool>,
@ -190,22 +199,20 @@ where
pub close: CFn,
/// Latest error as reported by the `messageerror` event.
pub error: Signal<Option<UseBroadcastChannelError<Err>>>,
pub error: Signal<Option<UseBroadcastChannelError<E, D>>>,
/// Wether the channel is closed
pub is_closed: Signal<bool>,
}
#[derive(Debug, Error, Clone)]
pub enum UseBroadcastChannelError<Err> {
#[derive(Debug, Error)]
pub enum UseBroadcastChannelError<E, D> {
#[error("failed to post message")]
PostMessage(JsValue),
#[error("channel message error")]
MessageEvent(web_sys::MessageEvent),
#[error("failed to encode value")]
Encode(Err),
#[error("failed to decode value")]
Decode(Err),
#[error("failed to (de)encode value")]
Codec(CodecError<E, D>),
#[error("received value is not a string")]
ValueNotString,
}

View file

@ -1,7 +1,7 @@
#![allow(clippy::too_many_arguments)]
use crate::core::now;
use crate::utils::StringCodec;
use crate::utils::{CodecError, Decoder, Encoder};
use cookie::time::{Duration, OffsetDateTime};
pub use cookie::SameSite;
use cookie::{Cookie, CookieJar};
@ -56,7 +56,11 @@ use std::rc::Rc;
/// # }
/// ```
///
/// See [`StringCodec`] for details on how to handle versioning — dealing with data that can outlast your code.
/// Values are (en)decoded via the given codec. You can use any of the string codecs or a
/// binary codec wrapped in [`Base64`].
///
/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
/// available and what feature flags they require.
///
/// ## Cookie attributes
///
@ -101,7 +105,7 @@ use std::rc::Rc;
/// # use leptos::*;
/// # use serde::{Deserialize, Serialize};
/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
/// # use leptos_use::utils::JsonCodec;
/// # use leptos_use::utils::JsonSerdeCodec;
/// #
/// # #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
/// # pub struct Auth {
@ -111,7 +115,7 @@ use std::rc::Rc;
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
/// use_cookie_with_options::<Auth, JsonCodec>(
/// use_cookie_with_options::<Auth, JsonSerdeCodec>(
/// "auth",
/// UseCookieOptions::default()
/// .ssr_cookies_header_getter(|| {
@ -136,7 +140,7 @@ use std::rc::Rc;
/// 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,
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
T: Clone,
{
use_cookie_with_options::<T, C>(cookie_name, UseCookieOptions::default())
@ -145,10 +149,10 @@ where
/// Version of [`use_cookie`] that takes [`UseCookieOptions`].
pub fn use_cookie_with_options<T, C>(
cookie_name: &str,
options: UseCookieOptions<T, C::Error>,
options: UseCookieOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
where
C: StringCodec<T> + Default + Clone,
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
T: Clone,
{
let UseCookieOptions {
@ -181,7 +185,6 @@ where
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);
@ -193,9 +196,8 @@ where
set_cookie.set(
jar.get(cookie_name)
.and_then(|c| {
codec
.decode(c.value().to_string())
.map_err(|err| on_error(err))
C::decode(c.value())
.map_err(|err| on_error(CodecError::Decode(err)))
.ok()
})
.or(default_value),
@ -213,19 +215,19 @@ where
#[cfg(not(feature = "ssr"))]
{
use crate::utils::{FromToStringCodec, OptionCodec};
use crate::{
use_broadcast_channel, watch_pausable, UseBroadcastChannelReturn, WatchPausableReturn,
};
let UseBroadcastChannelReturn { message, post, .. } =
use_broadcast_channel::<Option<String>, OptionStringCodec>(&format!(
use_broadcast_channel::<Option<String>, OptionCodec<FromToStringCodec>>(&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();
@ -238,7 +240,7 @@ where
let value = cookie.with_untracked(|cookie| {
cookie
.as_ref()
.and_then(|cookie| codec.encode(cookie).map_err(|err| on_error(err)).ok())
.and_then(|cookie| C::encode(cookie).map_err(|err| on_error(err)).ok())
});
if value
@ -290,7 +292,7 @@ where
pause();
if let Some(message) = message {
match codec.decode(message.clone()) {
match C::decode(&message) {
Ok(value) => {
let ssr_cookies_header_getter =
Rc::clone(&ssr_cookies_header_getter);
@ -359,9 +361,11 @@ where
if !readonly {
let value = cookie
.with_untracked(|cookie| {
cookie
.as_ref()
.map(|cookie| codec.encode(&cookie).map_err(|err| on_error(err)).ok())
cookie.as_ref().map(|cookie| {
C::encode(&cookie)
.map_err(|err| on_error(CodecError::Encode(err)))
.ok()
})
})
.flatten();
jar.update_value(|jar| {
@ -387,7 +391,7 @@ where
/// Options for [`use_cookie_with_options`].
#[derive(DefaultBuilder)]
pub struct UseCookieOptions<T, Err> {
pub struct UseCookieOptions<T, E, D> {
/// [`Max-Age` of the cookie](https://tools.ietf.org/html/rfc6265#section-5.2.2) in milliseconds. The returned signal will turn to `None` after the max age is reached.
/// Default: `None`
///
@ -464,10 +468,10 @@ pub struct UseCookieOptions<T, Err> {
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)>,
on_error: Rc<dyn Fn(CodecError<E, D>)>,
}
impl<T, Err> Default for UseCookieOptions<T, Err> {
impl<T, E, D> Default for UseCookieOptions<T, E, D> {
#[allow(dead_code)]
fn default() -> Self {
Self {
@ -879,21 +883,3 @@ fn load_and_parse_cookie_jar(
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()))
}
}

View file

@ -1,6 +1,6 @@
use crate::core::ConnectionReadyState;
use crate::utils::StringCodec;
use crate::{js, use_event_listener};
use crate::utils::Decoder;
use crate::{js, use_event_listener, ReconnectLimit};
use default_struct_builder::DefaultBuilder;
use leptos::*;
use std::cell::Cell;
@ -18,14 +18,16 @@ use thiserror::Error;
///
/// ## Usage
///
/// Values are decoded via the given [`Codec`].
/// Values are decoded via the given decoder. You can use any of the string codecs or a
/// binary codec wrapped in [`Base64`].
///
/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
/// available and what feature flags they require.
///
/// > To use the [`JsonCodec`], you will need to add the `"serde"` feature to your project's `Cargo.toml`.
/// > To use [`ProstCodec`], add the feature `"prost"`.
///
/// ```
/// # use leptos::*;
/// # use leptos_use::{use_event_source, UseEventSourceReturn, utils::JsonCodec};
/// # use leptos_use::{use_event_source, UseEventSourceReturn, utils::JsonSerdeCodec};
/// # use serde::{Deserialize, Serialize};
/// #
/// #[derive(Serialize, Deserialize, Clone, PartialEq)]
@ -38,7 +40,7 @@ use thiserror::Error;
/// # fn Demo() -> impl IntoView {
/// let UseEventSourceReturn {
/// ready_state, data, error, close, ..
/// } = use_event_source::<EventSourceData, JsonCodec>("https://event-source-url");
/// } = use_event_source::<EventSourceData, JsonSerdeCodec>("https://event-source-url");
/// #
/// # view! { }
/// # }
@ -85,7 +87,7 @@ use thiserror::Error;
///
/// ```
/// # use leptos::*;
/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions, utils::FromToStringCodec};
/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions, utils::FromToStringCodec, ReconnectLimit};
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
@ -94,7 +96,7 @@ use thiserror::Error;
/// } = use_event_source_with_options::<bool, FromToStringCodec>(
/// "https://event-source-url",
/// UseEventSourceOptions::default()
/// .reconnect_limit(5) // at most 5 attempts
/// .reconnect_limit(ReconnectLimit::Limited(5)) // at most 5 attempts
/// .reconnect_interval(2000) // wait for 2 seconds between attempts
/// );
/// #
@ -113,22 +115,21 @@ pub fn use_event_source<T, C>(
) -> UseEventSourceReturn<T, C::Error, impl Fn() + Clone + 'static, impl Fn() + Clone + 'static>
where
T: Clone + PartialEq + 'static,
C: StringCodec<T> + Default,
C: Decoder<T, Encoded = str>,
{
use_event_source_with_options(url, UseEventSourceOptions::<T, C>::default())
use_event_source_with_options::<T, C>(url, UseEventSourceOptions::<T>::default())
}
/// Version of [`use_event_source`] that takes a `UseEventSourceOptions`. See [`use_event_source`] for how to use.
pub fn use_event_source_with_options<T, C>(
url: &str,
options: UseEventSourceOptions<T, C>,
options: UseEventSourceOptions<T>,
) -> UseEventSourceReturn<T, C::Error, impl Fn() + Clone + 'static, impl Fn() + Clone + 'static>
where
T: Clone + PartialEq + 'static,
C: StringCodec<T> + Default,
C: Decoder<T, Encoded = str>,
{
let UseEventSourceOptions {
codec,
reconnect_limit,
reconnect_interval,
on_failed,
@ -151,7 +152,7 @@ where
let set_data_from_string = move |data_string: Option<String>| {
if let Some(data_string) = data_string {
match codec.decode(data_string) {
match C::decode(&data_string) {
Ok(data) => set_data.set(Some(data)),
Err(err) => set_error.set(Some(UseEventSourceError::Deserialize(err))),
}
@ -213,12 +214,15 @@ where
// only reconnect if EventSource isn't reconnecting by itself
// this is the case when the connection is closed (readyState is 2)
if es.ready_state() == 2 && !explicitly_closed.get() && reconnect_limit > 0 {
if es.ready_state() == 2
&& !explicitly_closed.get()
&& matches!(reconnect_limit, ReconnectLimit::Limited(_))
{
es.close();
retried.set(retried.get() + 1);
if retried.get() < reconnect_limit {
if reconnect_limit.is_exceeded_by(retried.get()) {
set_timeout(
move || {
if let Some(init) = init.get_value() {
@ -312,16 +316,13 @@ where
/// Options for [`use_event_source_with_options`].
#[derive(DefaultBuilder)]
pub struct UseEventSourceOptions<T, C>
pub struct UseEventSourceOptions<T>
where
T: 'static,
C: StringCodec<T>,
{
/// Decodes from the received String to a value of type `T`.
codec: C,
/// Retry times. Defaults to 3.
reconnect_limit: u64,
/// Retry times. Defaults to `ReconnectLimit::Limited(3)`. Use `ReconnectLimit::Infinite` for
/// infinite retries.
reconnect_limit: ReconnectLimit,
/// Retry interval in ms. Defaults to 3000.
reconnect_interval: u64,
@ -344,11 +345,10 @@ where
_marker: PhantomData<T>,
}
impl<T, C: StringCodec<T> + Default> Default for UseEventSourceOptions<T, C> {
impl<T> Default for UseEventSourceOptions<T> {
fn default() -> Self {
Self {
codec: C::default(),
reconnect_limit: 3,
reconnect_limit: ReconnectLimit::default(),
reconnect_interval: 3000,
on_failed: Rc::new(|| {}),
immediate: true,

View file

@ -0,0 +1,46 @@
use crate::utils::{Decoder, Encoder};
use serde::{Deserialize, Serialize};
/// A codec that relies on `bincode` adn `serde` to encode data in the bincode format.
///
/// This is only available with the **`bincode` feature** enabled.
pub struct BincodeSerdeCodec;
impl<T: serde::Serialize> Encoder<T> for BincodeSerdeCodec {
type Error = bincode::Error;
type Encoded = Vec<u8>;
fn encode(val: &T) -> Result<Self::Encoded, Self::Error> {
bincode::serialize(val)
}
}
impl<T: serde::de::DeserializeOwned> Decoder<T> for BincodeSerdeCodec {
type Error = bincode::Error;
type Encoded = [u8];
fn decode(val: &Self::Encoded) -> Result<T, Self::Error> {
bincode::deserialize(val)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bincode_codec() {
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct Test {
s: String,
i: i32,
}
let t = Test {
s: String::from("party time 🎉"),
i: 42,
};
let enc = BincodeSerdeCodec::encode(&t).unwrap();
let dec: Test = BincodeSerdeCodec::decode(&enc).unwrap();
assert_eq!(dec, t);
}
}

View file

@ -1,7 +1,10 @@
use super::BinCodec;
use crate::utils::{Decoder, Encoder};
use thiserror::Error;
#[derive(Copy, Clone, Default, PartialEq)]
/// A binary codec that uses rust own binary encoding functions to encode and decode data.
/// This can be used if you want to encode only primitives and don't want to rely on third party
/// crates like `bincode` or `rmp-serde`. If you have more complex data check out
/// [`BincodeSerdeCodec`] or [`MsgpackSerdeCodec`].
pub struct FromToBytesCodec;
#[derive(Error, Debug)]
@ -15,14 +18,20 @@ pub enum FromToBytesCodecError {
macro_rules! impl_bin_codec_for_number {
($num:ty) => {
impl BinCodec<$num> for FromToBytesCodec {
type Error = FromToBytesCodecError;
impl Encoder<$num> for FromToBytesCodec {
type Error = ();
type Encoded = Vec<u8>;
fn encode(&self, val: &$num) -> Result<Vec<u8>, Self::Error> {
fn encode(val: &$num) -> Result<Self::Encoded, Self::Error> {
Ok(val.to_be_bytes().to_vec())
}
}
fn decode(&self, val: &[u8]) -> Result<$num, Self::Error> {
impl Decoder<$num> for FromToBytesCodec {
type Error = FromToBytesCodecError;
type Encoded = [u8];
fn decode(val: &Self::Encoded) -> Result<$num, Self::Error> {
Ok(<$num>::from_be_bytes(val.try_into()?))
}
}
@ -50,30 +59,54 @@ impl_bin_codec_for_number!(usize);
impl_bin_codec_for_number!(f32);
impl_bin_codec_for_number!(f64);
impl BinCodec<bool> for FromToBytesCodec {
type Error = FromToBytesCodecError;
impl Encoder<bool> for FromToBytesCodec {
type Error = ();
type Encoded = Vec<u8>;
fn encode(&self, val: &bool) -> Result<Vec<u8>, Self::Error> {
let codec = FromToBytesCodec;
fn encode(val: &bool) -> Result<Self::Encoded, Self::Error> {
let num: u8 = if *val { 1 } else { 0 };
codec.encode(&num)
Self::encode(&num)
}
}
fn decode(&self, val: &[u8]) -> Result<bool, Self::Error> {
let codec = FromToBytesCodec;
let num: u8 = codec.decode(val)?;
impl Decoder<bool> for FromToBytesCodec {
type Error = FromToBytesCodecError;
type Encoded = [u8];
fn decode(val: &Self::Encoded) -> Result<bool, Self::Error> {
let num: u8 = Self::decode(val)?;
Ok(num != 0)
}
}
impl BinCodec<String> for FromToBytesCodec {
type Error = FromToBytesCodecError;
impl Encoder<String> for FromToBytesCodec {
type Error = ();
type Encoded = Vec<u8>;
fn encode(&self, val: &String) -> Result<Vec<u8>, Self::Error> {
fn encode(val: &String) -> Result<Self::Encoded, Self::Error> {
Ok(val.as_bytes().to_vec())
}
}
fn decode(&self, val: &[u8]) -> Result<String, Self::Error> {
impl Decoder<String> for FromToBytesCodec {
type Error = FromToBytesCodecError;
type Encoded = [u8];
fn decode(val: &Self::Encoded) -> Result<String, Self::Error> {
Ok(String::from_utf8(val.to_vec())?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fromtobytes_codec() {
let t = 50;
let enc: Vec<u8> = FromToBytesCodec::encode(&t).unwrap();
let dec: i32 = FromToBytesCodec::decode(enc.as_slice()).unwrap();
assert_eq!(dec, t);
}
}

View file

@ -1,15 +1,16 @@
#[cfg(feature = "bincode_serde")]
mod bincode_serde;
mod from_to_bytes;
#[cfg(feature = "msgpack_serde")]
mod msgpack_serde;
#[cfg(feature = "prost")]
mod prost;
#[cfg(feature = "bincode")]
pub use bincode_serde::*;
#[allow(unused_imports)]
pub use from_to_bytes::*;
/// A codec for encoding and decoding values to and from strings.
/// These strings are intended to be sent over the network.
pub trait BinCodec<T>: Clone + 'static {
/// The error type returned when encoding or decoding fails.
type Error;
/// Encodes a value to a string.
fn encode(&self, val: &T) -> Result<Vec<u8>, Self::Error>;
/// Decodes a string to a value. Should be able to decode any string encoded by [`encode`].
fn decode(&self, val: &[u8]) -> Result<T, Self::Error>;
}
#[cfg(feature = "msgpack")]
pub use msgpack_serde::*;
#[cfg(feature = "prost")]
pub use prost::*;

View file

@ -0,0 +1,46 @@
use crate::utils::{Decoder, Encoder};
use serde::{Deserialize, Serialize};
/// A codec that relies on `rmp-serde` to encode data in the msgpack format.
///
/// This is only available with the **`msgpack` feature** enabled.
pub struct MsgpackSerdeCodec;
impl<T: serde::Serialize> Encoder<T> for MsgpackSerdeCodec {
type Error = rmp_serde::encode::Error;
type Encoded = Vec<u8>;
fn encode(val: &T) -> Result<Self::Encoded, Self::Error> {
rmp_serde::to_vec(val)
}
}
impl<T: serde::de::DeserializeOwned> Decoder<T> for MsgpackSerdeCodec {
type Error = rmp_serde::decode::Error;
type Encoded = [u8];
fn decode(val: &Self::Encoded) -> Result<T, Self::Error> {
rmp_serde::from_slice(val)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_msgpack_codec() {
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct Test {
s: String,
i: i32,
}
let t = Test {
s: String::from("party time 🎉"),
i: 42,
};
let enc = MsgpackSerdeCodec::encode(&t).unwrap();
let dec: Test = MsgpackSerdeCodec::decode(&enc).unwrap();
assert_eq!(dec, t);
}
}

View file

@ -0,0 +1,76 @@
use crate::utils::{Decoder, Encoder};
/// A codec for storing ProtoBuf messages that relies on [`prost`](https://github.com/tokio-rs/prost) to parse.
///
/// [Protocol buffers](https://protobuf.dev/overview/) is a serialisation format useful for
/// long-term storage. It provides semantics for versioning that are not present in JSON or other
/// formats. [`prost`] is a Rust implementation of Protocol Buffers.
///
/// This codec uses [`prost`](https://github.com/tokio-rs/prost) to encode the message into a byte stream.
/// To use it with local storage in the example below we wrap it with [`Base64`] to represent the bytes as a string.
///
/// ## Example
/// ```
/// # use leptos::*;
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions};
/// # use leptos_use::utils::{Base64, ProstCodec};
/// #
/// # pub fn Demo() -> impl IntoView {
/// // Primitive types:
/// let (get, set, remove) = use_local_storage::<i32, Base64<ProstCodec>>("my-key");
///
/// // Structs:
/// #[derive(Clone, PartialEq, prost::Message)]
/// pub struct MyState {
/// #[prost(string, tag = "1")]
/// pub hello: String,
/// }
/// let (get, set, remove) = use_local_storage::<MyState, Base64<ProstCodec>>("my-struct-key");
/// # view! { }
/// # }
/// ```
///
/// Note: we've defined and used the `prost` attribute here for brevity. Alternate usage would be to
/// describe the message in a .proto file and use [`prost_build`](https://docs.rs/prost-build) to
/// auto-generate the Rust code.
pub struct ProstCodec;
impl<T: prost::Message> Encoder<T> for ProstCodec {
type Error = ();
type Encoded = Vec<u8>;
fn encode(val: &T) -> Result<Self::Encoded, Self::Error> {
let buf = val.encode_to_vec();
Ok(buf)
}
}
impl<T: prost::Message + Default> Decoder<T> for ProstCodec {
type Error = prost::DecodeError;
type Encoded = [u8];
fn decode(val: &Self::Encoded) -> Result<T, Self::Error> {
T::decode(val)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prost_codec() {
#[derive(Clone, PartialEq, prost::Message)]
struct Test {
#[prost(string, tag = "1")]
s: String,
#[prost(int32, tag = "2")]
i: i32,
}
let t = Test {
s: String::from("party time 🎉"),
i: 42,
};
assert_eq!(ProstCodec::decode(&ProstCodec::encode(&t).unwrap()), Ok(t));
}
}

View file

@ -1,4 +1,46 @@
mod bin;
mod string;
pub use bin::*;
pub use string::*;
use thiserror::Error;
/// Trait every encoder must implement.
pub trait Encoder<T>: 'static {
type Error;
type Encoded;
fn encode(val: &T) -> Result<Self::Encoded, Self::Error>;
}
/// Trait every decoder must implement.
pub trait Decoder<T>: 'static {
type Error;
type Encoded: ?Sized;
fn decode(val: &Self::Encoded) -> Result<T, Self::Error>;
}
/// Trait to check if a type is binary or encodes data in a string.
pub trait IsBinary<T> {
fn is_binary() -> bool {
true
}
}
impl<Enc, T> IsBinary<T> for Enc
where
Enc: Encoder<T, Encoded = String>,
{
fn is_binary() -> bool {
false
}
}
#[derive(Error, Debug)]
pub enum CodecError<E, D> {
#[error("failed to encode: {0}")]
Encode(E),
#[error("failed to decode: {0}")]
Decode(D),
}

View file

@ -0,0 +1,67 @@
use crate::utils::{Decoder, Encoder};
use base64::Engine;
use thiserror::Error;
/// Wraps a binary codec and make it a string codec by representing the binary data as a base64
/// string.
///
/// Only available with the **`base64` feature** enabled.
///
/// Example:
///
/// ```
/// # use leptos_use::utils::{Base64, MsgpackSerdeCodec, Encoder, Decoder};
/// # use serde::{Serialize, Deserialize};
/// #
/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
/// struct MyState {
/// chicken_count: u32,
/// egg_count: u32,
/// farm_name: String,
/// }
///
/// let original_value = MyState {
/// chicken_count: 10,
/// egg_count: 20,
/// farm_name: "My Farm".to_owned(),
/// };
///
/// let encoded: String = Base64::<MsgpackSerdeCodec>::encode(&original_value).unwrap();
/// let decoded: MyState = Base64::<MsgpackSerdeCodec>::decode(&encoded).unwrap();
///
/// assert_eq!(decoded, original_value);
/// ```
pub struct Base64<C>(C);
#[derive(Error, Debug, PartialEq)]
pub enum Base64DecodeError<Err> {
#[error("failed to decode base64: {0}")]
DecodeBase64(#[from] base64::DecodeError),
#[error("failed to decode: {0}")]
Decoder(Err),
}
impl<T, E> Encoder<T> for Base64<E>
where
E: Encoder<T, Encoded = Vec<u8>>,
{
type Error = E::Error;
type Encoded = String;
fn encode(val: &T) -> Result<Self::Encoded, Self::Error> {
Ok(base64::engine::general_purpose::STANDARD.encode(E::encode(val)?))
}
}
impl<T, D> Decoder<T> for Base64<D>
where
D: Decoder<T, Encoded = [u8]>,
{
type Error = Base64DecodeError<D::Error>;
type Encoded = str;
fn decode(val: &Self::Encoded) -> Result<T, Self::Error> {
let buf = base64::engine::general_purpose::STANDARD.decode(val)?;
D::decode(&buf).map_err(|err| Base64DecodeError::Decoder(err))
}
}

View file

@ -1,9 +1,11 @@
use super::StringCodec;
use crate::utils::{Decoder, Encoder};
use std::str::FromStr;
/// A codec for strings that relies on [`FromStr`] and [`ToString`] to parse.
/// A string codec that relies on [`FromStr`] and [`ToString`]. It can encode anything that
/// implements [`ToString`] and decode anything that implements [`FromStr`].
///
/// This makes simple key / value easy to use for primitive types. It is also useful for encoding simple data structures without depending on serde.
/// This makes simple key / value easy to use for primitive types. It is also useful for encoding
/// simply data structures without depending on third party crates like serde and serde_json.
///
/// ## Example
/// ```
@ -16,18 +18,23 @@ use std::str::FromStr;
/// # view! { }
/// # }
/// ```
#[derive(Copy, Clone, Default, PartialEq)]
pub struct FromToStringCodec;
impl<T: FromStr + ToString> StringCodec<T> for FromToStringCodec {
type Error = T::Err;
impl<T: ToString> Encoder<T> for FromToStringCodec {
type Error = ();
type Encoded = String;
fn encode(&self, val: &T) -> Result<String, Self::Error> {
fn encode(val: &T) -> Result<String, Self::Error> {
Ok(val.to_string())
}
}
fn decode(&self, str: String) -> Result<T, Self::Error> {
T::from_str(&str)
impl<T: FromStr> Decoder<T> for FromToStringCodec {
type Error = T::Err;
type Encoded = str;
fn decode(val: &Self::Encoded) -> Result<T, Self::Error> {
T::from_str(val)
}
}
@ -38,8 +45,7 @@ mod tests {
#[test]
fn test_string_codec() {
let s = String::from("party time 🎉");
let codec = FromToStringCodec;
assert_eq!(codec.encode(&s), Ok(s.clone()));
assert_eq!(codec.decode(s.clone()), Ok(s));
assert_eq!(FromToStringCodec::encode(&s), Ok(s.clone()));
assert_eq!(FromToStringCodec::decode(&s), Ok(s));
}
}

View file

@ -1,154 +0,0 @@
use super::StringCodec;
/// A codec for storing JSON messages that relies on [`serde_json`] to parse.
///
/// ## Example
/// ```
/// # use leptos::*;
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions};
/// # use serde::{Deserialize, Serialize};
/// # use leptos_use::utils::JsonCodec;
/// #
/// # pub fn Demo() -> impl IntoView {
/// // Primitive types:
/// let (get, set, remove) = use_local_storage::<i32, JsonCodec>("my-key");
///
/// // Structs:
/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)]
/// pub struct MyState {
/// pub hello: String,
/// }
/// let (get, set, remove) = use_local_storage::<MyState, JsonCodec>("my-struct-key");
/// # view! { }
/// # }
/// ```
///
/// ## Versioning
///
/// If the JSON decoder fails, the storage hook will return `T::Default` dropping the stored JSON value. See [`Codec`](super::Codec) for general information on codec versioning.
///
/// ### Rely on serde
/// This codec uses [`serde_json`] under the hood. 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).
///
/// ### String replacement
/// Previous versions of leptos-use offered a `merge_defaults` fn to rewrite the encoded value. This is possible by wrapping the codec but should be avoided.
///
/// ```
/// # use leptos::*;
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions};
/// # use serde::{Deserialize, Serialize};
/// # use leptos_use::utils::StringCodec;
/// #
/// # pub fn Demo() -> impl IntoView {
/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)]
/// pub struct MyState {
/// pub hello: String,
/// pub greeting: String,
/// }
///
/// #[derive(Clone, Default)]
/// pub struct MyStateCodec();
/// impl StringCodec<MyState> for MyStateCodec {
/// type Error = serde_json::Error;
///
/// fn encode(&self, val: &MyState) -> Result<String, Self::Error> {
/// serde_json::to_string(val)
/// }
///
/// fn decode(&self, stored_value: String) -> Result<MyState, Self::Error> {
/// let default_value = MyState::default();
/// let rewritten = if stored_value.contains(r#""greeting":"#) {
/// stored_value
/// } else {
/// // add "greeting": "Hello" to the string
/// stored_value.replace("}", &format!(r#""greeting": "{}"}}"#, default_value.greeting))
/// };
/// serde_json::from_str(&rewritten)
/// }
/// }
///
/// let (get, set, remove) = use_local_storage::<MyState, MyStateCodec>("my-struct-key");
/// # view! { }
/// # }
/// ```
///
/// ### Transform a `JsValue`
/// A better alternative to string replacement might be to parse the JSON then transform the resulting `JsValue` before decoding it to to your struct again.
///
/// ```
/// # 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::StringCodec;
/// #
/// # pub fn Demo() -> impl IntoView {
/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)]
/// pub struct MyState {
/// pub hello: String,
/// pub greeting: String,
/// }
///
/// #[derive(Clone, Default)]
/// pub struct MyStateCodec();
/// impl StringCodec<MyState> for MyStateCodec {
/// type Error = serde_json::Error;
///
/// fn encode(&self, val: &MyState) -> Result<String, Self::Error> {
/// serde_json::to_string(val)
/// }
///
/// fn decode(&self, stored_value: String) -> Result<MyState, Self::Error> {
/// 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())
/// }
/// }
/// }
///
/// let (get, set, remove) = use_local_storage::<MyState, MyStateCodec>("my-struct-key");
/// # view! { }
/// # }
/// ```
#[derive(Copy, Clone, Default, PartialEq)]
pub struct JsonCodec;
impl<T: serde::Serialize + serde::de::DeserializeOwned> StringCodec<T> for JsonCodec {
type Error = serde_json::Error;
fn encode(&self, val: &T) -> Result<String, Self::Error> {
serde_json::to_string(val)
}
fn decode(&self, str: String) -> Result<T, Self::Error> {
serde_json::from_str(&str)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_codec() {
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct Test {
s: String,
i: i32,
}
let t = Test {
s: String::from("party time 🎉"),
i: 42,
};
let codec = JsonCodec;
let enc = codec.encode(&t).unwrap();
let dec: Test = codec.decode(enc).unwrap();
assert_eq!(dec, t);
}
}

View file

@ -0,0 +1,67 @@
use crate::utils::{Decoder, Encoder};
/// A codec for encoding JSON messages that relies on [`serde_json`].
///
/// Only available with the **`json` feature** enabled.
///
/// ## Example
///
/// ```
/// # use leptos::*;
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions};
/// # use serde::{Deserialize, Serialize};
/// # use leptos_use::utils::JsonSerdeCodec;
/// #
/// # pub fn Demo() -> impl IntoView {
/// // Primitive types:
/// let (get, set, remove) = use_local_storage::<i32, JsonSerdeCodec>("my-key");
///
/// // Structs:
/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)]
/// pub struct MyState {
/// pub hello: String,
/// }
/// let (get, set, remove) = use_local_storage::<MyState, JsonSerdeCodec>("my-struct-key");
/// # view! { }
/// # }
/// ```
pub struct JsonSerdeCodec;
impl<T: serde::Serialize> Encoder<T> for JsonSerdeCodec {
type Error = serde_json::Error;
type Encoded = String;
fn encode(val: &T) -> Result<Self::Encoded, Self::Error> {
serde_json::to_string(val)
}
}
impl<T: serde::de::DeserializeOwned> Decoder<T> for JsonSerdeCodec {
type Error = serde_json::Error;
type Encoded = str;
fn decode(val: &Self::Encoded) -> Result<T, Self::Error> {
serde_json::from_str(val)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_codec() {
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct Test {
s: String,
i: i32,
}
let t = Test {
s: String::from("party time 🎉"),
i: 42,
};
let enc = JsonSerdeCodec::encode(&t).unwrap();
let dec: Test = JsonSerdeCodec::decode(&enc).unwrap();
assert_eq!(dec, t);
}
}

View file

@ -1,36 +1,13 @@
#[cfg(feature = "base64")]
mod base64;
mod from_to_string;
#[cfg(feature = "serde_json")]
mod json;
#[cfg(feature = "prost")]
mod prost;
#[cfg(feature = "json_serde")]
mod json_serde;
mod option;
#[cfg(feature = "base64")]
pub use base64::*;
pub use from_to_string::*;
#[cfg(feature = "serde_json")]
pub use json::*;
#[cfg(feature = "prost")]
pub use prost::*;
/// A codec for encoding and decoding values to and from strings.
/// These strings are intended to be stored in browser storage or sent over the network.
///
/// ## 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 thousand separator on 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 [`StringCodec`](super::StringCodec) can avoid versioning entirely by keeping to privimitive 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 fallback to the default without interfering with the other field.
///
/// - The [`ProstCodec`](super::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 [`JsonCodec`](super::JsonCodec) stores data as JSON. We can then rely on serde or by providing our own manual version handling. See the codec for more details.
pub trait StringCodec<T>: Clone + 'static {
/// The error type returned when encoding or decoding fails.
type Error;
/// Encodes a value to a string.
fn encode(&self, val: &T) -> Result<String, Self::Error>;
/// Decodes a string to a value. Should be able to decode any string encoded by [`encode`].
fn decode(&self, str: String) -> Result<T, Self::Error>;
}
#[cfg(feature = "json_serde")]
pub use json_serde::*;
pub use option::*;

View file

@ -0,0 +1,45 @@
use crate::utils::{Decoder, Encoder};
/// Wraps a string codec that encodes `T` to create a codec that encodes `Option<T>`.
///
/// Example:
///
/// ```
/// # use leptos_use::utils::{OptionCodec, FromToStringCodec, Encoder, Decoder};
/// #
/// let original_value = Some(4);
/// let encoded = OptionCodec::<FromToStringCodec>::encode(&original_value).unwrap();
/// let decoded = OptionCodec::<FromToStringCodec>::decode(&encoded).unwrap();
///
/// assert_eq!(decoded, original_value);
/// ```
pub struct OptionCodec<C>(C);
impl<T, E> Encoder<Option<T>> for OptionCodec<E>
where
E: Encoder<T, Encoded = String>,
{
type Error = E::Error;
type Encoded = String;
fn encode(val: &Option<T>) -> Result<String, Self::Error> {
match val {
Some(val) => Ok(format!("~<|Some|>~{}", E::encode(val)?)),
None => Ok("~<|None|>~".to_owned()),
}
}
}
impl<T, D> Decoder<Option<T>> for OptionCodec<D>
where
D: Decoder<T, Encoded = str>,
{
type Error = D::Error;
type Encoded = str;
fn decode(str: &Self::Encoded) -> Result<Option<T>, Self::Error> {
str.strip_prefix("~<|Some|>~")
.map(|v| D::decode(v))
.transpose()
}
}

View file

@ -1,80 +0,0 @@
use super::StringCodec;
use base64::Engine;
use thiserror::Error;
/// A codec for storing ProtoBuf messages that relies on [`prost`] to parse.
///
/// [Protocol buffers](https://protobuf.dev/overview/) is a serialisation format useful for long-term storage. It provides semantics for versioning that are not present in JSON or other formats. [`prost`] is a Rust implementation of Protocol Buffers.
///
/// This codec uses [`prost`] to encode the message and then [`base64`](https://docs.rs/base64) to represent the bytes as a string.
///
/// ## Example
/// ```
/// # use leptos::*;
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions};
/// # use leptos_use::utils::ProstCodec;
/// #
/// # pub fn Demo() -> impl IntoView {
/// // Primitive types:
/// let (get, set, remove) = use_local_storage::<i32, ProstCodec>("my-key");
///
/// // Structs:
/// #[derive(Clone, PartialEq, prost::Message)]
/// pub struct MyState {
/// #[prost(string, tag = "1")]
/// pub hello: String,
/// }
/// let (get, set, remove) = use_local_storage::<MyState, ProstCodec>("my-struct-key");
/// # view! { }
/// # }
/// ```
///
/// Note: we've defined and used the `prost` attribute here for brevity. Alternate usage would be to describe the message in a .proto file and use [`prost_build`](https://docs.rs/prost-build) to auto-generate the Rust code.
#[derive(Copy, Clone, Default, PartialEq)]
pub struct ProstCodec;
#[derive(Error, Debug, PartialEq)]
pub enum ProstCodecError {
#[error("failed to decode base64")]
DecodeBase64(base64::DecodeError),
#[error("failed to decode protobuf")]
DecodeProst(#[from] prost::DecodeError),
}
impl<T: Default + prost::Message> StringCodec<T> for ProstCodec {
type Error = ProstCodecError;
fn encode(&self, val: &T) -> Result<String, Self::Error> {
let buf = val.encode_to_vec();
Ok(base64::engine::general_purpose::STANDARD.encode(buf))
}
fn decode(&self, str: String) -> Result<T, Self::Error> {
let buf = base64::engine::general_purpose::STANDARD
.decode(str)
.map_err(ProstCodecError::DecodeBase64)?;
T::decode(buf.as_slice()).map_err(ProstCodecError::DecodeProst)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prost_codec() {
#[derive(Clone, PartialEq, prost::Message)]
struct Test {
#[prost(string, tag = "1")]
s: String,
#[prost(int32, tag = "2")]
i: i32,
}
let t = Test {
s: String::from("party time 🎉"),
i: 42,
};
let codec = ProstCodec;
assert_eq!(codec.decode(codec.encode(&t).unwrap()), Ok(t));
}
}