Skip to main content

platform_core/
context.rs

1use crate::clock::{Clock, SystemClock};
2use crate::config::{AppConfig, is_local_development_environment};
3use crate::db::DbPool;
4use crate::events::EventPublisher;
5use crate::execution_logs::{ExecutionLogProvider, PostgresExecutionLogProvider};
6use crate::health::HealthRegistry;
7use crate::ids::{IdGenerator, UuidGenerator};
8use crate::runtime_config::{RuntimeConfigProvider, StaticRuntimeConfigProvider};
9use crate::shutdown::Shutdown;
10use crate::telemetry_query::{NoopTelemetrySpanProvider, TelemetrySpanProvider};
11use serde::{Deserialize, Serialize};
12use std::fmt::Debug;
13use std::sync::Arc;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
16pub struct CorrelationId(pub String);
17
18impl CorrelationId {
19    pub fn new(value: impl Into<String>) -> Self {
20        Self(value.into())
21    }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
25pub struct RequestId(pub String);
26
27impl RequestId {
28    pub fn new(value: impl Into<String>) -> Self {
29        Self(value.into())
30    }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
34pub struct TenantId(pub String);
35
36#[derive(Debug, Clone, Default, Deserialize, Serialize)]
37pub struct TraceContext {
38    pub trace_id: Option<String>,
39    pub span_id: Option<String>,
40    pub baggage: Vec<(String, String)>,
41}
42
43#[derive(Debug, Clone, Deserialize, Serialize)]
44#[serde(tag = "kind", rename_all = "snake_case")]
45pub enum ActorContext {
46    Anonymous,
47    User {
48        user_id: String,
49        scopes: Vec<String>,
50    },
51    Service {
52        service_id: String,
53        scopes: Vec<String>,
54    },
55    System,
56}
57
58impl Default for ActorContext {
59    fn default() -> Self {
60        Self::Anonymous
61    }
62}
63
64#[derive(Debug, Clone, Default)]
65pub struct ActorResolutionRequest {
66    pub authorization: Option<String>,
67    pub cookie: Option<String>,
68}
69
70#[async_trait::async_trait]
71pub trait ActorResolver: Debug + Send + Sync {
72    async fn resolve_actor(&self, request: ActorResolutionRequest) -> ActorContext;
73}
74
75#[derive(Debug, Clone)]
76pub struct DevActorResolver {
77    environment: String,
78}
79
80impl DevActorResolver {
81    #[must_use]
82    pub fn new(environment: impl Into<String>) -> Self {
83        Self {
84            environment: environment.into(),
85        }
86    }
87}
88
89#[async_trait::async_trait]
90impl ActorResolver for DevActorResolver {
91    async fn resolve_actor(&self, request: ActorResolutionRequest) -> ActorContext {
92        let _ = request.cookie;
93        request
94            .authorization
95            .and_then(|value| parse_dev_bearer_actor(&value, &self.environment))
96            .unwrap_or_default()
97    }
98}
99
100fn parse_dev_bearer_actor(value: &str, environment: &str) -> Option<ActorContext> {
101    if !is_local_development_environment(environment) {
102        return None;
103    }
104
105    let token = value.strip_prefix("Bearer ")?;
106
107    if let Some(user_token) = token.strip_prefix("dev-user:") {
108        let (user_id, scopes) = parse_dev_actor_scopes(user_token);
109        return Some(ActorContext::User { user_id, scopes });
110    }
111
112    if let Some(service_token) = token.strip_prefix("dev-service:") {
113        let (service_id, scopes) = parse_dev_actor_scopes(service_token);
114        return Some(ActorContext::Service { service_id, scopes });
115    }
116
117    None
118}
119
120fn parse_dev_actor_scopes(value: &str) -> (String, Vec<String>) {
121    let Some((id, raw_scopes)) = value.split_once(':') else {
122        return (value.to_owned(), Vec::new());
123    };
124    let scopes = raw_scopes
125        .split(',')
126        .filter(|scope| !scope.is_empty())
127        .map(ToOwned::to_owned)
128        .collect();
129    (id.to_owned(), scopes)
130}
131
132#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
133#[serde(default)]
134pub struct ClientRequestMetadata {
135    pub ip: Option<String>,
136    pub user_agent: Option<String>,
137}
138
139impl ClientRequestMetadata {
140    #[must_use]
141    pub fn is_empty(&self) -> bool {
142        self.ip.is_none() && self.user_agent.is_none()
143    }
144}
145
146#[derive(Debug, Clone, Deserialize, Serialize)]
147pub struct RequestContext {
148    pub request_id: RequestId,
149    pub correlation_id: CorrelationId,
150    pub trace: TraceContext,
151    pub actor: ActorContext,
152    pub tenant_id: Option<TenantId>,
153    pub causation_id: Option<String>,
154    #[serde(default)]
155    pub client: ClientRequestMetadata,
156}
157
158impl RequestContext {
159    pub fn new(request_id: RequestId, correlation_id: CorrelationId) -> Self {
160        Self {
161            request_id,
162            correlation_id,
163            trace: TraceContext::default(),
164            actor: ActorContext::Anonymous,
165            tenant_id: None,
166            causation_id: None,
167            client: ClientRequestMetadata::default(),
168        }
169    }
170}
171
172#[derive(Clone)]
173pub struct AppContext {
174    pub config: Arc<AppConfig>,
175    pub db: DbPool,
176    pub redis: Option<crate::RedisConnection>,
177    pub actor_resolver: Arc<dyn ActorResolver>,
178    pub clock: Arc<dyn Clock>,
179    pub ids: Arc<dyn IdGenerator>,
180    pub events: Arc<dyn EventPublisher>,
181    pub telemetry_spans: Arc<dyn TelemetrySpanProvider>,
182    pub execution_logs: Arc<dyn ExecutionLogProvider>,
183    pub runtime_config: Arc<dyn RuntimeConfigProvider>,
184    pub health: HealthRegistry,
185    pub shutdown: Shutdown,
186}
187
188impl Debug for AppContext {
189    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        formatter
191            .debug_struct("AppContext")
192            .field("config", &self.config)
193            .field("db", &"<pool>")
194            .field("redis", &self.redis.as_ref().map(|_| "<connection>"))
195            .field("actor_resolver", &self.actor_resolver)
196            .field("telemetry_spans", &self.telemetry_spans)
197            .field("execution_logs", &self.execution_logs)
198            .field("runtime_config", &self.runtime_config)
199            .field("health", &self.health)
200            .field("shutdown", &self.shutdown)
201            .finish_non_exhaustive()
202    }
203}
204
205impl AppContext {
206    pub fn new(config: AppConfig, db: DbPool, events: Arc<dyn EventPublisher>) -> Self {
207        let execution_logs = Arc::new(PostgresExecutionLogProvider::new(db.clone()));
208        let actor_resolver = Arc::new(DevActorResolver::new(config.service.environment.clone()));
209        Self {
210            config: Arc::new(config),
211            db,
212            redis: None,
213            actor_resolver,
214            clock: Arc::new(SystemClock),
215            ids: Arc::new(UuidGenerator),
216            events,
217            telemetry_spans: Arc::new(NoopTelemetrySpanProvider),
218            execution_logs,
219            runtime_config: Arc::new(StaticRuntimeConfigProvider::empty()),
220            health: HealthRegistry::default(),
221            shutdown: Shutdown::new(),
222        }
223    }
224
225    pub fn with_actor_resolver(mut self, actor_resolver: Arc<dyn ActorResolver>) -> Self {
226        self.actor_resolver = actor_resolver;
227        self
228    }
229
230    pub fn with_redis(mut self, redis: Option<crate::RedisConnection>) -> Self {
231        self.redis = redis;
232        self
233    }
234
235    pub fn with_telemetry_span_provider(
236        mut self,
237        telemetry_spans: Arc<dyn TelemetrySpanProvider>,
238    ) -> Self {
239        self.telemetry_spans = telemetry_spans;
240        self
241    }
242
243    pub fn with_execution_log_provider(
244        mut self,
245        execution_logs: Arc<dyn ExecutionLogProvider>,
246    ) -> Self {
247        self.execution_logs = execution_logs;
248        self
249    }
250
251    pub fn with_runtime_config_provider(
252        mut self,
253        runtime_config: Arc<dyn RuntimeConfigProvider>,
254    ) -> Self {
255        self.runtime_config = runtime_config;
256        self
257    }
258}