Skip to content

Commit

Permalink
demonstrate strongly-typed buckets pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
maxcountryman committed Sep 22, 2023
1 parent 748450a commit 49e3d95
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,7 @@ required-features = ["axum-core", "sqlite-store", "tokio"]
[[example]]
name = "postgres-store"
required-features = ["axum-core", "postgres-store", "tokio"]

[[example]]
name = "strongly-typed"
required-features = ["axum-core", "memory-store"]
148 changes: 148 additions & 0 deletions examples/strongly-typed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use std::{fmt::Display, net::SocketAddr};

use async_trait::async_trait;
use axum::{
error_handling::HandleErrorLayer, extract::FromRequestParts, response::IntoResponse,
routing::get, BoxError, Router,
};
use http::{request::Parts, StatusCode};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use tower::ServiceBuilder;
use tower_sessions::{time::Duration, MemoryStore, Session, SessionManagerLayer};
use uuid::Uuid;

#[derive(Clone, Deserialize, Serialize)]
struct GuestData {
id: Uuid,
pageviews: usize,
first_seen: OffsetDateTime,
last_seen: OffsetDateTime,
}

impl Default for GuestData {
fn default() -> Self {
Self {
id: Uuid::new_v4(),
pageviews: 0,
first_seen: OffsetDateTime::now_utc(),
last_seen: OffsetDateTime::now_utc(),
}
}
}

struct Guest {
session: Session,
guest_data: GuestData,
}

impl Guest {
const GUEST_DATA_KEY: &'static str = "guest_data";

fn id(&self) -> Uuid {
self.guest_data.id
}

fn first_seen(&self) -> OffsetDateTime {
self.guest_data.first_seen
}

fn last_seen(&self) -> OffsetDateTime {
self.guest_data.last_seen
}

fn pageviews(&self) -> usize {
self.guest_data.pageviews
}

fn mark_pageview(&mut self) {
self.guest_data.pageviews += 1;
self.update_session()
}

fn update_session(&self) {
self.session
.insert(Self::GUEST_DATA_KEY, self.guest_data.clone())
.expect("infallible")
}
}

impl Display for Guest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let now = OffsetDateTime::now_utc();
write!(
f,
r#"
Guest ID {}
Pageviews {}
First seen {} ago
Last seen {} ago
"#,
self.id().as_hyphenated(),
self.pageviews(),
now - self.first_seen(),
now - self.last_seen()
)
}
}

#[async_trait]
impl<S> FromRequestParts<S> for Guest
where
S: Send + Sync,
{
type Rejection = (StatusCode, &'static str);

async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let session = Session::from_request_parts(req, state).await?;

let mut guest_data: GuestData = session
.get(Self::GUEST_DATA_KEY)
.expect("infallible")
.unwrap_or_default();

guest_data.last_seen = OffsetDateTime::now_utc();
session
.insert(Self::GUEST_DATA_KEY, guest_data.clone())
.expect("infallible");

Ok(Guest {
session,
guest_data,
})
}
}

#[tokio::main]
async fn main() {
let session_store = MemoryStore::default();
let session_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::BAD_REQUEST
}))
.layer(
SessionManagerLayer::new(session_store)
.with_secure(false)
.with_max_age(Duration::seconds(10)),
);

let app = Router::new()
.route("/", get(handler))
.layer(session_service);

let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}

// This demonstrates a `Guest` extractor, but we could have any number of
// namespaced, strongly-typed "buckets" like `Guest` in the same session.
//
// Use cases could include buckets for site preferences, analytics,
// feature flags, etc.
async fn handler(mut guest: Guest) -> impl IntoResponse {
guest.mark_pageview();
format!("{}", guest)
}

0 comments on commit 49e3d95

Please sign in to comment.