Skip to main content

platform_provider/
admin_action.rs

1use crate::ProviderHostEffectCoordinator;
2use crate::config::ProviderConfig;
3use crate::invocation::{self, InvocationContext};
4use crate::protocol::{
5    ProviderActionInvokeResponse, ProviderAdminActionInvokeRequest, ProviderInvocationMode,
6    ProviderOperationKind,
7};
8use platform_core::{ActorContext, AppError, AppResult, ErrorCode, TraceContext};
9use platform_module::AdminActionSource;
10use serde_json::Value;
11use std::time::Duration;
12
13#[derive(Debug, Clone)]
14pub struct ProviderAdminActionSource {
15    client: reqwest::Client,
16    config: ProviderConfig,
17    effects: ProviderHostEffectCoordinator,
18}
19
20impl ProviderAdminActionSource {
21    pub fn new(config: ProviderConfig) -> AppResult<Self> {
22        let client = reqwest::Client::builder()
23            .timeout(Duration::from_millis(config.timeout_ms))
24            .build()
25            .map_err(|error| {
26                AppError::new(
27                    ErrorCode::Internal,
28                    format!("failed to build Provider Service client: {error}"),
29                )
30            })?;
31        Ok(Self {
32            client,
33            config,
34            effects: ProviderHostEffectCoordinator::rejecting(),
35        })
36    }
37
38    #[must_use]
39    pub fn with_effect_coordinator(mut self, effects: ProviderHostEffectCoordinator) -> Self {
40        self.effects = effects;
41        self
42    }
43}
44
45#[async_trait::async_trait]
46impl AdminActionSource for ProviderAdminActionSource {
47    async fn invoke(&self, action: &str, input: Value) -> AppResult<Value> {
48        validate_action_name(action)?;
49        let invocation_id = uuid::Uuid::now_v7().to_string();
50        let invocation = invocation::build(
51            &self.config,
52            ProviderOperationKind::AdminAction,
53            action,
54            "1",
55            ProviderInvocationMode::Durable,
56            InvocationContext {
57                request_id: invocation_id.clone(),
58                invocation_id,
59                attempt: 1,
60                actor: ActorContext::System,
61                correlation_id: uuid::Uuid::now_v7().to_string(),
62                causation_id: None,
63                trace: TraceContext::default(),
64            },
65            serde_json::to_value(ProviderAdminActionInvokeRequest {
66                action: action.to_owned(),
67                input,
68            })
69            .map_err(|error| AppError::new(ErrorCode::Internal, error.to_string()))?,
70        )?;
71        let outcome = invocation::send(
72            &self.client,
73            &self.config,
74            &self.effects,
75            "admin:act",
76            &invocation,
77        )
78        .await?;
79        let envelope: ProviderActionInvokeResponse =
80            serde_json::from_value(invocation::result(&invocation, outcome)?).map_err(|error| {
81                AppError::new(
82                    ErrorCode::ExternalDependency,
83                    format!("Provider admin action result violated its contract: {error}"),
84                )
85            })?;
86        Ok(envelope.result)
87    }
88}
89
90fn validate_action_name(action: &str) -> AppResult<()> {
91    let valid = !action.is_empty()
92        && action.chars().all(|character| {
93            character.is_ascii_alphanumeric()
94                || character == '.'
95                || character == '_'
96                || character == '-'
97        });
98    if valid {
99        return Ok(());
100    }
101
102    Err(AppError::new(
103        ErrorCode::Validation,
104        "provider admin action name must be a stable path segment",
105    ))
106}