use crate::config::RemoteModuleConfig;
use crate::config::RemoteModuleTransport;
use crate::protocol::{RemoteGetResponse, RemoteListResponse, RemoteQueryResponse};
use crate::response::{
MAX_REMOTE_JSON_RESPONSE_BYTES, ResponseBodyPolicy, decode_json_response_with_policy,
};
use platform_core::{AppError, AppResult, ErrorCode};
use platform_module::{AdminDataSource, AdminListQuery, AdminPage, AdminQuerySource};
use serde_json::Value;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct RemoteAdminDataSource {
client: reqwest::Client,
config: RemoteModuleConfig,
}
impl RemoteAdminDataSource {
pub fn new(config: RemoteModuleConfig) -> AppResult<Self> {
let client = reqwest::Client::builder()
.timeout(Duration::from_millis(config.timeout_ms))
.build()
.map_err(|error| {
AppError::new(
ErrorCode::Internal,
format!("failed to build remote module client: {error}"),
)
})?;
Ok(Self { client, config })
}
fn url(&self, path: &str) -> String {
format!("{}/{}", self.config.base_url, path.trim_start_matches('/'))
}
fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
let request = self.client.request(method, self.url(path));
match &self.config.auth_token {
Some(token) => request.bearer_auth(token),
None => request,
}
}
async fn send_json<T: serde::de::DeserializeOwned>(
&self,
request: reqwest::RequestBuilder,
) -> AppResult<Option<T>> {
let response = request.send().await.map_err(|error| {
AppError::new(
ErrorCode::ExternalDependency,
format!("remote module request failed: {error}"),
)
.retryable()
})?;
decode_json_response_with_policy(
response,
"admin data",
true,
ResponseBodyPolicy {
max_bytes: Some(MAX_REMOTE_JSON_RESPONSE_BYTES),
require_json_content_type: true,
allow_empty_success: false,
},
)
.await
}
}
#[async_trait::async_trait]
impl AdminDataSource for RemoteAdminDataSource {
async fn list(&self, entity: &str, query: &AdminListQuery) -> AppResult<AdminPage> {
if self.config.transport == RemoteModuleTransport::Grpc {
return crate::grpc::list_admin_records(&self.config, entity, query)
.await
.map(Into::into);
}
let mut request = self
.request(reqwest::Method::GET, &format!("admin/{entity}"))
.query(&[("limit", query.limit.to_string())]);
if let Some(cursor) = &query.cursor {
request = request.query(&[("cursor", cursor)]);
}
let response = self
.send_json::<RemoteListResponse>(request)
.await?
.ok_or_else(|| AppError::new(ErrorCode::NotFound, "remote admin entity not found"))?;
Ok(response.into())
}
async fn get(&self, entity: &str, id: &str) -> AppResult<Option<Value>> {
if self.config.transport == RemoteModuleTransport::Grpc {
return match crate::grpc::get_admin_record(&self.config, entity, id).await {
Ok(response) => Ok(response.record),
Err(error) if error.code == ErrorCode::NotFound => Ok(None),
Err(error) => Err(error),
};
}
let request = self.request(reqwest::Method::GET, &format!("admin/{entity}/{id}"));
Ok(self
.send_json::<RemoteGetResponse>(request)
.await?
.and_then(|response| response.record))
}
}
#[async_trait::async_trait]
impl AdminQuerySource for RemoteAdminDataSource {
async fn query(&self, query: &str) -> AppResult<Value> {
validate_admin_query_name(query)?;
if self.config.transport == RemoteModuleTransport::Grpc {
return crate::grpc::query_admin_value(&self.config, query)
.await
.map(|response| response.data);
}
let request = self.request(reqwest::Method::GET, &format!("admin/queries/{query}"));
let response = self
.send_json::<RemoteQueryResponse>(request)
.await?
.ok_or_else(|| AppError::new(ErrorCode::NotFound, "remote admin query not found"))?;
Ok(response.data)
}
}
fn validate_admin_query_name(query: &str) -> AppResult<()> {
let valid = !query.is_empty()
&& query.chars().all(|character| {
character.is_ascii_alphanumeric()
|| character == '.'
|| character == '_'
|| character == '-'
});
if valid {
return Ok(());
}
Err(AppError::new(
ErrorCode::Validation,
"remote admin query name must be a stable path segment",
))
}