mirror of
https://github.com/adoyle0/leptos-use.git
synced 2025-01-23 00:59:22 -05:00
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:
parent
ae019b4734
commit
a8de6a96dd
24 changed files with 826 additions and 504 deletions
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
|
@ -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
|
||||
|
|
104
CHANGELOG.md
104
CHANGELOG.md
|
@ -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
|
||||
|
||||
|
@ -117,8 +120,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- The `leptos` version is now 0.6
|
||||
- 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 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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -146,15 +150,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- (@feral-dot-io) The use `use_<type>_storage` functions have been rewritten to use `Codec`s instead of always
|
||||
requiring `serde`.
|
||||
- This also removes the feature `storage`
|
||||
- By default the `StringCodec` is used which relies on types implementing `FromString + ToString`
|
||||
- If you want to use `JsonCodec` you have to enable the feature `serde`
|
||||
- If you want to use `ProstCodec` (new!) you have to enable the feature `prost`.
|
||||
- This also removes the feature `storage`
|
||||
- By default the `StringCodec` is used which relies on types implementing `FromString + ToString`
|
||||
- If you want to use `JsonCodec` you have to enable the feature `serde`
|
||||
- If you want to use `ProstCodec` (new!) you have to enable the feature `prost`.
|
||||
- (@feral-dot-io) The Rust flag `--cfg=web_sys_unstable_apis` is not needed anymore since relevant `web_sys` APIs are
|
||||
now stable.
|
||||
This affects in particular
|
||||
- `use_element_size`
|
||||
- `use_resize_observer`
|
||||
- `use_element_size`
|
||||
- `use_resize_observer`
|
||||
|
||||
### Fixes 🍕
|
||||
|
||||
|
@ -170,16 +174,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Fixes 🍕
|
||||
|
||||
- Fixed SSR for
|
||||
- use_timestamp
|
||||
- use_raf_fn
|
||||
- use_idle
|
||||
- use_timestamp
|
||||
- use_raf_fn
|
||||
- use_idle
|
||||
|
||||
## [0.8.1] - 2023-10-28
|
||||
|
||||
### Fixes 🍕
|
||||
|
||||
- Using strings for `ElementMaybeSignal` and `ElementsMaybeSignal` is now SSR safe.
|
||||
- This fixes specifically `use_color_mode` to work on the server.
|
||||
- This fixes specifically `use_color_mode` to work on the server.
|
||||
|
||||
## [0.8.0] - 2023-10-24
|
||||
|
||||
|
@ -234,17 +238,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- `use_mutation_observer_with_options` now takes a `UseMutationObserverOptions` instead of
|
||||
a `web_sys::MutationObserverInit`.
|
||||
- `use_websocket`:
|
||||
- takes now a `&str` instead of a `String` as its `url` parameter.
|
||||
- same for the returned `send` method.
|
||||
- The `ready_state` return type is now renamed to `ConnectionReadyState` instead of `UseWebSocketReadyState`.
|
||||
- The returned signals `ready_state`, `message`, `message_bytes` have now the type
|
||||
`Signal<...>` instead of `ReadSignal<...>` to make them more consistent with other functions.
|
||||
- The options `reconnect_limit` and `reconnect_interval` now take a `u64` instead of `Option<u64>` to improve DX.
|
||||
- The option `manual` has been renamed to `immediate` to make it more consistent with other functions.
|
||||
To port please note that `immediate` is the inverse of `manual` (`immediate` = `!manual`).
|
||||
- Added documentation how pass it ergonomically as context.
|
||||
- takes now a `&str` instead of a `String` as its `url` parameter.
|
||||
- same for the returned `send` method.
|
||||
- The `ready_state` return type is now renamed to `ConnectionReadyState` instead of `UseWebSocketReadyState`.
|
||||
- The returned signals `ready_state`, `message`, `message_bytes` have now the type
|
||||
`Signal<...>` instead of `ReadSignal<...>` to make them more consistent with other functions.
|
||||
- The options `reconnect_limit` and `reconnect_interval` now take a `u64` instead of `Option<u64>` to improve DX.
|
||||
- The option `manual` has been renamed to `immediate` to make it more consistent with other functions.
|
||||
To port please note that `immediate` is the inverse of `manual` (`immediate` = `!manual`).
|
||||
- Added documentation how pass it ergonomically as context.
|
||||
- `use_color_mode`:
|
||||
- The optional `on_changed` handler parameters have changed slightly. Please refer to the docs for more details.
|
||||
- The optional `on_changed` handler parameters have changed slightly. Please refer to the docs for more details.
|
||||
- Throttled or debounced functions cannot be `FnOnce` anymore.
|
||||
- All traits `ClonableFn...` have been removed.
|
||||
|
||||
|
@ -255,16 +259,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Callback in `use_raf_fn` doesn't require to be cloneable anymore
|
||||
- All (!) functions can now be safely called on the server. Specifically this includes the following that before
|
||||
panicked on the server:
|
||||
- `use_scroll`
|
||||
- `use_event_listener`
|
||||
- `use_element_hover`
|
||||
- `on_click_outside`
|
||||
- `use_drop_zone`
|
||||
- `use_element_size`
|
||||
- `use_element_visibility`
|
||||
- `use_resize_observer`
|
||||
- `use_intersection_observer`
|
||||
- `use_mutation_observer`
|
||||
- `use_scroll`
|
||||
- `use_event_listener`
|
||||
- `use_element_hover`
|
||||
- `on_click_outside`
|
||||
- `use_drop_zone`
|
||||
- `use_element_size`
|
||||
- `use_element_visibility`
|
||||
- `use_resize_observer`
|
||||
- `use_intersection_observer`
|
||||
- `use_mutation_observer`
|
||||
|
||||
### Fixes 🍕
|
||||
|
||||
|
@ -304,18 +308,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- The following functions now accept a `MaybeRwSignal` as their initial/default value which means
|
||||
you can use a synchronized `RwSignal` in those places.
|
||||
- `use_color_mode`
|
||||
- `use_cycle_list`
|
||||
- `use_favicon`
|
||||
- `use_storage`
|
||||
- `use_local_storage`
|
||||
- `use_session_storage`
|
||||
- `use_color_mode`
|
||||
- `use_cycle_list`
|
||||
- `use_favicon`
|
||||
- `use_storage`
|
||||
- `use_local_storage`
|
||||
- `use_session_storage`
|
||||
- Instead of returning `ReadSignal`, the following functions now return `Signal`.
|
||||
- `use_color_mode`
|
||||
- `use_favicon`
|
||||
- `use_storage`
|
||||
- `use_local_storage`
|
||||
- `use_session_storage`
|
||||
- `use_color_mode`
|
||||
- `use_favicon`
|
||||
- `use_storage`
|
||||
- `use_local_storage`
|
||||
- `use_session_storage`
|
||||
|
||||
### Fixes 🍕
|
||||
|
||||
|
@ -388,11 +392,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- You can now specify a `&str` or `Signal<String>` with CSS selectors wherever a node ref is accepted
|
||||
- Callbacks of the following functions no longer require `Clone`
|
||||
- `use_resize_observer`
|
||||
- `use_intersection_observer`
|
||||
- `use_resize_observer`
|
||||
- `use_intersection_observer`
|
||||
- These functions now also accept multiple target elements in addition to a single one:
|
||||
- `use_resize_observer`
|
||||
- `use_intersection_observer`
|
||||
- `use_resize_observer`
|
||||
- `use_intersection_observer`
|
||||
|
||||
### New Functions 🚀
|
||||
|
||||
|
|
10
Cargo.toml
10
Cargo.toml
|
@ -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
156
docs/book/src/codecs.md
Normal 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! { }
|
||||
# }
|
||||
```
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
46
src/utils/codecs/bin/bincode_serde.rs
Normal file
46
src/utils/codecs/bin/bincode_serde.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
46
src/utils/codecs/bin/msgpack_serde.rs
Normal file
46
src/utils/codecs/bin/msgpack_serde.rs
Normal 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);
|
||||
}
|
||||
}
|
76
src/utils/codecs/bin/prost.rs
Normal file
76
src/utils/codecs/bin/prost.rs
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
|
|
67
src/utils/codecs/string/base64.rs
Normal file
67
src/utils/codecs/string/base64.rs
Normal 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))
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
67
src/utils/codecs/string/json_serde.rs
Normal file
67
src/utils/codecs/string/json_serde.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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::*;
|
||||
|
|
45
src/utils/codecs/string/option.rs
Normal file
45
src/utils/codecs/string/option.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue