Skip to main content

platform_module_remote/
admin_data.rs

1use crate::config::RemoteModuleConfig;
2use crate::config::RemoteModuleTransport;
3use crate::protocol::{RemoteGetResponse, RemoteListResponse, RemoteQueryResponse};
4use crate::response::{
5    MAX_REMOTE_JSON_RESPONSE_BYTES, ResponseBodyPolicy, decode_json_response_with_policy,
6};
7use platform_core::{AppError, AppResult, ErrorCode};
8use platform_module::{AdminDataSource, AdminListQuery, AdminPage, AdminQuerySource};
9use serde_json::Value;
10use std::time::Duration;
11
12#[derive(Debug, Clone)]
13pub struct RemoteAdminDataSource {
14    client: reqwest::Client,
15    config: RemoteModuleConfig,
16}
17
18impl RemoteAdminDataSource {
19    pub fn new(config: RemoteModuleConfig) -> AppResult<Self> {
20        let client = reqwest::Client::builder()
21            .timeout(Duration::from_millis(config.timeout_ms))
22            .build()
23            .map_err(|error| {
24                AppError::new(
25                    ErrorCode::Internal,
26                    format!("failed to build remote module client: {error}"),
27                )
28            })?;
29        Ok(Self { client, config })
30    }
31
32    fn url(&self, path: &str) -> String {
33        format!("{}/{}", self.config.base_url, path.trim_start_matches('/'))
34    }
35
36    fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
37        let request = self.client.request(method, self.url(path));
38        match &self.config.auth_token {
39            Some(token) => request.bearer_auth(token),
40            None => request,
41        }
42    }
43
44    async fn send_json<T: serde::de::DeserializeOwned>(
45        &self,
46        request: reqwest::RequestBuilder,
47    ) -> AppResult<Option<T>> {
48        let response = request.send().await.map_err(|error| {
49            AppError::new(
50                ErrorCode::ExternalDependency,
51                format!("remote module request failed: {error}"),
52            )
53            .retryable()
54        })?;
55
56        decode_json_response_with_policy(
57            response,
58            "admin data",
59            true,
60            ResponseBodyPolicy {
61                max_bytes: Some(MAX_REMOTE_JSON_RESPONSE_BYTES),
62                require_json_content_type: true,
63                allow_empty_success: false,
64            },
65        )
66        .await
67    }
68}
69
70#[async_trait::async_trait]
71impl AdminDataSource for RemoteAdminDataSource {
72    async fn list(&self, entity: &str, query: &AdminListQuery) -> AppResult<AdminPage> {
73        if self.config.transport == RemoteModuleTransport::Grpc {
74            return crate::grpc::list_admin_records(&self.config, entity, query)
75                .await
76                .map(Into::into);
77        }
78
79        let mut request = self
80            .request(reqwest::Method::GET, &format!("admin/{entity}"))
81            .query(&[("limit", query.limit.to_string())]);
82        if let Some(cursor) = &query.cursor {
83            request = request.query(&[("cursor", cursor)]);
84        }
85        let response = self
86            .send_json::<RemoteListResponse>(request)
87            .await?
88            .ok_or_else(|| AppError::new(ErrorCode::NotFound, "remote admin entity not found"))?;
89        Ok(response.into())
90    }
91
92    async fn get(&self, entity: &str, id: &str) -> AppResult<Option<Value>> {
93        if self.config.transport == RemoteModuleTransport::Grpc {
94            return match crate::grpc::get_admin_record(&self.config, entity, id).await {
95                Ok(response) => Ok(response.record),
96                Err(error) if error.code == ErrorCode::NotFound => Ok(None),
97                Err(error) => Err(error),
98            };
99        }
100
101        let request = self.request(reqwest::Method::GET, &format!("admin/{entity}/{id}"));
102        Ok(self
103            .send_json::<RemoteGetResponse>(request)
104            .await?
105            .and_then(|response| response.record))
106    }
107}
108
109#[async_trait::async_trait]
110impl AdminQuerySource for RemoteAdminDataSource {
111    async fn query(&self, query: &str) -> AppResult<Value> {
112        validate_admin_query_name(query)?;
113        if self.config.transport == RemoteModuleTransport::Grpc {
114            return crate::grpc::query_admin_value(&self.config, query)
115                .await
116                .map(|response| response.data);
117        }
118
119        let request = self.request(reqwest::Method::GET, &format!("admin/queries/{query}"));
120        let response = self
121            .send_json::<RemoteQueryResponse>(request)
122            .await?
123            .ok_or_else(|| AppError::new(ErrorCode::NotFound, "remote admin query not found"))?;
124        Ok(response.data)
125    }
126}
127
128fn validate_admin_query_name(query: &str) -> AppResult<()> {
129    let valid = !query.is_empty()
130        && query.chars().all(|character| {
131            character.is_ascii_alphanumeric()
132                || character == '.'
133                || character == '_'
134                || character == '-'
135        });
136    if valid {
137        return Ok(());
138    }
139
140    Err(AppError::new(
141        ErrorCode::Validation,
142        "remote admin query name must be a stable path segment",
143    ))
144}