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, AssetId, CommandId, EmptyId, InputId, InstanceId, InteractionId, InviteId,
15 NodeId, ProfileRevisionId, ReleaseId, RequestId, RevisionId, RunId, SessionId, SpaceId,
16 SubjectId, TenantId, ToolCallId,
17};
18
19#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22pub struct RequestContext {
23 pub tenant_id: TenantId,
25 pub subject_id: SubjectId,
27 pub roles: BTreeSet<String>,
29 pub locale: String,
31 pub request_id: RequestId,
33 pub entitlements: BTreeSet<String>,
35}
36
37impl RequestContext {
38 pub fn validate(&self) -> Result<(), ContextError> {
40 for (name, value) in [
41 ("tenant_id", self.tenant_id.as_str()),
42 ("subject_id", self.subject_id.as_str()),
43 ("request_id", self.request_id.as_str()),
44 ] {
45 if value.trim().is_empty() {
46 return Err(ContextError::Missing(name));
47 }
48 }
49 Ok(())
50 }
51
52 pub fn durable_identity(&self) -> Self {
55 Self {
56 tenant_id: self.tenant_id.clone(),
57 subject_id: self.subject_id.clone(),
58 roles: BTreeSet::new(),
59 locale: self.locale.clone(),
60 request_id: self.request_id.clone(),
61 entitlements: BTreeSet::new(),
62 }
63 }
64}
65
66#[derive(Debug, Clone, Default, PartialEq, Eq)]
68pub struct RequestMetadata(pub BTreeMap<String, String>);
69
70#[derive(Debug, thiserror::Error, PartialEq, Eq)]
72pub enum ContextError {
73 #[error("missing request context field '{0}'")]
75 Missing(&'static str),
76 #[error("request context rejected: {0}")]
78 Rejected(String),
79 #[error("request context dependency unavailable: {0}")]
81 Unavailable(String),
82}
83
84#[async_trait]
87pub trait PlatformContextProvider: Send + Sync {
88 async fn resolve(&self, metadata: &RequestMetadata) -> Result<RequestContext, ContextError>;
90}
91
92#[async_trait]
94pub trait ScheduledContextProvider: Send + Sync {
95 async fn resolve_scheduled(
97 &self,
98 tenant_id: &TenantId,
99 subject_id: &SubjectId,
100 request_id: &RequestId,
101 locale: &str,
102 ) -> Result<RequestContext, ContextError>;
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn identity_fields_cannot_be_blank() {
111 assert_eq!(SubjectId::try_from(""), Err(EmptyId("SubjectId")));
112 let context = RequestContext {
113 tenant_id: "tenant".parse().unwrap(),
114 subject_id: "subject".parse().unwrap(),
115 roles: BTreeSet::new(),
116 locale: "en".into(),
117 request_id: "request".parse().unwrap(),
118 entitlements: BTreeSet::new(),
119 };
120 assert_eq!(context.validate(), Ok(()));
121 assert_eq!(context.durable_identity().tenant_id, "tenant");
122 }
123}