Skip to main content

platform_module_remote/
source.rs

1use crate::admin_action::RemoteAdminActionSource;
2use crate::admin_data::RemoteAdminDataSource;
3use crate::binding::RemoteBinding;
4use crate::config::{RemoteModuleConfig, RemoteModuleTransport};
5use crate::protocol::RemoteManifestResponse;
6use crate::response::decode_json_response;
7use platform_core::error::ErrorDetail;
8use platform_core::{AppError, AppResult, ErrorCode};
9use platform_module::{
10    AdminDeclarativeComponent, AdminDeclarativeSurface, AdminSurface, Module, ModuleHttpRoute,
11};
12use std::sync::Arc;
13use std::time::Duration;
14
15#[derive(Debug, Clone)]
16pub struct RemoteModuleSource {
17    client: reqwest::Client,
18    config: RemoteModuleConfig,
19}
20
21impl RemoteModuleSource {
22    pub fn new(config: RemoteModuleConfig) -> AppResult<Self> {
23        let client = reqwest::Client::builder()
24            .timeout(Duration::from_millis(config.timeout_ms))
25            .build()
26            .map_err(|error| {
27                AppError::new(
28                    ErrorCode::Internal,
29                    format!("failed to build remote module client: {error}"),
30                )
31            })?;
32        Ok(Self { client, config })
33    }
34
35    pub async fn load(&self) -> AppResult<Module> {
36        let manifest = self.fetch_manifest().await?;
37        if manifest.name != self.config.name {
38            return Err(AppError::new(
39                ErrorCode::Internal,
40                format!(
41                    "remote module manifest name '{}' does not match configured name '{}'",
42                    manifest.name, self.config.name
43                ),
44            ));
45        }
46        validate_remote_http_routes(&manifest.http_routes)?;
47        let binding = RemoteBinding::from_surfaces(
48            self.config.clone(),
49            manifest.runtime.as_ref(),
50            manifest.events.as_ref(),
51        )?;
52
53        let has_admin_data = match &manifest.admin {
54            Some(AdminSurface::Schema(_)) => true,
55            Some(AdminSurface::DeclarativeCustom(surface)) => surface.fallback_schema.is_some(),
56            _ => false,
57        };
58        let has_admin_actions = matches!(
59            &manifest.admin,
60            Some(AdminSurface::DeclarativeCustom(surface)) if !surface.actions.is_empty()
61        );
62        let has_admin_queries = matches!(
63            &manifest.admin,
64            Some(AdminSurface::DeclarativeCustom(surface)) if has_query_value_component(surface)
65        );
66        let mut module = Module::remote(manifest, Arc::new(binding));
67        if has_admin_data {
68            module =
69                module.with_admin_data(Arc::new(RemoteAdminDataSource::new(self.config.clone())?));
70        }
71        if has_admin_actions {
72            module = module
73                .with_admin_actions(Arc::new(RemoteAdminActionSource::new(self.config.clone())?));
74        }
75        if has_admin_queries {
76            module = module
77                .with_admin_queries(Arc::new(RemoteAdminDataSource::new(self.config.clone())?));
78        }
79        Ok(module)
80    }
81
82    async fn fetch_manifest(&self) -> AppResult<RemoteManifestResponse> {
83        if self.config.transport == RemoteModuleTransport::Grpc {
84            return crate::grpc::fetch_manifest(&self.config).await;
85        }
86
87        let request = self
88            .client
89            .get(format!("{}/manifest", self.config.base_url));
90        let request = match &self.config.auth_token {
91            Some(token) => request.bearer_auth(token),
92            None => request,
93        };
94        let response = request.send().await.map_err(|error| {
95            AppError::new(
96                ErrorCode::ExternalDependency,
97                format!("remote manifest request failed: {error}"),
98            )
99            .retryable()
100        })?;
101
102        decode_json_response(response, "manifest", false)
103            .await?
104            .ok_or_else(|| AppError::new(ErrorCode::NotFound, "remote module manifest not found"))
105    }
106}
107
108fn has_query_value_component(surface: &AdminDeclarativeSurface) -> bool {
109    surface.pages.iter().any(|page| {
110        page.sections.iter().any(|section| {
111            matches!(
112                section.component,
113                AdminDeclarativeComponent::QueryValue { .. }
114            )
115        })
116    })
117}
118
119fn validate_remote_http_routes(routes: &[ModuleHttpRoute]) -> AppResult<()> {
120    let mut details = Vec::new();
121    for (index, route) in routes.iter().enumerate() {
122        if !is_valid_remote_http_route_path(&route.path) {
123            details.push(ErrorDetail {
124                field: Some(format!("http_routes.{index}.path")),
125                reason: "remote HTTP route path must be module-local, start with '/', and not contain empty or '..' segments".to_owned(),
126            });
127        }
128    }
129
130    if details.is_empty() {
131        Ok(())
132    } else {
133        Err(AppError::validation(
134            "remote module manifest contains invalid HTTP route declarations",
135            details,
136        ))
137    }
138}
139
140fn is_valid_remote_http_route_path(path: &str) -> bool {
141    path.starts_with('/')
142        && !path.starts_with("//")
143        && !path.contains("://")
144        && !path.contains('?')
145        && !path.contains('#')
146        && path
147            .split('/')
148            .skip(1)
149            .all(|segment| !segment.is_empty() && segment != "." && segment != "..")
150}