From f6c1e42b8e395732e3df171f4163331d5a6a813d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Tue, 31 Dec 2019 03:43:10 +0100 Subject: [PATCH] update readme, add option to clear all expired --- Cargo.toml | 5 +--- README.md | 79 +++++++++++++++++++++++++++++++++++++++++++++----- src/session.rs | 54 ++++++++++++++++++++++++++-------- 3 files changed, 114 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 556703e..a012dfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,6 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = { version = "1.0", features = ["derive"] } -serde_json = { version="1.0", features= ["preserve_order"] } -json_dotpath = "0.1.2" rand = "0.7.2" -rocket = { version="0.4.2", default-features = false} +rocket = "0.4.2" parking_lot = "0.10.0" diff --git a/README.md b/README.md index 5b7a343..c215d3a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,27 @@ # Sessions for Rocket.rs -Adding cookie-based sessions to a rocket application is extremely simple: +Adding cookie-based sessions to a rocket application is extremely simple with this crate. + +The implementation is generic to support any type as session data: a custom struct, `String`, +`HashMap`, or perhaps `serde_json::Value`. You're free to choose. + +The session expiry time is configurable through the Fairing. When a session expires, +the data associated with it is dropped. + +## Basic example + +This simple example uses u64 as the session variable; note that it can be a struct, map, or anything else, +it just needs to implement `Send + Sync + Default`. ```rust #![feature(proc_macro_hygiene, decl_macro)] #[macro_use] extern crate rocket; -use rocket_session::Session; use std::time::Duration; +// It's convenient to define a type alias: +pub type Session<'a> = rocket_session::Session<'a, u64>; + fn main() { rocket::ignite() .attach(Session::fairing(Duration::from_secs(3600))) @@ -18,16 +31,66 @@ fn main() { #[get("/")] fn index(session: Session) -> String { - let mut count: usize = session.get_or_default("count"); - count += 1; - session.set("count", count); + let count = session.tap(|n| { + // Change the stored value (it is &mut) + *n += 1; + + // Return something to the caller. + // This can be any type, 'tap' is generic. + *n + }); format!("{} visits", count) } ``` -Anything serializable can be stored in the session, just make sure to unpack it to the right type. +## Extending by a trait + +The `tap` method is powerful, but sometimes you may wish for something more convenient. + +Here is an example of using a custom trait and the `json_dotpath` crate to implement +a polymorphic store based on serde serialization: + +```rust +use serde_json::Value; +use serde::de::DeserializeOwned; +use serde::Serialize; +use json_dotpath::DotPaths; + +pub type Session<'a> = rocket_session::Session<'a, serde_json::Map>; + +pub trait SessionAccess { + fn get(&self, path: &str) -> Option; -The session driver internally uses `serde_json::Value` and the `json_dotpath` crate. -Therefore, it's possible to use dotted paths and store the session data in a more structured way. + fn take(&self, path: &str) -> Option; + + fn replace(&self, path: &str, new: N) -> Option; + + fn set(&self, path: &str, value: T); + + fn remove(&self, path: &str) -> bool; +} + +impl<'a> SessionAccess for Session<'a> { + fn get(&self, path: &str) -> Option { + self.tap(|data| data.dot_get(path)) + } + + fn take(&self, path: &str) -> Option { + self.tap(|data| data.dot_take(path)) + } + + fn replace(&self, path: &str, new: N) -> Option { + self.tap(|data| data.dot_replace(path, new)) + } + + fn set(&self, path: &str, value: T) { + self.tap(|data| data.dot_set(path, value)); + } + + fn remove(&self, path: &str) -> bool { + self.tap(|data| data.dot_remove(path)) + } +} +``` diff --git a/src/session.rs b/src/session.rs index 86892a7..583a011 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,20 +1,20 @@ use parking_lot::RwLock; use rand::Rng; -use rocket::fairing::{self, Fairing, Info}; -use rocket::request::FromRequest; use rocket::{ + fairing::{self, Fairing, Info}, http::{Cookie, Status}, + request::FromRequest, Outcome, Request, Response, Rocket, State, }; -use serde::export::PhantomData; use std::collections::HashMap; +use std::marker::PhantomData; use std::ops::Add; use std::time::{Duration, Instant}; const SESSION_COOKIE: &str = "SESSID"; -const SESSION_ID_LEN : usize = 16; +const SESSION_ID_LEN: usize = 16; /// Session, as stored in the sessions store #[derive(Debug)] @@ -30,7 +30,7 @@ where /// Session store (shared state) #[derive(Default, Debug)] -struct SessionStore +pub struct SessionStore where D: 'static + Sync + Send + Default, { @@ -40,6 +40,17 @@ where lifespan: Duration, } +impl SessionStore +where + D: 'static + Sync + Send + Default, +{ + /// Remove all expired sessions + pub fn remove_expired(&self) { + let now = Instant::now(); + self.inner.write().retain(|_k, v| v.expires > now); + } +} + /// Session ID newtype for rocket's "local_cache" #[derive(PartialEq, Hash, Clone, Debug)] struct SessionID(String); @@ -112,16 +123,40 @@ where } } + /// Access the session store + pub fn get_store(&self) -> &SessionStore { + &self.store + } + + /// Set the session object to its default state + pub fn reset(&self) { + self.tap(|m| { + *m = D::default(); + }) + } + + /// Renew the session without changing any data + pub fn renew(&self) { + self.tap(|_| ()) + } + /// Run a closure with a mutable reference to the session object. /// The closure's return value is send to the caller. pub fn tap(&self, func: impl FnOnce(&mut D) -> T) -> T { let mut wg = self.store.inner.write(); if let Some(instance) = wg.get_mut(&self.id.0) { + // wipe session data if expired + if instance.expires <= Instant::now() { + instance.data = D::default(); + } + // update expiry timestamp instance.expires = Instant::now().add(self.store.lifespan); + func(&mut instance.data) } else { + // no object in the store yet, start fresh let mut data = D::default(); - let rv = func(&mut data); + let result = func(&mut data); wg.insert( self.id.0.clone(), SessionInstance { @@ -129,14 +164,9 @@ where expires: Instant::now().add(self.store.lifespan), }, ); - rv + result } } - - /// Renew the session - pub fn renew(&self) { - self.tap(|_| ()) - } } /// Fairing struct