use std::collections::BTreeMap;
use super::{
DynamicPluginAttestationMode, DynamicPluginCheckState, DynamicPluginFailure, DynamicPluginId,
DynamicPluginManifest, DynamicPluginMetadata, DynamicPluginRecord, DynamicPluginRuntimeStatus,
DynamicPluginStartupClass, DynamicPluginValidationStatus, bump_generation,
stamp_creation_metadata,
};
use crate::plugin::{PluginError, Result};
#[derive(Debug, Default)]
pub struct DynamicPluginRegistry {
records: BTreeMap<DynamicPluginId, DynamicPluginRecord>,
}
impl DynamicPluginRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn from_records(records: Vec<DynamicPluginRecord>) -> Result<Self> {
let mut registry = Self::new();
for mut record in records {
normalize_record_shape(&mut record);
validate_record_shape(&record)?;
let plugin_id = record.metadata.id.clone();
if registry.records.contains_key(&plugin_id) {
return Err(PluginError::Conflict(format!(
"dynamic plugin '{plugin_id}' is duplicated in persisted registry state"
)));
}
registry.records.insert(plugin_id, record);
}
Ok(registry)
}
pub fn get(&self, plugin_id: &str) -> Option<&DynamicPluginRecord> {
self.records.get(plugin_id)
}
pub fn list(&self, include_tombstoned: bool) -> Vec<&DynamicPluginRecord> {
self.records
.values()
.filter(|record| include_tombstoned || !record.is_tombstoned())
.collect()
}
pub fn cloned_records(&self, include_tombstoned: bool) -> Vec<DynamicPluginRecord> {
self.list(include_tombstoned).into_iter().cloned().collect()
}
pub fn add(&mut self, mut record: DynamicPluginRecord) -> Result<&DynamicPluginRecord> {
normalize_record_shape(&mut record);
validate_record_shape(&record)?;
let plugin_id = record.metadata.id.clone();
record.spec.present = true;
if let Some(existing) = self.records.get(&plugin_id) {
if !existing.is_tombstoned() {
return Err(PluginError::Conflict(format!(
"dynamic plugin '{plugin_id}' is already registered"
)));
}
inherit_tombstoned_lineage(&mut record.metadata, &existing.metadata);
}
stamp_creation_metadata(&mut record.metadata);
self.records.insert(plugin_id.clone(), record);
Ok(self
.records
.get(&plugin_id)
.expect("dynamic plugin record must exist immediately after insert"))
}
pub fn add_manifest(
&mut self,
manifest: DynamicPluginManifest,
manifest_ref: Option<String>,
) -> Result<&DynamicPluginRecord> {
let record = manifest.into_record(manifest_ref)?;
self.add(record)
}
pub fn enable(&mut self, plugin_id: &str) -> Result<bool> {
let record = self.lookup_mut(plugin_id)?;
ensure_live_record(record, plugin_id)?;
if record.spec.enabled {
return Ok(false);
}
record.spec.enabled = true;
bump_generation(record);
Ok(true)
}
pub fn disable(&mut self, plugin_id: &str) -> Result<bool> {
let record = self.lookup_mut(plugin_id)?;
ensure_live_record(record, plugin_id)?;
if !record.spec.enabled {
return Ok(false);
}
record.spec.enabled = false;
bump_generation(record);
Ok(true)
}
pub fn remove(&mut self, plugin_id: &str) -> Result<bool> {
let record = self.lookup_mut(plugin_id)?;
if record.is_tombstoned() {
return Ok(false);
}
record.spec.present = false;
record.spec.enabled = false;
bump_generation(record);
Ok(true)
}
pub fn update_validation_status(
&mut self,
plugin_id: &str,
mut validation: DynamicPluginValidationStatus,
) -> Result<()> {
validation.checked_at = Some(super::current_timestamp());
let record = self.lookup_mut(plugin_id)?;
record.status.validation = validation;
Ok(())
}
pub fn update_runtime_status(
&mut self,
plugin_id: &str,
mut runtime: DynamicPluginRuntimeStatus,
) -> Result<()> {
runtime.updated_at = Some(super::current_timestamp());
let record = self.lookup_mut(plugin_id)?;
record.status.runtime = runtime;
Ok(())
}
pub fn update_last_error(
&mut self,
plugin_id: &str,
last_error: Option<DynamicPluginFailure>,
) -> Result<()> {
let record = self.lookup_mut(plugin_id)?;
record.status.last_error = last_error;
Ok(())
}
pub fn update_environment(
&mut self,
plugin_id: &str,
environment_ref: Option<String>,
environment: DynamicPluginCheckState,
) -> Result<()> {
let record = self.lookup_mut(plugin_id)?;
record.source.environment_ref = environment_ref;
record.status.validation.environment = environment;
record.status.validation.checked_at = Some(super::current_timestamp());
Ok(())
}
pub fn update_policy_status(
&mut self,
plugin_id: &str,
policy_satisfied: DynamicPluginCheckState,
startup_class: DynamicPluginStartupClass,
attestation_mode: DynamicPluginAttestationMode,
last_error: Option<DynamicPluginFailure>,
) -> Result<()> {
let record = self.lookup_mut(plugin_id)?;
record.status.validation.policy_satisfied = policy_satisfied;
record.status.startup_class = Some(startup_class);
record.status.attestation_mode = Some(attestation_mode);
record.status.last_error = last_error;
Ok(())
}
fn lookup_mut(&mut self, plugin_id: &str) -> Result<&mut DynamicPluginRecord> {
self.records.get_mut(plugin_id).ok_or_else(|| {
PluginError::NotFound(format!("dynamic plugin '{plugin_id}' is not registered"))
})
}
}
fn ensure_live_record(record: &DynamicPluginRecord, plugin_id: &str) -> Result<()> {
if record.is_tombstoned() {
return Err(PluginError::Conflict(format!(
"dynamic plugin '{plugin_id}' has been removed"
)));
}
Ok(())
}
fn inherit_tombstoned_lineage(
metadata: &mut DynamicPluginMetadata,
existing: &DynamicPluginMetadata,
) {
let next_generation = existing.generation.saturating_add(1);
if metadata.created_at.is_none() {
metadata.created_at = existing.created_at.clone();
}
metadata.generation = next_generation;
}
fn normalize_record_shape(record: &mut DynamicPluginRecord) {
record.metadata.id = record.metadata.id.trim().to_owned();
match &mut record.compatibility {
super::DynamicPluginCompatibility::RustDynamic(compatibility) => {
compatibility.relay = compatibility.relay.trim().to_owned();
compatibility.native_api = compatibility.native_api.trim().to_owned();
}
super::DynamicPluginCompatibility::Worker(compatibility) => {
compatibility.relay = compatibility.relay.trim().to_owned();
compatibility.worker_protocol = compatibility.worker_protocol.trim().to_owned();
}
}
match &mut record.load {
super::DynamicPluginLoadContract::Worker(load) => {
load.entrypoint = load.entrypoint.trim().to_owned();
}
super::DynamicPluginLoadContract::RustDynamic(load) => {
load.library = load.library.trim().to_owned();
load.symbol = load.symbol.trim().to_owned();
}
}
}
fn validate_record_shape(record: &DynamicPluginRecord) -> Result<()> {
if record.metadata.id.trim().is_empty() {
return Err(PluginError::InvalidConfig(
"dynamic plugin id must not be empty".into(),
));
}
match record.metadata.kind {
super::DynamicPluginKind::RustDynamic => {
match &record.compatibility {
super::DynamicPluginCompatibility::RustDynamic(compatibility) => {
if compatibility.relay.trim().is_empty() {
return Err(PluginError::InvalidConfig(
"dynamic plugin record must declare compat.relay".into(),
));
}
if compatibility.native_api.trim().is_empty() {
return Err(PluginError::InvalidConfig(
"dynamic rust_dynamic record has invalid compatibility shape".into(),
));
}
}
_ => {
return Err(PluginError::InvalidConfig(
"dynamic rust_dynamic record has invalid compatibility shape".into(),
));
}
}
match &record.load {
super::DynamicPluginLoadContract::RustDynamic(load)
if !load.library.trim().is_empty() && !load.symbol.trim().is_empty() => {}
_ => {
return Err(PluginError::InvalidConfig(
"dynamic rust_dynamic record has invalid load shape".into(),
));
}
}
}
super::DynamicPluginKind::Worker => {
match &record.compatibility {
super::DynamicPluginCompatibility::Worker(compatibility) => {
if compatibility.relay.trim().is_empty() {
return Err(PluginError::InvalidConfig(
"dynamic plugin record must declare compat.relay".into(),
));
}
if compatibility.worker_protocol.trim().is_empty() {
return Err(PluginError::InvalidConfig(
"dynamic worker record has invalid compatibility shape".into(),
));
}
}
_ => {
return Err(PluginError::InvalidConfig(
"dynamic worker record has invalid compatibility shape".into(),
));
}
}
match &record.load {
super::DynamicPluginLoadContract::Worker(load)
if !load.entrypoint.trim().is_empty() => {}
_ => {
return Err(PluginError::InvalidConfig(
"dynamic worker record has invalid load shape".into(),
));
}
}
}
}
Ok(())
}