platform_module_remote/
admin_action.rs1use 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}