use platform_core::{AppError, ErrorCode, RequestContext, StoryDisplayDescriptor};
use platform_http::{ApiErrorResponse, ApiOpenApiRouter, OpenApiRouter, routes};
use platform_module::{
AdminActionSource, AdminDataSource, AdminQuerySource, AdminSchema, AdminSurface,
ConsoleSurface, EventSurface, LifecycleSurface, ModuleHttpRoute, ModuleLoadStatus,
ModuleSource, RuntimeSurface,
};
use std::sync::{Arc, OnceLock, RwLock};
use std::time::Instant;
mod dto;
mod handlers;
pub use dto::*;
#[allow(clippy::wildcard_imports)]
use handlers::*;
#[derive(Clone, Debug)]
pub struct AdminModule {
pub module_name: String,
pub source: ModuleSource,
pub load_status: ModuleLoadStatus,
pub schema: AdminSchema,
pub admin: Option<AdminSurface>,
pub listed_in_schema: bool,
pub data_source: Option<Arc<dyn AdminDataSource>>,
pub action_source: Option<Arc<dyn AdminActionSource>>,
pub query_source: Option<Arc<dyn AdminQuerySource>>,
}
#[derive(Clone, Debug)]
pub struct AdminModuleMetadata {
pub module_name: String,
pub source: ModuleSource,
pub load_status: ModuleLoadStatus,
pub http_routes: Vec<ModuleHttpRoute>,
pub runtime: Option<RuntimeSurface>,
pub events: Option<EventSurface>,
pub lifecycle: Option<LifecycleSurface>,
pub console: Vec<ConsoleSurface>,
pub story_display: Vec<StoryDisplayDescriptor>,
pub capabilities: Vec<String>,
pub dependencies: Vec<String>,
pub admin: Option<AdminSurface>,
pub source_diagnostics: Option<AdminModuleSourceDiagnostics>,
}
#[derive(Clone, Debug)]
pub enum AdminModuleSourceDiagnostics {
Remote(AdminRemoteModuleDiagnostics),
}
#[derive(Clone, Debug)]
pub struct AdminRemoteModuleDiagnostics {
pub transport: String,
pub base_url: String,
pub manifest_url: String,
pub timeout_ms: u64,
pub auth_configured: bool,
pub load_duration_ms: Option<u64>,
pub last_checked_at: Option<String>,
pub last_load_error: Option<String>,
}
#[derive(Clone, Debug, Default)]
struct AdminModuleMetadataSnapshot {
modules: Vec<AdminModuleMetadata>,
refreshed_at: Option<String>,
refresh_error: Option<String>,
refresh_history: Vec<AdminModuleMetadataRefreshRecord>,
}
#[derive(Clone, Debug)]
pub struct AdminModuleMetadataRefreshRecord {
pub id: String,
pub status: AdminModuleMetadataRefreshStatus,
pub started_at: String,
pub completed_at: String,
pub duration_ms: u64,
pub module_count: usize,
pub error: Option<String>,
pub module_results: Vec<AdminModuleMetadataRefreshModuleResult>,
}
#[derive(Clone, Debug)]
pub struct AdminModuleMetadataRefreshModuleResult {
pub module_name: String,
pub source: ModuleSource,
pub status: AdminModuleMetadataRefreshModuleStatus,
pub duration_ms: Option<u64>,
pub endpoint: Option<String>,
pub error: Option<String>,
}
#[derive(Clone, Copy, Debug)]
pub enum AdminModuleMetadataRefreshModuleStatus {
Loaded,
Error,
}
#[derive(Clone, Copy, Debug)]
pub enum AdminModuleMetadataRefreshStatus {
Success,
Error,
}
static ADMIN_REGISTRY: OnceLock<RwLock<Vec<AdminModule>>> = OnceLock::new();
static ADMIN_METADATA_REGISTRY: OnceLock<RwLock<AdminModuleMetadataSnapshot>> = OnceLock::new();
static ADMIN_REFRESHER: OnceLock<RwLock<Option<Arc<dyn AdminModuleRefresher>>>> = OnceLock::new();
static ADMIN_METADATA_REFRESHER: OnceLock<RwLock<Option<Arc<dyn AdminModuleMetadataRefresher>>>> =
OnceLock::new();
#[async_trait::async_trait]
pub trait AdminModuleRefresher: Send + Sync {
async fn refresh_admin_modules(&self) -> platform_core::AppResult<Vec<AdminModule>>;
}
#[async_trait::async_trait]
pub trait AdminModuleMetadataRefresher: Send + Sync {
async fn refresh_admin_module_metadata(
&self,
) -> platform_core::AppResult<Vec<AdminModuleMetadata>>;
}
struct StaticAdminModuleRefresher<F>(F);
struct StaticAdminModuleMetadataRefresher<F>(F);
#[async_trait::async_trait]
impl<F, Fut> AdminModuleRefresher for StaticAdminModuleRefresher<F>
where
F: Fn() -> Fut + Send + Sync,
Fut: std::future::Future<Output = platform_core::AppResult<Vec<AdminModule>>> + Send,
{
async fn refresh_admin_modules(&self) -> platform_core::AppResult<Vec<AdminModule>> {
(self.0)().await
}
}
#[async_trait::async_trait]
impl<F, Fut> AdminModuleMetadataRefresher for StaticAdminModuleMetadataRefresher<F>
where
F: Fn() -> Fut + Send + Sync,
Fut: std::future::Future<Output = platform_core::AppResult<Vec<AdminModuleMetadata>>> + Send,
{
async fn refresh_admin_module_metadata(
&self,
) -> platform_core::AppResult<Vec<AdminModuleMetadata>> {
(self.0)().await
}
}
pub fn install_admin_modules(modules: Vec<AdminModule>) {
let registry = ADMIN_REGISTRY.get_or_init(|| RwLock::new(Vec::new()));
*registry.write().expect("admin registry lock poisoned") = modules;
}
pub fn install_admin_module_metadata(modules: Vec<AdminModuleMetadata>) {
let registry =
ADMIN_METADATA_REGISTRY.get_or_init(|| RwLock::new(AdminModuleMetadataSnapshot::default()));
*registry
.write()
.expect("admin metadata registry lock poisoned") = AdminModuleMetadataSnapshot {
modules,
refreshed_at: Some(current_timestamp()),
refresh_error: None,
refresh_history: Vec::new(),
};
}
pub(crate) fn record_admin_module_metadata_refresh_success(
modules: Vec<AdminModuleMetadata>,
started_at: String,
started: Instant,
) -> AdminModuleMetadataSnapshot {
let registry =
ADMIN_METADATA_REGISTRY.get_or_init(|| RwLock::new(AdminModuleMetadataSnapshot::default()));
let mut snapshot = registry
.write()
.expect("admin metadata registry lock poisoned");
let completed_at = current_timestamp();
let record = AdminModuleMetadataRefreshRecord {
id: format!(
"module_refresh_{}",
completed_at.replace([':', '.', '+'], "_")
),
status: AdminModuleMetadataRefreshStatus::Success,
started_at,
completed_at: completed_at.clone(),
duration_ms: duration_ms(started),
module_count: modules.len(),
error: None,
module_results: refresh_module_results(&modules),
};
snapshot.modules = modules;
snapshot.refreshed_at = Some(completed_at);
snapshot.refresh_error = None;
push_refresh_record(&mut snapshot.refresh_history, record);
snapshot.clone()
}
pub fn install_admin_module_refresher(refresher: Arc<dyn AdminModuleRefresher>) {
let registry = ADMIN_REFRESHER.get_or_init(|| RwLock::new(None));
*registry.write().expect("admin refresher lock poisoned") = Some(refresher);
}
pub fn install_admin_module_refresh_fn<F, Fut>(refresh: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = platform_core::AppResult<Vec<AdminModule>>> + Send + 'static,
{
install_admin_module_refresher(Arc::new(StaticAdminModuleRefresher(refresh)));
}
pub fn install_admin_module_metadata_refresher(refresher: Arc<dyn AdminModuleMetadataRefresher>) {
let registry = ADMIN_METADATA_REFRESHER.get_or_init(|| RwLock::new(None));
*registry
.write()
.expect("admin metadata refresher lock poisoned") = Some(refresher);
}
pub fn install_admin_module_metadata_refresh_fn<F, Fut>(refresh: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = platform_core::AppResult<Vec<AdminModuleMetadata>>>
+ Send
+ 'static,
{
install_admin_module_metadata_refresher(Arc::new(StaticAdminModuleMetadataRefresher(refresh)));
}
fn admin_modules() -> Vec<AdminModule> {
ADMIN_REGISTRY
.get()
.map(|registry| {
registry
.read()
.expect("admin registry lock poisoned")
.clone()
})
.unwrap_or_default()
}
fn admin_module_metadata_snapshot() -> AdminModuleMetadataSnapshot {
ADMIN_METADATA_REGISTRY
.get()
.map(|registry| {
registry
.read()
.expect("admin metadata registry lock poisoned")
.clone()
})
.unwrap_or_default()
}
pub(crate) fn record_admin_module_metadata_refresh_error(
error: String,
started_at: String,
started: Instant,
) -> AdminModuleMetadataSnapshot {
let registry =
ADMIN_METADATA_REGISTRY.get_or_init(|| RwLock::new(AdminModuleMetadataSnapshot::default()));
let mut snapshot = registry
.write()
.expect("admin metadata registry lock poisoned");
snapshot.refresh_error = Some(error);
let completed_at = current_timestamp();
let record = AdminModuleMetadataRefreshRecord {
id: format!(
"module_refresh_{}",
completed_at.replace([':', '.', '+'], "_")
),
status: AdminModuleMetadataRefreshStatus::Error,
started_at,
completed_at,
duration_ms: duration_ms(started),
module_count: snapshot.modules.len(),
error: snapshot.refresh_error.clone(),
module_results: Vec::new(),
};
push_refresh_record(&mut snapshot.refresh_history, record);
snapshot.clone()
}
pub(crate) fn current_timestamp() -> String {
use platform_core::Clock;
platform_core::SystemClock.now().to_rfc3339()
}
fn duration_ms(started: Instant) -> u64 {
u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
}
fn push_refresh_record(
history: &mut Vec<AdminModuleMetadataRefreshRecord>,
record: AdminModuleMetadataRefreshRecord,
) {
history.insert(0, record);
history.truncate(10);
}
fn refresh_module_results(
modules: &[AdminModuleMetadata],
) -> Vec<AdminModuleMetadataRefreshModuleResult> {
modules
.iter()
.map(|module| {
let remote = match &module.source_diagnostics {
Some(AdminModuleSourceDiagnostics::Remote(remote)) => Some(remote),
None => None,
};
AdminModuleMetadataRefreshModuleResult {
module_name: module.module_name.clone(),
source: module.source,
status: match module.load_status {
ModuleLoadStatus::Loaded => AdminModuleMetadataRefreshModuleStatus::Loaded,
ModuleLoadStatus::Error { .. } => AdminModuleMetadataRefreshModuleStatus::Error,
},
duration_ms: remote.and_then(|diagnostics| diagnostics.load_duration_ms),
endpoint: remote.map(|diagnostics| diagnostics.base_url.clone()),
error: match &module.load_status {
ModuleLoadStatus::Loaded => {
remote.and_then(|diagnostics| diagnostics.last_load_error.clone())
}
ModuleLoadStatus::Error { message } => Some(message.clone()),
},
}
})
.collect()
}
fn admin_refresher() -> Option<Arc<dyn AdminModuleRefresher>> {
ADMIN_REFRESHER.get().and_then(|registry| {
registry
.read()
.expect("admin refresher lock poisoned")
.clone()
})
}
fn admin_metadata_refresher() -> Option<Arc<dyn AdminModuleMetadataRefresher>> {
ADMIN_METADATA_REFRESHER.get().and_then(|registry| {
registry
.read()
.expect("admin metadata refresher lock poisoned")
.clone()
})
}
fn find_module(module: &str, ctx: &RequestContext) -> Result<AdminModule, ApiErrorResponse> {
admin_modules()
.into_iter()
.find(|m| m.module_name == module)
.ok_or_else(|| {
ApiErrorResponse::with_context(
AppError::new(ErrorCode::NotFound, format!("unknown module: {module}")),
ctx,
)
})
}
fn find_loaded_module(module: &str, ctx: &RequestContext) -> Result<AdminModule, ApiErrorResponse> {
let admin_module = find_module(module, ctx)?;
if admin_module.data_source.is_some() {
Ok(admin_module)
} else {
Err(ApiErrorResponse::with_context(
AppError::new(
ErrorCode::ExternalDependency,
format!("module {module} is not loaded"),
)
.retryable(),
ctx,
))
}
}
fn find_loaded_action_module(
module: &str,
ctx: &RequestContext,
) -> Result<AdminModule, ApiErrorResponse> {
let admin_module = find_module(module, ctx)?;
if matches!(admin_module.load_status, ModuleLoadStatus::Loaded) {
Ok(admin_module)
} else {
Err(ApiErrorResponse::with_context(
AppError::new(
ErrorCode::ExternalDependency,
format!("module {module} is not loaded"),
)
.retryable(),
ctx,
))
}
}
fn find_loaded_query_module(
module: &str,
ctx: &RequestContext,
) -> Result<AdminModule, ApiErrorResponse> {
let admin_module = find_module(module, ctx)?;
if matches!(admin_module.load_status, ModuleLoadStatus::Loaded) {
Ok(admin_module)
} else {
Err(ApiErrorResponse::with_context(
AppError::new(
ErrorCode::ExternalDependency,
format!("module {module} is not loaded"),
)
.retryable(),
ctx,
))
}
}
pub fn router() -> ApiOpenApiRouter {
OpenApiRouter::new()
.routes(routes!(list_modules))
.routes(routes!(refresh_modules))
.routes(routes!(available_modules))
.routes(routes!(install_available_module))
.routes(routes!(uninstall_available_module))
.routes(routes!(module_registry_snapshot))
.routes(routes!(list_schemas))
.routes(routes!(refresh_schemas))
.routes(routes!(invoke_action))
.routes(routes!(query_value))
.routes(routes!(list_records))
.routes(routes!(get_record))
}