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::decode_json_response;
5use platform_core::{AppError, AppResult, ErrorCode};
6use platform_module::{AdminDataSource, AdminListQuery, AdminPage, AdminQuerySource};
7use serde_json::Value;
8use std::time::Duration;
9
10#[derive(Debug, Clone)]
11pub struct RemoteAdminDataSource {
12    client: reqwest::Client,
13    config: RemoteModuleConfig,
14}
15
16impl RemoteAdminDataSource {
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    async fn send_json<T: serde::de::DeserializeOwned>(
43        &self,
44        request: reqwest::RequestBuilder,
45    ) -> AppResult<Option<T>> {
46        let response = request.send().await.map_err(|error| {
47            AppError::new(
48                ErrorCode::ExternalDependency,
49                format!("remote module request failed: {error}"),
50            )
51            .retryable()
52        })?;
53
54        decode_json_response(response, "admin data", true).await
55    }
56}
57
58#[async_trait::async_trait]
59impl AdminDataSource for RemoteAdminDataSource {
60    async fn list(&self, entity: &str, query: &AdminListQuery) -> AppResult<AdminPage> {
61        if self.config.transport == RemoteModuleTransport::Grpc {
62            return crate::grpc::list_admin_records(&self.config, entity, query)
63                .await
64                .map(Into::into);
65        }
66
67        let mut request = self
68            .request(reqwest::Method::GET, &format!("admin/{entity}"))
69            .query(&[("limit", query.limit.to_string())]);
70        if let Some(cursor) = &query.cursor {
71            request = request.query(&[("cursor", cursor)]);
72        }
73        let response = self
74            .send_json::<RemoteListResponse>(request)
75            .await?
76            .ok_or_else(|| AppError::new(ErrorCode::NotFound, "remote admin entity not found"))?;
77        Ok(response.into())
78    }
79
80    async fn get(&self, entity: &str, id: &str) -> AppResult<Option<Value>> {
81        if self.config.transport == RemoteModuleTransport::Grpc {
82            return match crate::grpc::get_admin_record(&self.config, entity, id).await {
83                Ok(response) => Ok(response.record),
84                Err(error) if error.code == ErrorCode::NotFound => Ok(None),
85                Err(error) => Err(error),
86            };
87        }
88
89        let request = self.request(reqwest::Method::GET, &format!("admin/{entity}/{id}"));
90        Ok(self
91            .send_json::<RemoteGetResponse>(request)
92            .await?
93            .and_then(|response| response.record))
94    }
95}
96
97#[async_trait::async_trait]
98impl AdminQuerySource for RemoteAdminDataSource {
99    async fn query(&self, query: &str) -> AppResult<Value> {
100        validate_admin_query_name(query)?;
101        if self.config.transport == RemoteModuleTransport::Grpc {
102            return crate::grpc::query_admin_value(&self.config, query)
103                .await
104                .map(|response| response.data);
105        }
106
107        let request = self.request(reqwest::Method::GET, &format!("admin/queries/{query}"));
108        let response = self
109            .send_json::<RemoteQueryResponse>(request)
110            .await?
111            .ok_or_else(|| AppError::new(ErrorCode::NotFound, "remote admin query not found"))?;
112        Ok(response.data)
113    }
114}
115
116fn validate_admin_query_name(query: &str) -> AppResult<()> {
117    let valid = !query.is_empty()
118        && query.chars().all(|character| {
119            character.is_ascii_alphanumeric()
120                || character == '.'
121                || character == '_'
122                || character == '-'
123        });
124    if valid {
125        return Ok(());
126    }
127
128    Err(AppError::new(
129        ErrorCode::Validation,
130        "remote admin query name must be a stable path segment",
131    ))
132}