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