1use std::collections::{BTreeMap, BTreeSet};
2
3use async_trait::async_trait;
4
5#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
8pub struct RequestContext {
9 pub tenant_id: String,
10 pub subject_id: String,
11 pub roles: BTreeSet<String>,
12 pub locale: String,
13 pub request_id: String,
14 pub entitlements: BTreeSet<String>,
15}
16
17impl RequestContext {
18 pub fn validate(&self) -> Result<(), ContextError> {
19 for (name, value) in [
20 ("tenant_id", self.tenant_id.as_str()),
21 ("subject_id", self.subject_id.as_str()),
22 ("request_id", self.request_id.as_str()),
23 ] {
24 if value.trim().is_empty() {
25 return Err(ContextError::Missing(name));
26 }
27 }
28 Ok(())
29 }
30
31 pub fn durable_identity(&self) -> Self {
34 Self {
35 tenant_id: self.tenant_id.clone(),
36 subject_id: self.subject_id.clone(),
37 roles: BTreeSet::new(),
38 locale: self.locale.clone(),
39 request_id: self.request_id.clone(),
40 entitlements: BTreeSet::new(),
41 }
42 }
43}
44
45#[derive(Debug, Clone, Default, PartialEq, Eq)]
47pub struct RequestMetadata(pub BTreeMap<String, String>);
48
49#[derive(Debug, thiserror::Error, PartialEq, Eq)]
50pub enum ContextError {
51 #[error("missing request context field '{0}'")]
52 Missing(&'static str),
53 #[error("request context rejected: {0}")]
54 Rejected(String),
55 #[error("request context dependency unavailable: {0}")]
56 Unavailable(String),
57}
58
59#[async_trait]
60pub trait PlatformContextProvider: Send + Sync {
61 async fn resolve(&self, metadata: &RequestMetadata) -> Result<RequestContext, ContextError>;
62}
63
64#[async_trait]
65pub trait ScheduledContextProvider: Send + Sync {
66 async fn resolve_scheduled(
67 &self,
68 tenant_id: &str,
69 subject_id: &str,
70 request_id: &str,
71 locale: &str,
72 ) -> Result<RequestContext, ContextError>;
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn rejects_missing_identity_fields() {
81 let context = RequestContext {
82 tenant_id: "tenant".into(),
83 subject_id: "".into(),
84 roles: BTreeSet::new(),
85 locale: "en".into(),
86 request_id: "request".into(),
87 entitlements: BTreeSet::new(),
88 };
89 assert_eq!(context.validate(), Err(ContextError::Missing("subject_id")));
90 }
91}