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