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::{RemoteManifestEnvelope, 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
21#[derive(Debug)]
22pub struct LoadedRemoteModule {
23    pub module: Module,
24    pub config: RemoteModuleConfig,
25}
26
27impl RemoteModuleSource {
28    pub fn new(config: RemoteModuleConfig) -> AppResult<Self> {
29        let client = reqwest::Client::builder()
30            .timeout(Duration::from_millis(config.timeout_ms))
31            .build()
32            .map_err(|error| {
33                AppError::new(
34                    ErrorCode::Internal,
35                    format!("failed to build remote module client: {error}"),
36                )
37            })?;
38        Ok(Self { client, config })
39    }
40
41    pub async fn load(&self) -> AppResult<Module> {
42        let mut loaded = self.load_all().await?;
43        if loaded.len() == 1 {
44            return Ok(loaded.remove(0).module);
45        }
46        loaded
47            .into_iter()
48            .find(|loaded| loaded.module.manifest.name == self.config.name)
49            .map(|loaded| loaded.module)
50            .ok_or_else(|| {
51                AppError::new(
52                    ErrorCode::Internal,
53                    format!(
54                        "remote service '{}' did not provide a module named '{}'",
55                        self.config.name, self.config.name
56                    ),
57                )
58            })
59    }
60
61    pub async fn load_all(&self) -> AppResult<Vec<LoadedRemoteModule>> {
62        match self.fetch_manifest().await? {
63            RemoteManifestEnvelope::Module(manifest) => {
64                if manifest.name != self.config.name {
65                    return Err(AppError::new(
66                        ErrorCode::Internal,
67                        format!(
68                            "remote module manifest name '{}' does not match configured name '{}'",
69                            manifest.name, self.config.name
70                        ),
71                    ));
72                }
73                Ok(vec![self.load_module(manifest, self.config.clone())?])
74            }
75            RemoteManifestEnvelope::Service(service) => {
76                if service.name != self.config.name {
77                    return Err(AppError::new(
78                        ErrorCode::Internal,
79                        format!(
80                            "remote service manifest name '{}' does not match configured name '{}'",
81                            service.name, self.config.name
82                        ),
83                    ));
84                }
85                service
86                    .modules
87                    .into_iter()
88                    .map(|manifest| {
89                        let config = self.config.for_service_module(&manifest.name);
90                        self.load_module(manifest, config)
91                    })
92                    .collect()
93            }
94        }
95    }
96
97    fn load_module(
98        &self,
99        manifest: RemoteManifestResponse,
100        config: RemoteModuleConfig,
101    ) -> AppResult<LoadedRemoteModule> {
102        validate_remote_http_routes(&manifest.http_routes)?;
103        let binding = RemoteBinding::from_surfaces(
104            config.clone(),
105            manifest.runtime.as_ref(),
106            manifest.events.as_ref(),
107        )?;
108
109        let has_admin_data = match &manifest.admin {
110            Some(AdminSurface::Schema(_)) => true,
111            Some(AdminSurface::DeclarativeCustom(surface)) => surface.fallback_schema.is_some(),
112            _ => false,
113        };
114        let has_admin_actions = matches!(
115            &manifest.admin,
116            Some(AdminSurface::DeclarativeCustom(surface)) if !surface.actions.is_empty()
117        );
118        let has_admin_queries = matches!(
119            &manifest.admin,
120            Some(AdminSurface::DeclarativeCustom(surface)) if has_query_value_component(surface)
121        );
122        let mut module = Module::remote(manifest, Arc::new(binding));
123        if has_admin_data {
124            module = module.with_admin_data(Arc::new(RemoteAdminDataSource::new(config.clone())?));
125        }
126        if has_admin_actions {
127            module =
128                module.with_admin_actions(Arc::new(RemoteAdminActionSource::new(config.clone())?));
129        }
130        if has_admin_queries {
131            module =
132                module.with_admin_queries(Arc::new(RemoteAdminDataSource::new(config.clone())?));
133        }
134        Ok(LoadedRemoteModule { module, config })
135    }
136
137    async fn fetch_manifest(&self) -> AppResult<RemoteManifestEnvelope> {
138        if self.config.transport == RemoteModuleTransport::Grpc {
139            return crate::grpc::fetch_manifest(&self.config)
140                .await
141                .map(RemoteManifestEnvelope::Module);
142        }
143
144        let request = self
145            .client
146            .get(format!("{}/manifest", self.config.base_url));
147        let request = match &self.config.auth_token {
148            Some(token) => request.bearer_auth(token),
149            None => request,
150        };
151        let response = request.send().await.map_err(|error| {
152            AppError::new(
153                ErrorCode::ExternalDependency,
154                format!("remote manifest request failed: {error}"),
155            )
156            .retryable()
157        })?;
158
159        decode_json_response(response, "manifest", false)
160            .await?
161            .ok_or_else(|| AppError::new(ErrorCode::NotFound, "remote module manifest not found"))
162    }
163}
164
165fn has_query_value_component(surface: &AdminDeclarativeSurface) -> bool {
166    surface.pages.iter().any(|page| {
167        page.sections.iter().any(|section| {
168            matches!(
169                section.component,
170                AdminDeclarativeComponent::QueryValue { .. }
171            )
172        })
173    })
174}
175
176fn validate_remote_http_routes(routes: &[ModuleHttpRoute]) -> AppResult<()> {
177    let mut details = Vec::new();
178    for (index, route) in routes.iter().enumerate() {
179        if !is_valid_remote_http_route_path(&route.path) {
180            details.push(ErrorDetail {
181                field: Some(format!("http_routes.{index}.path")),
182                reason: "remote HTTP route path must be module-local, start with '/', and not contain empty or '..' segments".to_owned(),
183            });
184        }
185    }
186
187    if details.is_empty() {
188        Ok(())
189    } else {
190        Err(AppError::validation(
191            "remote module manifest contains invalid HTTP route declarations",
192            details,
193        ))
194    }
195}
196
197fn is_valid_remote_http_route_path(path: &str) -> bool {
198    path.starts_with('/')
199        && !path.starts_with("//")
200        && !path.contains("://")
201        && !path.contains('?')
202        && !path.contains('#')
203        && path
204            .split('/')
205            .skip(1)
206            .all(|segment| !segment.is_empty() && segment != "." && segment != "..")
207}