Skip to main content

platform_module_remote/
admin_action.rs

1use crate::config::RemoteModuleConfig;
2use crate::config::RemoteModuleTransport;
3use crate::protocol::RemoteActionInvokeResponse;
4use crate::response::decode_json_response;
5use platform_core::{AppError, AppResult, ErrorCode};
6use platform_module::AdminActionSource;
7use serde_json::Value;
8use std::time::Duration;
9
10#[derive(Debug, Clone)]
11pub struct RemoteAdminActionSource {
12    client: reqwest::Client,
13    config: RemoteModuleConfig,
14}
15
16impl RemoteAdminActionSource {
17    pub fn new(config: RemoteModuleConfig) -> AppResult<Self> {
18        let client = reqwest::Client::builder()
19            .timeout(Duration::from_millis(config.timeout_ms))
20            .build()
21            .map_err(|error| {
22                AppError::new(
23                    ErrorCode::Internal,
24                    format!("failed to build remote module client: {error}"),
25                )
26            })?;
27        Ok(Self { client, config })
28    }
29
30    fn url(&self, path: &str) -> String {
31        format!("{}/{}", self.config.base_url, path.trim_start_matches('/'))
32    }
33
34    fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
35        let request = self.client.request(method, self.url(path));
36        match &self.config.auth_token {
37            Some(token) => request.bearer_auth(token),
38            None => request,
39        }
40    }
41}
42
43#[async_trait::async_trait]
44impl AdminActionSource for RemoteAdminActionSource {
45    async fn invoke(&self, action: &str, input: Value) -> AppResult<Value> {
46        validate_action_name(action)?;
47        if self.config.transport == RemoteModuleTransport::Grpc {
48            return crate::grpc::invoke_admin_action(&self.config, action, input)
49                .await
50                .map(|envelope| envelope.result);
51        }
52
53        let response = self
54            .request(reqwest::Method::POST, &format!("admin/actions/{action}"))
55            .json(&input)
56            .send()
57            .await
58            .map_err(|error| {
59                AppError::new(
60                    ErrorCode::ExternalDependency,
61                    format!("remote module action request failed: {error}"),
62                )
63                .retryable()
64            })?;
65
66        let envelope =
67            decode_json_response::<RemoteActionInvokeResponse>(response, "admin action", true)
68                .await?
69                .ok_or_else(|| {
70                    AppError::new(ErrorCode::NotFound, "remote admin action not found")
71                })?;
72        Ok(envelope.result)
73    }
74}
75
76fn validate_action_name(action: &str) -> AppResult<()> {
77    let valid = !action.is_empty()
78        && action.chars().all(|character| {
79            character.is_ascii_alphanumeric()
80                || character == '.'
81                || character == '_'
82                || character == '-'
83        });
84    if valid {
85        return Ok(());
86    }
87
88    Err(AppError::new(
89        ErrorCode::Validation,
90        "remote admin action name must be a stable path segment",
91    ))
92}