use std::collections::HashMap;
use std::sync::RwLock;
use axum::extract::{Path, State};
use axum::Json;
use serde::Serialize;
use uuid::Uuid;
use super::error::UhpFailure;
use super::sessions::Turn;
use super::state::UhpState;
#[derive(Debug, Default)]
pub struct Shares {
by_share: RwLock<HashMap<String, String>>,
by_session: RwLock<HashMap<String, String>>,
}
impl Shares {
pub fn mint(&self, session_id: &str) -> String {
if let Some(existing) = self.for_session(session_id) {
return existing;
}
let share_id = format!("share_{}", Uuid::new_v4().simple());
if let Ok(mut by_share) = self.by_share.write() {
by_share.insert(share_id.clone(), session_id.to_owned());
}
if let Ok(mut by_session) = self.by_session.write() {
by_session.insert(session_id.to_owned(), share_id.clone());
}
share_id
}
pub fn for_session(&self, session_id: &str) -> Option<String> {
self.by_session.read().ok()?.get(session_id).cloned()
}
pub fn session_of(&self, share_id: &str) -> Option<String> {
self.by_share.read().ok()?.get(share_id).cloned()
}
pub fn revoke(&self, session_id: &str) -> bool {
let share_id = self
.by_session
.write()
.map_or(None, |mut by_session| by_session.remove(session_id));
let Some(share_id) = share_id else {
return false;
};
if let Ok(mut by_share) = self.by_share.write() {
by_share.remove(&share_id);
}
true
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Share {
pub id: String,
pub object: &'static str,
pub session_id: String,
pub url: String,
}
fn share_path(share_id: &str) -> String {
format!("/share/{share_id}")
}
pub async fn create(
State(state): State<UhpState>,
Path(session_id): Path<String>,
) -> Result<Json<Share>, UhpFailure> {
if state.store.session_turns(&session_id).is_none() {
return Err(UhpFailure::not_found(
"session_not_found",
"no session with that id exists",
));
}
let id = state.shares.mint(&session_id);
Ok(Json(Share {
url: share_path(&id),
id,
object: "session.share",
session_id,
}))
}
pub async fn get(
State(state): State<UhpState>,
Path(session_id): Path<String>,
) -> Result<Json<Share>, UhpFailure> {
let id = state.shares.for_session(&session_id).ok_or_else(|| {
UhpFailure::not_found("share_not_found", "this session has no shared view")
})?;
Ok(Json(Share {
url: share_path(&id),
id,
object: "session.share",
session_id,
}))
}
pub async fn revoke(
State(state): State<UhpState>,
Path(session_id): Path<String>,
) -> Result<Json<super::sessions::Deleted>, UhpFailure> {
if state.shares.revoke(&session_id) {
Ok(Json(super::sessions::Deleted {
id: session_id,
object: "session.share.deleted",
deleted: true,
}))
} else {
Err(UhpFailure::not_found(
"share_not_found",
"this session has no shared view",
))
}
}
#[derive(Debug, Clone, Serialize)]
pub struct SharedView {
pub object: &'static str,
pub session_id: String,
pub turns: Vec<Turn>,
pub read_only: bool,
}
pub async fn view(
State(state): State<UhpState>,
Path(share_id): Path<String>,
) -> Result<Json<SharedView>, UhpFailure> {
let session_id = state.shares.session_of(&share_id).ok_or_else(|| {
UhpFailure::not_found("share_not_found", "this link does not open anything")
})?;
let turns = state.store.session_turns(&session_id).ok_or_else(|| {
UhpFailure::not_found("share_not_found", "this link does not open anything")
})?;
Ok(Json(SharedView {
object: "session.shared_view",
session_id,
turns: turns.iter().map(Turn::from).collect(),
read_only: true,
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minting_twice_returns_one_link() {
let shares = Shares::default();
let first = shares.mint("sess_1");
let second = shares.mint("sess_1");
assert_eq!(first, second);
assert!(first.starts_with("share_"));
}
#[test]
fn revocation_kills_the_link_itself() {
let shares = Shares::default();
let id = shares.mint("sess_1");
assert_eq!(shares.session_of(&id).as_deref(), Some("sess_1"));
assert!(shares.revoke("sess_1"));
assert!(
shares.session_of(&id).is_none(),
"a revoked link must stop opening for every holder, not just the minter"
);
assert!(shares.for_session("sess_1").is_none());
assert!(!shares.revoke("sess_1"), "revoking twice reports false");
}
#[test]
fn a_share_id_is_not_a_session_id() {
let shares = Shares::default();
let id = shares.mint("sess_1");
assert_ne!(id, "sess_1");
assert!(id.starts_with("share_"), "distinct namespace from sess_");
}
}