mirror of
https://github.com/adoyle0/leptos-use.git
synced 2025-01-22 16:49:22 -05:00
codecs extracted into crate codee
This commit is contained in:
parent
c20d78c2ea
commit
e4ad9f11af
30 changed files with 70 additions and 725 deletions
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
@ -30,13 +30,13 @@ jobs:
|
|||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
- name: Clippy
|
||||
run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings
|
||||
run: cargo clippy --features docs,math --tests -- -D warnings
|
||||
- name: Run tests (general)
|
||||
run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64
|
||||
run: cargo test --features math,docs,ssr
|
||||
- name: Run tests (axum)
|
||||
run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64,axum --doc use_cookie::use_cookie
|
||||
run: cargo test --features math,docs,ssr,axum --doc use_cookie::use_cookie
|
||||
- name: Run tests (actix)
|
||||
run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64,actix --doc use_cookie::use_cookie
|
||||
run: cargo test --features math,docs,ssr,actix --doc use_cookie::use_cookie
|
||||
|
||||
#### mdbook
|
||||
- name: Install mdbook I
|
||||
|
|
8
.github/workflows/tests.yml
vendored
8
.github/workflows/tests.yml
vendored
|
@ -23,10 +23,10 @@ jobs:
|
|||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
- name: Clippy
|
||||
run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings
|
||||
run: cargo clippy --features docs,math --tests -- -D warnings
|
||||
- name: Run tests (general)
|
||||
run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64
|
||||
run: cargo test --features math,docs,ssr
|
||||
- name: Run tests (axum)
|
||||
run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64,axum --doc use_cookie::use_cookie
|
||||
run: cargo test --features math,docs,ssr,axum --doc use_cookie::use_cookie
|
||||
- name: Run tests (actix)
|
||||
run: cargo test --features math,docs,ssr,prost,json_serde,msgpack_serde,bincode_serde,base64,actix --doc use_cookie::use_cookie
|
||||
run: cargo test --features math,docs,ssr,actix --doc use_cookie::use_cookie
|
||||
|
|
17
Cargo.toml
17
Cargo.toml
|
@ -15,9 +15,8 @@ homepage = "https://leptos-use.rs"
|
|||
[dependencies]
|
||||
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 }
|
||||
codee = "0.1"
|
||||
cookie = { version = "0.18", features = ["percent-encode"] }
|
||||
default-struct-builder = "0.5"
|
||||
futures-util = "0.3"
|
||||
|
@ -33,10 +32,6 @@ leptos_actix = { version = "0.6", optional = true }
|
|||
leptos-spin = { version = "0.1", optional = true }
|
||||
num = { version = "0.4", optional = true }
|
||||
paste = "1"
|
||||
prost = { version = "0.12", optional = true }
|
||||
rmp-serde = { version = "1.1", optional = true }
|
||||
serde = { version = "1", optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
thiserror = "1"
|
||||
wasm-bindgen = "0.2.92"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
|
@ -136,19 +131,19 @@ features = [
|
|||
getrandom = { version = "0.2", features = ["js"] }
|
||||
leptos_meta = "0.6"
|
||||
rand = "0.8"
|
||||
codee = { version = "0.1", features = ["json_serde", "msgpack_serde", "base64", "prost"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
actix = ["dep:actix-web", "dep:leptos_actix", "dep:http0_2"]
|
||||
axum = ["dep:leptos_axum", "dep:http1"]
|
||||
docs = []
|
||||
math = ["num"]
|
||||
prost = ["dep:prost"]
|
||||
json_serde = ["dep:serde_json", "dep:serde"]
|
||||
spin = ["dep:leptos-spin", "dep:http1"]
|
||||
ssr = []
|
||||
msgpack_serde = ["dep:rmp-serde", "dep:serde"]
|
||||
bincode_serde = ["dep:bincode", "dep:serde"]
|
||||
wasm_ssr = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["math", "docs", "ssr", "prost", "json_serde", "msgpack_serde", "bincode_serde"]
|
||||
features = ["math", "docs", "ssr"]
|
||||
rustdoc-args = ["--cfg=web_sys_unstable_apis"]
|
||||
rustc-args = ["--cfg=web_sys_unstable_apis"]
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
[Introduction](introduction.md)
|
||||
[Get Started](get_started.md)
|
||||
[Options](options.md)
|
||||
[Element Parameters](element_parameters.md)
|
||||
[Server-Side Rendering](server_side_rendering.md)
|
||||
[Encoding and Decoding Data](codecs.md)
|
||||
|
|
22
docs/book/src/options.md
Normal file
22
docs/book/src/options.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Options
|
||||
|
||||
Most functions in Leptos-Use come with a version `..._with_options`. For example `use_css_var` has a
|
||||
version `use_css_var_with_options`. As the name suggests, you can provide additional options to those versions of the
|
||||
functions.
|
||||
|
||||
These options are defined as structs with the corresponding PascalCase name. For our example `use_css_var_with_options`
|
||||
the name of the struct is `UseCssVarOptions`. Every option struct implements `Default` and the builder pattern to
|
||||
make it easy to change only the values needed. This can look like the following example.
|
||||
|
||||
```rust
|
||||
let (color, set_color) = use_css_var_with_options(
|
||||
"--color",
|
||||
UseCssVarOptions::default()
|
||||
.target(el)
|
||||
.initial_value("#eee"),
|
||||
);
|
||||
```
|
||||
|
||||
Here only the values `target` and `initial_value` are changed and everything else is left to default.
|
||||
|
||||
TODO : automatic conversion like Fn and Option
|
|
@ -4,11 +4,12 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
codee = { path = "../../../codee", features = ["json_serde"] }
|
||||
leptos = { version = "0.6", features = ["nightly", "csr"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos-use = { path = "../..", features = ["docs", "json_serde"] }
|
||||
leptos-use = { path = "../..", features = ["docs"] }
|
||||
web-sys = "0.3"
|
||||
serde = "1.0.163"
|
||||
|
||||
|
|
|
@ -5,10 +5,11 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
leptos = { version = "0.6", features = ["nightly", "csr"] }
|
||||
codee = { path = "../../../codee", features = ["msgpack_serde"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos-use = { path = "../..", features = ["docs", "msgpack_serde"] }
|
||||
leptos-use = { path = "../..", features = ["docs"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
web-sys = "0.3"
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use leptos_use::{
|
|||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use leptos_use::utils::{FromToStringCodec, MsgpackSerdeCodec};
|
||||
use codee::{binary::MsgpackSerdeCodec, string::FromToStringCodec};
|
||||
use web_sys::{CloseEvent, Event};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::{use_storage_with_options, StorageType, UseStorageOptions};
|
||||
use crate::utils::{Decoder, Encoder};
|
||||
use codee::{Decoder, Encoder};
|
||||
use leptos::signal_prelude::*;
|
||||
|
||||
/// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::{use_storage_with_options, StorageType, UseStorageOptions};
|
||||
use crate::utils::{Decoder, Encoder};
|
||||
use codee::{Decoder, Encoder};
|
||||
use leptos::signal_prelude::*;
|
||||
|
||||
/// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage).
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::utils::{CodecError, Decoder, Encoder};
|
||||
use crate::{
|
||||
core::{MaybeRwSignal, StorageType},
|
||||
utils::FilterOptions,
|
||||
};
|
||||
use codee::{CodecError, Decoder, Encoder};
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use leptos::*;
|
||||
use std::rc::Rc;
|
||||
|
@ -39,7 +39,8 @@ 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, JsonSerdeCodec, ProstCodec, Base64};
|
||||
/// # use codee::string::{FromToStringCodec, JsonSerdeCodec, Base64};
|
||||
/// # use codee::binary::ProstCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # pub fn Demo() -> impl IntoView {
|
||||
|
@ -94,7 +95,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
|
|||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::use_session_storage;
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # pub fn Example() -> impl IntoView {
|
||||
|
@ -127,7 +128,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
|
|||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::{use_local_storage_with_options, UseStorageOptions};
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # pub fn Example() -> impl IntoView {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::utils::{CodecError, Decoder, Encoder};
|
||||
use crate::{
|
||||
js, use_event_listener, use_event_listener_with_options, use_supported, UseEventListenerOptions,
|
||||
};
|
||||
use codee::{CodecError, Decoder, Encoder};
|
||||
use leptos::*;
|
||||
use thiserror::Error;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
@ -23,7 +23,7 @@ use wasm_bindgen::JsValue;
|
|||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::{use_broadcast_channel, UseBroadcastChannelReturn};
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
|
@ -54,7 +54,7 @@ use wasm_bindgen::JsValue;
|
|||
/// # use leptos::*;
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// # use leptos_use::use_broadcast_channel;
|
||||
/// # use leptos_use::utils::JsonSerdeCodec;
|
||||
/// # use codee::string::JsonSerdeCodec;
|
||||
/// #
|
||||
/// // Data sent in JSON must implement Serialize, Deserialize:
|
||||
/// #[derive(Serialize, Deserialize, Clone, PartialEq)]
|
||||
|
|
|
@ -2,8 +2,8 @@ use crate::core::url;
|
|||
use crate::core::StorageType;
|
||||
use crate::core::{ElementMaybeSignal, MaybeRwSignal};
|
||||
use crate::storage::{use_storage_with_options, UseStorageOptions};
|
||||
use crate::utils::FromToStringCodec;
|
||||
use crate::{sync_signal_with_options, use_cookie, use_preferred_dark, SyncSignalOptions};
|
||||
use codee::string::FromToStringCodec;
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use leptos::*;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use crate::core::now;
|
||||
use crate::utils::{CodecError, Decoder, Encoder};
|
||||
use codee::{CodecError, Decoder, Encoder};
|
||||
use cookie::time::{Duration, OffsetDateTime};
|
||||
pub use cookie::SameSite;
|
||||
use cookie::{Cookie, CookieJar};
|
||||
|
@ -30,7 +30,7 @@ use std::rc::Rc;
|
|||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::use_cookie;
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// # use rand::prelude::*;
|
||||
///
|
||||
/// #
|
||||
|
@ -70,7 +70,7 @@ use std::rc::Rc;
|
|||
/// # use cookie::SameSite;
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
|
@ -105,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::JsonSerdeCodec;
|
||||
/// # use codee::string::JsonSerdeCodec;
|
||||
/// #
|
||||
/// # #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
/// # pub struct Auth {
|
||||
|
@ -215,10 +215,10 @@ where
|
|||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
use crate::utils::{FromToStringCodec, OptionCodec};
|
||||
use crate::{
|
||||
use_broadcast_channel, watch_pausable, UseBroadcastChannelReturn, WatchPausableReturn,
|
||||
};
|
||||
use codee::string::{FromToStringCodec, OptionCodec};
|
||||
|
||||
let UseBroadcastChannelReturn { message, post, .. } =
|
||||
use_broadcast_channel::<Option<String>, OptionCodec<FromToStringCodec>>(&format!(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::core::ConnectionReadyState;
|
||||
use crate::utils::Decoder;
|
||||
use crate::{js, use_event_listener, ReconnectLimit};
|
||||
use codee::Decoder;
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use leptos::*;
|
||||
use std::cell::Cell;
|
||||
|
@ -27,7 +27,8 @@ use thiserror::Error;
|
|||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::{use_event_source, UseEventSourceReturn, utils::JsonSerdeCodec};
|
||||
/// # use leptos_use::{use_event_source, UseEventSourceReturn};
|
||||
/// # use codee::string::JsonSerdeCodec;
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// #
|
||||
/// #[derive(Serialize, Deserialize, Clone, PartialEq)]
|
||||
|
@ -56,7 +57,8 @@ 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};
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
|
@ -87,7 +89,8 @@ use thiserror::Error;
|
|||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions, utils::FromToStringCodec, ReconnectLimit};
|
||||
/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions, ReconnectLimit};
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
|
|
|
@ -8,7 +8,7 @@ use std::time::Duration;
|
|||
use thiserror::Error;
|
||||
|
||||
use crate::core::ConnectionReadyState;
|
||||
use crate::utils::{
|
||||
use codee::{
|
||||
CodecError, Decoder, Encoder, HybridCoderError, HybridDecoder, HybridEncoder, IsBinary,
|
||||
};
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
|
@ -31,7 +31,7 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket};
|
|||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// # use leptos_use::{use_websocket, UseWebSocketReturn};
|
||||
/// # use leptos_use::core::ConnectionReadyState;
|
||||
/// #
|
||||
|
@ -81,7 +81,7 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket};
|
|||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::utils::MsgpackSerdeCodec;
|
||||
/// # use codee::binary::MsgpackSerdeCodec;
|
||||
/// # use leptos_use::{use_websocket, UseWebSocketReturn};
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// #
|
||||
|
@ -164,7 +164,7 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket};
|
|||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// # use leptos_use::{use_websocket, UseWebSocketReturn};
|
||||
/// # use std::rc::Rc;
|
||||
/// # #[derive(Clone)]
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
use crate::utils::{Decoder, Encoder};
|
||||
|
||||
/// 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,112 +0,0 @@
|
|||
use crate::utils::{Decoder, Encoder};
|
||||
use thiserror::Error;
|
||||
|
||||
/// 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)]
|
||||
pub enum FromToBytesCodecError {
|
||||
#[error("failed to convert byte slice to byte array")]
|
||||
InvalidByteSlice(#[from] std::array::TryFromSliceError),
|
||||
|
||||
#[error("failed to convert byte array to string")]
|
||||
InvalidString(#[from] std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
macro_rules! impl_bin_codec_for_number {
|
||||
($num:ty) => {
|
||||
impl Encoder<$num> for FromToBytesCodec {
|
||||
type Error = ();
|
||||
type Encoded = Vec<u8>;
|
||||
|
||||
fn encode(val: &$num) -> Result<Self::Encoded, Self::Error> {
|
||||
Ok(val.to_be_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
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()?))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_bin_codec_for_number!(i8);
|
||||
impl_bin_codec_for_number!(u8);
|
||||
|
||||
impl_bin_codec_for_number!(i16);
|
||||
impl_bin_codec_for_number!(u16);
|
||||
|
||||
impl_bin_codec_for_number!(i32);
|
||||
impl_bin_codec_for_number!(u32);
|
||||
|
||||
impl_bin_codec_for_number!(i64);
|
||||
impl_bin_codec_for_number!(u64);
|
||||
|
||||
impl_bin_codec_for_number!(i128);
|
||||
impl_bin_codec_for_number!(u128);
|
||||
|
||||
impl_bin_codec_for_number!(isize);
|
||||
impl_bin_codec_for_number!(usize);
|
||||
|
||||
impl_bin_codec_for_number!(f32);
|
||||
impl_bin_codec_for_number!(f64);
|
||||
|
||||
impl Encoder<bool> for FromToBytesCodec {
|
||||
type Error = ();
|
||||
type Encoded = Vec<u8>;
|
||||
|
||||
fn encode(val: &bool) -> Result<Self::Encoded, Self::Error> {
|
||||
let num: u8 = if *val { 1 } else { 0 };
|
||||
Self::encode(&num)
|
||||
}
|
||||
}
|
||||
|
||||
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 Encoder<String> for FromToBytesCodec {
|
||||
type Error = ();
|
||||
type Encoded = Vec<u8>;
|
||||
|
||||
fn encode(val: &String) -> Result<Self::Encoded, Self::Error> {
|
||||
Ok(val.as_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
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,16 +0,0 @@
|
|||
#[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_serde")]
|
||||
pub use bincode_serde::*;
|
||||
#[allow(unused_imports)]
|
||||
pub use from_to_bytes::*;
|
||||
#[cfg(feature = "msgpack_serde")]
|
||||
pub use msgpack_serde::*;
|
||||
#[cfg(feature = "prost")]
|
||||
pub use prost::*;
|
|
@ -1,45 +0,0 @@
|
|||
use crate::utils::{Decoder, Encoder};
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
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,108 +0,0 @@
|
|||
use crate::utils::{Decoder, Encoder};
|
||||
use thiserror::Error;
|
||||
|
||||
pub trait IsBinary<T, E: ?Sized> {
|
||||
fn is_binary() -> bool;
|
||||
}
|
||||
|
||||
impl<D, T> IsBinary<T, [u8]> for D
|
||||
where
|
||||
D: Decoder<T, Encoded = [u8]>,
|
||||
{
|
||||
fn is_binary() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, T> IsBinary<T, str> for D
|
||||
where
|
||||
D: Decoder<T, Encoded = str>,
|
||||
{
|
||||
fn is_binary() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HybridCoderError<E> {
|
||||
#[error("Not implemented: {0}")]
|
||||
NotImplemented(&'static str),
|
||||
#[error("Decoding error")]
|
||||
Coder(#[from] E),
|
||||
}
|
||||
|
||||
pub trait HybridDecoder<T, E: ?Sized> {
|
||||
type Error;
|
||||
|
||||
fn decode_str(_val: &str) -> Result<T, HybridCoderError<Self::Error>> {
|
||||
Err(HybridCoderError::NotImplemented(
|
||||
"You're trying to decode from a string. This codec is binary.",
|
||||
))
|
||||
}
|
||||
|
||||
fn decode_bin(_val: &[u8]) -> Result<T, HybridCoderError<Self::Error>> {
|
||||
Err(HybridCoderError::NotImplemented(
|
||||
"You're trying to decode from a byte slice. This codec is a string codec.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, D> HybridDecoder<T, [u8]> for D
|
||||
where
|
||||
D: Decoder<T, Encoded = [u8]>,
|
||||
{
|
||||
type Error = D::Error;
|
||||
|
||||
fn decode_bin(val: &[u8]) -> Result<T, HybridCoderError<Self::Error>> {
|
||||
Ok(D::decode(val)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, D> HybridDecoder<T, str> for D
|
||||
where
|
||||
D: Decoder<T, Encoded = str>,
|
||||
{
|
||||
type Error = D::Error;
|
||||
|
||||
fn decode_str(val: &str) -> Result<T, HybridCoderError<Self::Error>> {
|
||||
Ok(D::decode(val)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HybridEncoder<T, E> {
|
||||
type Error;
|
||||
|
||||
fn encode_str(_val: &T) -> Result<String, HybridCoderError<Self::Error>> {
|
||||
Err(HybridCoderError::NotImplemented(
|
||||
"You're trying to encode into a string. This codec is binary.",
|
||||
))
|
||||
}
|
||||
|
||||
fn encode_bin(_val: &T) -> Result<Vec<u8>, HybridCoderError<Self::Error>> {
|
||||
Err(HybridCoderError::NotImplemented(
|
||||
"You're trying to encode into a byte vec. This codec is a string codec.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> HybridEncoder<T, Vec<u8>> for E
|
||||
where
|
||||
E: Encoder<T, Encoded = Vec<u8>>,
|
||||
{
|
||||
type Error = E::Error;
|
||||
|
||||
fn encode_bin(val: &T) -> Result<Vec<u8>, HybridCoderError<Self::Error>> {
|
||||
Ok(E::encode(val)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> HybridEncoder<T, String> for E
|
||||
where
|
||||
E: Encoder<T, Encoded = String>,
|
||||
{
|
||||
type Error = E::Error;
|
||||
|
||||
fn encode_str(val: &T) -> Result<String, HybridCoderError<Self::Error>> {
|
||||
Ok(E::encode(val)?)
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
mod bin;
|
||||
mod hybrid;
|
||||
mod string;
|
||||
|
||||
pub use bin::*;
|
||||
pub use hybrid::*;
|
||||
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>;
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CodecError<E, D> {
|
||||
#[error("failed to encode: {0}")]
|
||||
Encode(E),
|
||||
#[error("failed to decode: {0}")]
|
||||
Decode(D),
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
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(Base64DecodeError::Decoder)
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
use crate::utils::{Decoder, Encoder};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// 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
|
||||
/// simply data structures without depending on third party crates like serde and serde_json.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions};
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// #
|
||||
/// # pub fn Demo() -> impl IntoView {
|
||||
/// let (get, set, remove) = use_local_storage::<i32, FromToStringCodec>("my-key");
|
||||
/// # view! { }
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct FromToStringCodec;
|
||||
|
||||
impl<T: ToString> Encoder<T> for FromToStringCodec {
|
||||
type Error = ();
|
||||
type Encoded = String;
|
||||
|
||||
fn encode(val: &T) -> Result<String, Self::Error> {
|
||||
Ok(val.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_string_codec() {
|
||||
let s = String::from("party time 🎉");
|
||||
assert_eq!(FromToStringCodec::encode(&s), Ok(s.clone()));
|
||||
assert_eq!(FromToStringCodec::decode(&s), Ok(s));
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
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,13 +0,0 @@
|
|||
#[cfg(feature = "base64")]
|
||||
mod base64;
|
||||
mod from_to_string;
|
||||
#[cfg(feature = "json_serde")]
|
||||
mod json_serde;
|
||||
mod option;
|
||||
|
||||
#[cfg(feature = "base64")]
|
||||
pub use base64::*;
|
||||
pub use from_to_string::*;
|
||||
#[cfg(feature = "json_serde")]
|
||||
pub use json_serde::*;
|
||||
pub use option::*;
|
|
@ -1,45 +0,0 @@
|
|||
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,4 +1,3 @@
|
|||
mod codecs;
|
||||
mod filters;
|
||||
mod is;
|
||||
mod js;
|
||||
|
@ -7,7 +6,6 @@ mod pausable;
|
|||
mod signal_filtered;
|
||||
mod use_derive_signal;
|
||||
|
||||
pub use codecs::*;
|
||||
pub use filters::*;
|
||||
pub use is::*;
|
||||
pub(crate) use js_value_from_to_string::*;
|
||||
|
|
Loading…
Add table
Reference in a new issue