From faba9b58b2b4d96f7e5815d1d97865d204d21873 Mon Sep 17 00:00:00 2001 From: fa-sharp Date: Mon, 8 Sep 2025 00:22:06 -0400 Subject: [PATCH 1/5] use builder pattern using bon --- Cargo.lock | 87 ++++++++++++++++++++++++++-- Cargo.toml | 1 + src/fairing.rs | 70 +++++++--------------- src/storage/redis/base.rs | 38 +++++------- src/storage/redis/storage_indexed.rs | 26 ++++----- src/storage/sqlx/postgres.rs | 47 ++++++++------- tests/storages_basic.rs | 19 ++++-- tests/storages_indexed.rs | 13 ++++- 8 files changed, 182 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f2bd82..50cabdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -216,6 +216,31 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2529c31017402be841eb45892278a6c21a000c0a17643af326c73a73f83f0fb" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82020dadcb845a345591863adb65d74fa8dc5c18a0b6d408470e13b7adc7005" +dependencies = [ + "darling 0.21.3", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.103", +] + [[package]] name = "bytemuck" version = "1.23.1" @@ -378,8 +403,18 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -392,21 +427,46 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 1.0.109", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.103", +] + [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", + "darling_core 0.13.4", "quote", "syn 1.0.109", ] +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.103", +] + [[package]] name = "der" version = "0.7.10" @@ -1595,6 +1655,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn 2.0.103", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -1850,6 +1920,7 @@ dependencies = [ name = "rocket_flex_session" version = "0.1.3" dependencies = [ + "bon", "fred", "rand 0.9.2", "retainer", @@ -1911,7 +1982,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de9519ac276544ae734c067b57745cc1a0dc9506f3a7625918e89babffd9b101" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro2", "quote", "rocket_http", @@ -2412,6 +2483,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index cf08ec4..45be0d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] +bon = "3.7.2" fred = { version = "10.1", optional = true, default-features = false, features = [ "i-keys", "i-hashes", diff --git a/src/fairing.rs b/src/fairing.rs index 7a31978..3069260 100644 --- a/src/fairing.rs +++ b/src/fairing.rs @@ -3,6 +3,7 @@ use std::{ sync::{Arc, Mutex}, }; +use bon::Builder; use rocket::{fairing::Fairing, Build, Orbit, Request, Response, Rocket}; use crate::{ @@ -52,24 +53,21 @@ fn rocket() -> _ { } ``` */ -#[derive(Clone)] -pub struct RocketFlexSession { +#[derive(Clone, Builder)] +pub struct RocketFlexSession { + /// Set the options directly. Alternatively, use `with_options` to customize the default options via a closure. + #[builder(default)] pub(crate) options: RocketFlexSessionOptions, + #[builder(default = Arc::new(MemoryStorage::default()), with = |storage: impl SessionStorage + 'static| Arc::new(storage))] + /// Set the session storage provider. The default is an in-memory storage. pub(crate) storage: Arc>, } -impl RocketFlexSession -where - T: Send + Sync + Clone + 'static, -{ - /// Build a session configuration - pub fn builder() -> RocketFlexSessionBuilder { - RocketFlexSessionBuilder::default() - } -} + impl Default for RocketFlexSession where T: Send + Sync + Clone + 'static, { + /// Create a new instance with default options and an in-memory storage. fn default() -> Self { Self { options: Default::default(), @@ -78,50 +76,24 @@ where } } -/// Builder to configure the [RocketFlexSession] fairing -pub struct RocketFlexSessionBuilder -where - T: Send + Sync + Clone + 'static, -{ - fairing: RocketFlexSession, -} -impl Default for RocketFlexSessionBuilder -where - T: Send + Sync + Clone + 'static, -{ - fn default() -> Self { - Self { - fairing: Default::default(), - } - } -} -impl RocketFlexSessionBuilder +use rocket_flex_session_builder::{IsUnset, SetOptions, State}; +impl RocketFlexSessionBuilder where T: Send + Sync + Clone + 'static, + S: State, { - /// Set the session options via a closure. If you're using a cookie-based storage - /// provider, make sure to set the corresponding cookie settings - /// in the storage configuration as well. - pub fn with_options(&mut self, options_fn: OptionsFn) -> &mut Self + /// Set the [options](RocketFlexSessionOptions) via a closure. Any options that are not set will be set to their default values. + pub fn with_options( + self, + options_fn: OptionsFn, + ) -> RocketFlexSessionBuilder> where + S::Options: IsUnset, OptionsFn: FnOnce(&mut RocketFlexSessionOptions), { - options_fn(&mut self.fairing.options); - self - } - - /// Set the session storage provider - pub fn storage(&mut self, storage: S) -> &mut Self - where - S: SessionStorage + 'static, - { - self.fairing.storage = Arc::new(storage); - self - } - - /// Build the fairing - pub fn build(&self) -> RocketFlexSession { - self.fairing.clone() + let mut options = RocketFlexSessionOptions::default(); + options_fn(&mut options); + self.options(options) } } diff --git a/src/storage/redis/base.rs b/src/storage/redis/base.rs index d5778f5..3140ec2 100644 --- a/src/storage/redis/base.rs +++ b/src/storage/redis/base.rs @@ -1,5 +1,6 @@ +use bon::Builder; use fred::{ - prelude::{HashesInterface, KeysInterface, Pool, Value}, + prelude::{HashesInterface, KeysInterface, Value}, types::Expiration, }; @@ -15,15 +16,14 @@ pub enum RedisType { /** Redis session storage using the [fred.rs](https://docs.rs/fred) crate. +# Requirements You can store the data as a Redis string or hash. Your session data type must implement [`FromValue`](https://docs.rs/fred/latest/fred/types/trait.FromValue.html) from the fred.rs crate, as well as the inverse `From` or `TryFrom` for [`Value`](https://docs.rs/fred/latest/fred/types/enum.Value.html) in order to dictate how the data will be converted to/from the Redis data type. - For Redis string types, convert to/from `Value::String` - For Redis hash types, convert to/from `Value::Map` -💡 Common hashmap types like `HashMap` are automatically supported - make sure to use `RedisType::Hash` -when constructing the storage to ensure they are properly converted and stored as Redis hashes. - +# Setup ```rust use fred::prelude::{Builder, ClientLike, Config, FromValue, Value}; use rocket_flex_session::{error::SessionError, storage::{redis::{RedisFredStorage, RedisType}}}; @@ -37,16 +37,16 @@ async fn setup_storage() -> RedisFredStorage { redis_pool.init().await.expect("Should initialize Redis pool"); // Construct the storage - let storage = RedisFredStorage::new( - redis_pool, - RedisType::String, // or RedisType::Hash - "sess:" // Prefix for Redis keys - ); + let storage = RedisFredStorage::builder() + .pool(redis_pool) + .prefix("sess:") // Prefix for Redis keys + .redis_type(RedisType::String) // or RedisType::Hash + .build(); storage } -// If using a custom struct for your session data, implement the following... +// Implement the following for your session data type... struct MySessionData { user_id: String, } @@ -67,26 +67,18 @@ impl From for Value { } ``` */ +#[derive(Builder)] pub struct RedisFredStorage { + /// The initialized fred.rs connection pool. pub(super) pool: fred::prelude::Pool, + /// The prefix to use for session keys. + #[builder(into, default = "sess:")] pub(super) prefix: String, + /// The Redis data type to use for storing sessions. pub(super) redis_type: RedisType, } impl RedisFredStorage { - /// Create the storage instance. - /// # Parameters - /// * `pool` - The initialized fred.rs connection pool. - /// * `redis_type` - The Redis data type to use for storing sessions. - /// * `key_prefix` - The prefix to use for session keys. (e.g. "sess:") - pub fn new(pool: Pool, redis_type: RedisType, key_prefix: &str) -> Self { - Self { - pool, - prefix: key_prefix.to_owned(), - redis_type, - } - } - pub(super) fn session_key(&self, id: &str) -> String { format!("{}{id}", self.prefix) } diff --git a/src/storage/redis/storage_indexed.rs b/src/storage/redis/storage_indexed.rs index 363799a..6c9fa7d 100644 --- a/src/storage/redis/storage_indexed.rs +++ b/src/storage/redis/storage_indexed.rs @@ -1,3 +1,4 @@ +use bon::Builder; use fred::prelude::{FromValue, HashesInterface, KeysInterface, SetsInterface, Value}; use rocket::http::CookieJar; @@ -14,29 +15,24 @@ const DEFAULT_INDEX_TTL: u32 = 60 * 60 * 24 * 7 * 2; // 2 weeks /// Redis session storage using the [fred.rs](https://docs.rs/fred) crate. This is a wrapper around /// [`RedisFredStorage`] that adds support for indexing sessions by an identifier (e.g. `user_id`). /// +/// # Requirements /// In addition to the requirements for `RedisFredStorage`, your session data type must /// implement [`SessionIdentifier`], and its [Id](`SessionIdentifier::Id`) type -/// must implement `ToString`. Sessions are tracked in Redis sets, with a key format of -/// `:`. e.g.: `sess:user_id:1` +/// must implement `ToString`. Sessions are tracked in Redis sets, with a key format of: +/// +/// `:` (e.g.: `sess:user_id:1`) +#[derive(Builder)] +#[builder(start_fn = from_storage)] pub struct RedisFredStorageIndexed { + #[builder(start_fn)] + /// The [`RedisFredStorage`] instance to use. base_storage: RedisFredStorage, + #[builder(default = DEFAULT_INDEX_TTL)] + /// The TTL for the session index - should match your longest expected session duration (default: 2 weeks). index_ttl: u32, } impl RedisFredStorageIndexed { - /// Create the indexed storage. - /// - /// # Parameters: - /// - `base_storage`: The [`RedisFredStorage`] instance to use. - /// - `index_ttl`: The TTL for the session index - should match - /// your longest expected session duration (default: 2 weeks). - pub fn new(base_storage: RedisFredStorage, index_ttl: Option) -> Self { - Self { - base_storage, - index_ttl: index_ttl.unwrap_or(DEFAULT_INDEX_TTL), - } - } - fn session_index_key(&self, identifier_name: &str, identifier: &impl ToString) -> String { format!( "{}{identifier_name}:{}", diff --git a/src/storage/sqlx/postgres.rs b/src/storage/sqlx/postgres.rs index e54f2e5..fa6de62 100644 --- a/src/storage/sqlx/postgres.rs +++ b/src/storage/sqlx/postgres.rs @@ -1,3 +1,4 @@ +use bon::Builder; use rocket::{ async_trait, http::CookieJar, @@ -23,6 +24,7 @@ const EXPIRES_COLUMN: &str = "expires"; /** Session store using PostgreSQL via [sqlx](https://docs.rs/crate/sqlx) that stores session data as a string, and supports session indexing. +# Requirements You'll need to implement `TryInto` and `TryFrom` for your session data type. You'll also need to implement [`SessionIdentifier`], and its [`Id`](crate::SessionIdentifier::Id) must be a [type supported by sqlx](https://docs.rs/sqlx/latest/sqlx/postgres/types/index.html). Expects a table to already exist with the following columns: @@ -33,35 +35,40 @@ Expects a table to already exist with the following columns: | data | `text` NOT NULL (or `jsonb` if using JSON) | | `` | `` (the name and type should match your [`SessionIdentifier`] impl) | | expires | `timestamptz` NOT NULL | + +# Creating the storage +Initialize the sqlx pool, then use the builder pattern to create a new instance of `SqlxPostgresStorage`: +``` +use rocket_flex_session::storage::sqlx::SqlxPostgresStorage; +use std::time::Duration; + +async fn create_storage() -> SqlxPostgresStorage { + let url = "postgres://..."; + let pool = sqlx::PgPool::connect(url).await.unwrap(); + SqlxPostgresStorage::builder() + .pool(pool.clone()) + .table_name("sessions") + // optional auto-deletion of expired sessions + .cleanup_interval(Duration::from_secs(600)) + .build() +} +``` */ +#[derive(Builder)] pub struct SqlxPostgresStorage { + /// An initialized Postgres connection pool. pool: PgPool, + /// The name of the table to use for storing sessions. + #[builder(into)] table_name: String, + /// Interval to check for and delete expired sessions. If not set, + /// expired sessions won't be cleaned up automatically. cleanup_interval: Option, + #[builder(skip)] shutdown_tx: Mutex>>, } impl SqlxPostgresStorage { - /// Creates a new [`SqlxPostgresStorage`]. - /// - /// Parameters: - /// - `pool`: An initialized Postgres connection pool. - /// - `table_name`: The name of the table to use for storing sessions. - /// - `cleanup_interval`: Interval to check for and clean up expired sessions. If `None`, - /// expired sessions won't be cleaned up automatically. - pub fn new( - pool: PgPool, - table_name: &str, - cleanup_interval: Option, - ) -> SqlxPostgresStorage { - Self { - pool, - table_name: table_name.to_owned(), - cleanup_interval, - shutdown_tx: Mutex::default(), - } - } - fn id_from_row(&self, row: &PgRow) -> sqlx::Result { row.try_get(ID_COLUMN) } diff --git a/tests/storages_basic.rs b/tests/storages_basic.rs index 1f21a52..60fcc8c 100644 --- a/tests/storages_basic.rs +++ b/tests/storages_basic.rs @@ -102,7 +102,11 @@ async fn create_rocket( ), "redis" => { let (pool, prefix) = setup_redis_fred().await; - let storage = RedisFredStorage::new(pool.clone(), RedisType::String, &prefix); + let storage = RedisFredStorage::builder() + .pool(pool.clone()) + .redis_type(RedisType::String) + .prefix(&prefix) + .build(); let fairing = RocketFlexSession::::builder() .storage(storage) .build(); @@ -111,8 +115,12 @@ async fn create_rocket( } "redis_indexed" => { let (pool, prefix) = setup_redis_fred().await; - let base_storage = RedisFredStorage::new(pool.clone(), RedisType::String, &prefix); - let storage = RedisFredStorageIndexed::new(base_storage, None); + let base_storage = RedisFredStorage::builder() + .pool(pool.clone()) + .redis_type(RedisType::String) + .prefix(&prefix) + .build(); + let storage = RedisFredStorageIndexed::from_storage(base_storage).build(); let fairing = RocketFlexSession::::builder() .storage(storage) .build(); @@ -121,7 +129,10 @@ async fn create_rocket( } "sqlx" => { let (pool, db_name) = setup_postgres(POSTGRES_URL).await; - let storage = SqlxPostgresStorage::new(pool.clone(), "sessions", None); + let storage = SqlxPostgresStorage::builder() + .pool(pool.clone()) + .table_name("sessions") + .build(); let fairing = RocketFlexSession::::builder() .storage(storage) .build(); diff --git a/tests/storages_indexed.rs b/tests/storages_indexed.rs index 3181827..2d6f48a 100644 --- a/tests/storages_indexed.rs +++ b/tests/storages_indexed.rs @@ -88,14 +88,21 @@ async fn create_storage( } "redis" => { let (pool, prefix) = setup_redis_fred().await; - let base_storage = RedisFredStorage::new(pool.clone(), RedisType::Hash, &prefix); - let storage = RedisFredStorageIndexed::new(base_storage, None); + let base_storage = RedisFredStorage::builder() + .pool(pool.clone()) + .prefix(&prefix) + .redis_type(RedisType::Hash) + .build(); + let storage = RedisFredStorageIndexed::from_storage(base_storage).build(); let cleanup_task = teardown_redis_fred(pool, prefix).boxed(); (Box::new(storage), Some(cleanup_task)) } "sqlx" => { let (pool, db_name) = setup_postgres(POSTGRES_URL).await; - let storage = SqlxPostgresStorage::new(pool.clone(), "sessions", None); + let storage = SqlxPostgresStorage::builder() + .pool(pool.clone()) + .table_name("sessions") + .build(); let cleanup_task = teardown_postgres(pool, db_name).boxed(); (Box::new(storage), Some(cleanup_task)) } From a87e51db45c0647debc555a1c7b23f2b081c6274 Mon Sep 17 00:00:00 2001 From: fa-sharp Date: Mon, 8 Sep 2025 00:23:05 -0400 Subject: [PATCH 2/5] Update fairing.rs --- src/fairing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fairing.rs b/src/fairing.rs index 3069260..66eebd3 100644 --- a/src/fairing.rs +++ b/src/fairing.rs @@ -82,7 +82,7 @@ where T: Send + Sync + Clone + 'static, S: State, { - /// Set the [options](RocketFlexSessionOptions) via a closure. Any options that are not set will be set to their default values. + /// Customize the [options](RocketFlexSessionOptions) via a closure. Any options that are not set will retain their default values. pub fn with_options( self, options_fn: OptionsFn, From 7f0554d100c9bfb09f016206e8d3938699c91ae0 Mon Sep 17 00:00:00 2001 From: fa-sharp Date: Mon, 8 Sep 2025 00:28:56 -0400 Subject: [PATCH 3/5] tweaks --- src/fairing.rs | 2 +- tests/session_indexed.rs | 37 +++++++++++++++++-------------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/fairing.rs b/src/fairing.rs index 66eebd3..3d69a5c 100644 --- a/src/fairing.rs +++ b/src/fairing.rs @@ -53,7 +53,7 @@ fn rocket() -> _ { } ``` */ -#[derive(Clone, Builder)] +#[derive(Builder)] pub struct RocketFlexSession { /// Set the options directly. Alternatively, use `with_options` to customize the default options via a closure. #[builder(default)] diff --git a/tests/session_indexed.rs b/tests/session_indexed.rs index ef1b131..728ace8 100644 --- a/tests/session_indexed.rs +++ b/tests/session_indexed.rs @@ -132,26 +132,23 @@ async fn user_profile(session: Session<'_, UserSession>) -> String { fn rocket() -> Rocket { let user_storage = MemoryStorageIndexed::::default(); - - rocket::build() - .attach( - RocketFlexSession::::builder() - .storage(user_storage) - .build(), - ) - .mount( - "/", - routes![ - user_login, - get_user_sessions, - get_sessions_for_user, - invalidate_all_user_sessions, - invalidate_other_user_sessions, - invalidate_sessions_for_user, - get_user_session_ids, - user_profile, - ], - ) + let fairing = RocketFlexSession::::builder() + .storage(user_storage) + .build(); + + rocket::build().attach(fairing).mount( + "/", + routes![ + user_login, + get_user_sessions, + get_sessions_for_user, + invalidate_all_user_sessions, + invalidate_other_user_sessions, + invalidate_sessions_for_user, + get_user_session_ids, + user_profile, + ], + ) } #[cfg(test)] From b82f88c561d23d27eac815abb6fd917663902a39 Mon Sep 17 00:00:00 2001 From: fa-sharp Date: Mon, 8 Sep 2025 00:39:48 -0400 Subject: [PATCH 4/5] Update lib.rs --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 641ff57..fcc9c46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -307,7 +307,7 @@ mod session_inner; pub mod error; pub mod storage; -pub use fairing::{RocketFlexSession, RocketFlexSessionBuilder}; +pub use fairing::RocketFlexSession; pub use options::RocketFlexSessionOptions; pub use session::Session; pub use session_hash::SessionHashMap; From 272b85f41bb5c5934680550f4e1ffcd8c833a333 Mon Sep 17 00:00:00 2001 From: fa-sharp Date: Mon, 8 Sep 2025 00:58:21 -0400 Subject: [PATCH 5/5] Update postgres.rs --- src/storage/sqlx/postgres.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/storage/sqlx/postgres.rs b/src/storage/sqlx/postgres.rs index fa6de62..3c7b142 100644 --- a/src/storage/sqlx/postgres.rs +++ b/src/storage/sqlx/postgres.rs @@ -26,14 +26,14 @@ Session store using PostgreSQL via [sqlx](https://docs.rs/crate/sqlx) that store # Requirements You'll need to implement `TryInto` and `TryFrom` for your session data type. You'll also need to implement [`SessionIdentifier`], -and its [`Id`](crate::SessionIdentifier::Id) must be a [type supported by sqlx](https://docs.rs/sqlx/latest/sqlx/postgres/types/index.html). +and its [`Id`](crate::SessionIdentifier::Id) type must be a [type supported by sqlx](https://docs.rs/sqlx/latest/sqlx/postgres/types/index.html). Expects a table to already exist with the following columns: | Name | Type | |------|---------| | id | `text` PRIMARY KEY | | data | `text` NOT NULL (or `jsonb` if using JSON) | -| `` | `` (the name and type should match your [`SessionIdentifier`] impl) | +| `` | `` (this identifier and type should match your [`SessionIdentifier`] impl) | | expires | `timestamptz` NOT NULL | # Creating the storage