use std::collections::{BTreeMap, BTreeSet};
use async_trait::async_trait;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RequestContext {
pub tenant_id: String,
pub subject_id: String,
pub roles: BTreeSet<String>,
pub locale: String,
pub request_id: String,
pub entitlements: BTreeSet<String>,
}
impl RequestContext {
pub fn validate(&self) -> Result<(), ContextError> {
for (name, value) in [
("tenant_id", self.tenant_id.as_str()),
("subject_id", self.subject_id.as_str()),
("request_id", self.request_id.as_str()),
] {
if value.trim().is_empty() {
return Err(ContextError::Missing(name));
}
}
Ok(())
}
pub fn durable_identity(&self) -> Self {
Self {
tenant_id: self.tenant_id.clone(),
subject_id: self.subject_id.clone(),
roles: BTreeSet::new(),
locale: self.locale.clone(),
request_id: self.request_id.clone(),
entitlements: BTreeSet::new(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RequestMetadata(pub BTreeMap<String, String>);
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ContextError {
#[error("missing request context field '{0}'")]
Missing(&'static str),
#[error("request context rejected: {0}")]
Rejected(String),
#[error("request context dependency unavailable: {0}")]
Unavailable(String),
}
#[async_trait]
pub trait PlatformContextProvider: Send + Sync {
async fn resolve(&self, metadata: &RequestMetadata) -> Result<RequestContext, ContextError>;
}
#[async_trait]
pub trait ScheduledContextProvider: Send + Sync {
async fn resolve_scheduled(
&self,
tenant_id: &str,
subject_id: &str,
request_id: &str,
locale: &str,
) -> Result<RequestContext, ContextError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_missing_identity_fields() {
let context = RequestContext {
tenant_id: "tenant".into(),
subject_id: "".into(),
roles: BTreeSet::new(),
locale: "en".into(),
request_id: "request".into(),
entitlements: BTreeSet::new(),
};
assert_eq!(context.validate(), Err(ContextError::Missing("subject_id")));
}
}