1#![deny(missing_docs)]
5#![deny(rustdoc::broken_intra_doc_links)]
6
7use std::collections::{BTreeMap, BTreeSet};
8
9use async_trait::async_trait;
10
11pub mod ids;
12pub mod testing;
13pub use ids::{
14 ActionIntentId, EmptyId, InputId, InstanceId, InteractionId, NodeId, ProfileRevisionId,
15 RequestId, RunId, SessionId, SpaceId, SubjectId, TenantId, ToolCallId,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
21pub struct RequestContext {
22 pub tenant_id: TenantId,
24 pub subject_id: SubjectId,
26 pub roles: BTreeSet<String>,
28 pub locale: String,
30 pub request_id: RequestId,
32 pub entitlements: BTreeSet<String>,
34}
35
36impl RequestContext {
37 pub fn validate(&self) -> Result<(), ContextError> {
39 for (name, value) in [
40 ("tenant_id", self.tenant_id.as_str()),
41 ("subject_id", self.subject_id.as_str()),
42 ("request_id", self.request_id.as_str()),
43 ] {
44 if value.trim().is_empty() {
45 return Err(ContextError::Missing(name));
46 }
47 }
48 Ok(())
49 }
50
51 pub fn durable_identity(&self) -> Self {
54 Self {
55 tenant_id: self.tenant_id.clone(),
56 subject_id: self.subject_id.clone(),
57 roles: BTreeSet::new(),
58 locale: self.locale.clone(),
59 request_id: self.request_id.clone(),
60 entitlements: BTreeSet::new(),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Default, PartialEq, Eq)]
67pub struct RequestMetadata(pub BTreeMap<String, String>);
68
69#[derive(Debug, thiserror::Error, PartialEq, Eq)]
71pub enum ContextError {
72 #[error("missing request context field '{0}'")]
74 Missing(&'static str),
75 #[error("request context rejected: {0}")]
77 Rejected(String),
78 #[error("request context dependency unavailable: {0}")]
80 Unavailable(String),
81}
82
83#[async_trait]
86pub trait PlatformContextProvider: Send + Sync {
87 async fn resolve(&self, metadata: &RequestMetadata) -> Result<RequestContext, ContextError>;
89}
90
91#[async_trait]
93pub trait ScheduledContextProvider: Send + Sync {
94 async fn resolve_scheduled(
96 &self,
97 tenant_id: &TenantId,
98 subject_id: &SubjectId,
99 request_id: &RequestId,
100 locale: &str,
101 ) -> Result<RequestContext, ContextError>;
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn identity_fields_cannot_be_blank() {
110 assert_eq!(SubjectId::try_from(""), Err(EmptyId("SubjectId")));
111 let context = RequestContext {
112 tenant_id: "tenant".parse().unwrap(),
113 subject_id: "subject".parse().unwrap(),
114 roles: BTreeSet::new(),
115 locale: "en".into(),
116 request_id: "request".parse().unwrap(),
117 entitlements: BTreeSet::new(),
118 };
119 assert_eq!(context.validate(), Ok(()));
120 assert_eq!(context.durable_identity().tenant_id, "tenant");
121 }
122}