#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
use std::collections::{BTreeMap, BTreeSet};
use async_trait::async_trait;
pub mod ids;
pub mod testing;
pub use ids::{
ActionCandidateId, ActionIntentId, AssetId, BacktestDefinitionId, BacktestRunId, BranchId,
CapabilityId, CommandId, DeadLetterId, DocsCollaborationSessionId, EmptyId, InputId,
InstanceId, InteractionId, InviteId, MemoryId, MeteringCorrectionId, MigrationId,
MigrationRecordId, NewsIngestRunId, NewsItemId, NewsSourceBindingId, NodeId,
NotificationAttemptId, NotificationId, ProfileDraftId, ProfileRevisionId, ProviderAttemptId,
ReleaseId, RequestId, RevisionId, RunId, SessionId, SessionSnapshotId, SpaceId, SubjectId,
TenantId, ToolCallId, TranslationRequestId, UsageReservationId, WorkflowDefinitionId,
WorkflowDraftId, WorkflowResourceId, WorkflowSourceId, WorkflowStepId,
};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RequestContext {
pub tenant_id: TenantId,
pub subject_id: SubjectId,
pub roles: BTreeSet<String>,
pub locale: String,
pub request_id: RequestId,
pub entitlements: BTreeSet<String>,
}
impl RequestContext {
pub fn validate(&self) -> Result<(), ContextError> {
for (name, value) in [
("tenant_id", self.tenant_id.as_str()),
("subject_id", self.subject_id.as_str()),
("request_id", self.request_id.as_str()),
] {
if value.trim().is_empty() {
return Err(ContextError::Missing(name));
}
}
Ok(())
}
pub fn durable_identity(&self) -> Self {
Self {
tenant_id: self.tenant_id.clone(),
subject_id: self.subject_id.clone(),
roles: BTreeSet::new(),
locale: self.locale.clone(),
request_id: self.request_id.clone(),
entitlements: BTreeSet::new(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RequestMetadata(pub BTreeMap<String, String>);
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ContextError {
#[error("missing request context field '{0}'")]
Missing(&'static str),
#[error("request context rejected: {0}")]
Rejected(String),
#[error("request context dependency unavailable: {0}")]
Unavailable(String),
}
#[async_trait]
pub trait PlatformContextProvider: Send + Sync {
async fn resolve(&self, metadata: &RequestMetadata) -> Result<RequestContext, ContextError>;
}
#[async_trait]
pub trait ScheduledContextProvider: Send + Sync {
async fn resolve_scheduled(
&self,
tenant_id: &TenantId,
subject_id: &SubjectId,
request_id: &RequestId,
locale: &str,
) -> Result<RequestContext, ContextError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_fields_cannot_be_blank() {
assert_eq!(SubjectId::try_from(""), Err(EmptyId("SubjectId")));
let context = RequestContext {
tenant_id: "tenant".parse().unwrap(),
subject_id: "subject".parse().unwrap(),
roles: BTreeSet::new(),
locale: "en".into(),
request_id: "request".parse().unwrap(),
entitlements: BTreeSet::new(),
};
assert_eq!(context.validate(), Ok(()));
assert_eq!(context.durable_identity().tenant_id, "tenant");
}
}