use crate::vgi::VirtualGraph;
use crate::vgi::{Capability, GraphType};
use crate::vgi::{VgiError, VgiResult};
use std::any::Any;
use std::time::Duration;
#[cfg(feature = "mcp")]
use rustc_hash::FxHashMap;
use std::collections::HashMap;
#[cfg(feature = "mcp")]
pub type FastHashMap<K, V> = FxHashMap<K, V>;
#[cfg(not(feature = "mcp"))]
pub type FastHashMap<K, V> = HashMap<K, V>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum PluginPriority {
Low,
#[default]
Normal,
High,
Critical,
}
impl PluginPriority {
pub fn as_u8(&self) -> u8 {
match self {
Self::Low => 0,
Self::Normal => 1,
Self::High => 2,
Self::Critical => 3,
}
}
}
#[derive(Debug, Clone)]
pub struct PluginInfo {
pub name: String,
pub version: String,
pub description: String,
pub author: Option<String>,
pub required_capabilities: Vec<Capability>,
pub supported_graph_types: Vec<GraphType>,
pub tags: Vec<String>,
pub priority: PluginPriority,
pub config_schema: FastHashMap<String, ConfigField>,
}
#[derive(Debug, Clone)]
pub struct ConfigField {
pub name: String,
pub field_type: ConfigFieldType,
pub required: bool,
pub default_value: Option<String>,
pub description: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigFieldType {
String,
Integer,
Float,
Boolean,
List,
}
impl ConfigField {
pub fn new(name: impl Into<String>, field_type: ConfigFieldType) -> Self {
Self {
name: name.into(),
field_type,
required: false,
default_value: None,
description: String::new(),
}
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn default_value(mut self, value: impl Into<String>) -> Self {
self.default_value = Some(value.into());
self
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = desc.into();
self
}
}
impl PluginInfo {
pub fn new(
name: impl Into<String>,
version: impl Into<String>,
description: impl Into<String>,
) -> Self {
Self {
name: name.into(),
version: version.into(),
description: description.into(),
author: None,
required_capabilities: Vec::new(),
supported_graph_types: Vec::new(),
tags: Vec::new(),
priority: PluginPriority::Normal,
config_schema: FastHashMap::default(),
}
}
pub fn with_author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
pub fn with_required_capabilities(mut self, caps: &[Capability]) -> Self {
self.required_capabilities = caps.to_vec();
self
}
pub fn with_supported_graph_types(mut self, types: &[GraphType]) -> Self {
self.supported_graph_types = types.to_vec();
self
}
pub fn with_tags(mut self, tags: &[&str]) -> Self {
self.tags = tags.iter().map(|s| s.to_string()).collect();
self
}
pub fn with_priority(mut self, priority: PluginPriority) -> Self {
self.priority = priority;
self
}
pub fn with_config_field(mut self, field: ConfigField) -> Self {
self.config_schema.insert(field.name.clone(), field);
self
}
pub fn with_config_fields(mut self, fields: Vec<ConfigField>) -> Self {
for field in fields {
self.config_schema.insert(field.name.clone(), field);
}
self
}
}
pub struct PluginContext<'a, G>
where
G: VirtualGraph + ?Sized,
{
pub graph: &'a G,
pub config: FastHashMap<String, String>,
pub cancelled: bool,
pub progress_callback: Option<Box<dyn Fn(f32) + Send + 'a>>,
pub timeout: Option<Duration>,
pub start_time: Option<std::time::Instant>,
pub execution_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ExecutionConfig {
pub timeout: Option<Duration>,
pub priority: PluginPriority,
pub parameters: FastHashMap<String, String>,
pub execution_id: Option<String>,
}
impl Default for ExecutionConfig {
fn default() -> Self {
Self {
timeout: Some(Duration::from_secs(60)), priority: PluginPriority::Normal,
parameters: FastHashMap::default(),
execution_id: None,
}
}
}
impl ExecutionConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn with_priority(mut self, priority: PluginPriority) -> Self {
self.priority = priority;
self
}
pub fn with_parameter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.parameters.insert(key.into(), value.into());
self
}
pub fn with_execution_id(mut self, id: impl Into<String>) -> Self {
self.execution_id = Some(id.into());
self
}
}
impl<'a, G> PluginContext<'a, G>
where
G: VirtualGraph + ?Sized,
{
pub fn new(graph: &'a G) -> Self {
Self {
graph,
config: FastHashMap::default(),
cancelled: false,
progress_callback: None,
timeout: None,
start_time: Some(std::time::Instant::now()),
execution_id: None,
}
}
pub fn with_config_ctx(graph: &'a G, exec_config: &ExecutionConfig) -> Self {
Self {
graph,
config: exec_config.parameters.clone(),
cancelled: false,
progress_callback: None,
timeout: exec_config.timeout,
start_time: Some(std::time::Instant::now()),
execution_id: exec_config.execution_id.clone(),
}
}
pub fn with_config(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.config.insert(key.into(), value.into());
self
}
pub fn get_config(&self, key: &str) -> Option<&String> {
self.config.get(key)
}
pub fn get_config_or<'b>(&'b self, key: &str, default: &'b str) -> &'b str {
self.config.get(key).map(|s| s.as_str()).unwrap_or(default)
}
pub fn get_config_as<T: std::str::FromStr>(&self, key: &str, default: T) -> T {
self.config
.get(key)
.and_then(|s| s.parse().ok())
.unwrap_or(default)
}
pub fn with_progress_callback<F>(mut self, callback: F) -> Self
where
F: Fn(f32) + Send + 'a,
{
self.progress_callback = Some(Box::new(callback));
self
}
pub fn report_progress(&self, progress: f32) {
if let Some(callback) = &self.progress_callback {
callback(progress);
}
}
pub fn is_cancelled(&self) -> bool {
self.cancelled
}
pub fn cancel(&mut self) {
self.cancelled = true;
}
pub fn is_timeout(&self) -> bool {
if let (Some(timeout), Some(start_time)) = (self.timeout, self.start_time) {
start_time.elapsed() > timeout
} else {
false
}
}
pub fn can_continue(&self) -> bool {
!self.is_cancelled() && !self.is_timeout()
}
pub fn check_capability(&self, capability: Capability) -> bool {
self.graph.has_capability(capability)
}
pub fn check_capabilities(&self, capabilities: &[Capability]) -> bool {
self.graph.has_capabilities(capabilities)
}
pub fn validate_config(&self, schema: &FastHashMap<String, ConfigField>) -> VgiResult<()> {
for (field_name, field) in schema {
let has_value = self.config.contains_key(field_name);
if !has_value && field.required {
return Err(VgiError::ValidationError {
message: format!("Required config field '{}' is missing", field_name),
});
}
if let Some(value) = self.config.get(field_name) {
match field.field_type {
ConfigFieldType::Integer => {
if value.parse::<i64>().is_err() {
return Err(VgiError::ValidationError {
message: format!("Field '{}' must be an integer", field_name),
});
}
}
ConfigFieldType::Float => {
if value.parse::<f64>().is_err() {
return Err(VgiError::ValidationError {
message: format!("Field '{}' must be a float", field_name),
});
}
}
ConfigFieldType::Boolean => {
if !["true", "false", "1", "0"].contains(&value.to_lowercase().as_str()) {
return Err(VgiError::ValidationError {
message: format!("Field '{}' must be a boolean", field_name),
});
}
}
_ => {} }
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct AlgorithmResult {
pub name: String,
pub data: AlgorithmData,
pub metadata: FastHashMap<String, String>,
}
impl AlgorithmResult {
pub fn new(name: impl Into<String>, data: AlgorithmData) -> Self {
Self {
name: name.into(),
data,
metadata: FastHashMap::default(),
}
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone)]
pub enum AlgorithmData {
NodeValues(FastHashMap<usize, f64>),
NodeList(Vec<usize>),
EdgeList(Vec<usize>),
Communities(Vec<usize>),
Scalar(f64),
Boolean(bool),
String(String),
Custom(String),
}
impl AlgorithmData {
pub fn as_node_values(&self) -> Option<&FastHashMap<usize, f64>> {
match self {
AlgorithmData::NodeValues(values) => Some(values),
_ => None,
}
}
pub fn as_node_list(&self) -> Option<&Vec<usize>> {
match self {
AlgorithmData::NodeList(nodes) => Some(nodes),
_ => None,
}
}
pub fn as_scalar(&self) -> Option<f64> {
match self {
AlgorithmData::Scalar(value) => Some(*value),
_ => None,
}
}
pub fn as_boolean(&self) -> Option<bool> {
match self {
AlgorithmData::Boolean(value) => Some(*value),
_ => None,
}
}
}
pub trait GraphAlgorithm: Send + Sync {
fn info(&self) -> PluginInfo;
fn validate<G>(&self, ctx: &PluginContext<G>) -> VgiResult<()>
where
G: VirtualGraph + ?Sized,
{
let info = self.info();
let graph_type = ctx.graph.graph_type();
if !info.supported_graph_types.is_empty()
&& !info.supported_graph_types.contains(&graph_type)
{
return Err(VgiError::MetadataMismatch {
expected: format!(
"One of {:?}",
info.supported_graph_types
.iter()
.map(|t| t.to_string())
.collect::<Vec<_>>()
),
actual: graph_type.to_string(),
});
}
if !info.required_capabilities.is_empty()
&& !ctx.check_capabilities(&info.required_capabilities)
{
let first_capability = info.required_capabilities
.first()
.map(|s| s.as_str())
.unwrap_or("unknown");
return Err(VgiError::UnsupportedCapability {
capability: first_capability.to_string(),
backend: "unknown".to_string(),
});
}
if !info.config_schema.is_empty() {
ctx.validate_config(&info.config_schema)?;
}
Ok(())
}
fn execute<G>(&self, ctx: &mut PluginContext<G>) -> VgiResult<AlgorithmResult>
where
G: VirtualGraph + ?Sized;
fn before_execute<G>(&self, _ctx: &PluginContext<G>) -> VgiResult<()>
where
G: VirtualGraph + ?Sized,
{
Ok(())
}
fn after_execute<G>(&self, _ctx: &PluginContext<G>, _result: &AlgorithmResult) -> VgiResult<()>
where
G: VirtualGraph + ?Sized,
{
Ok(())
}
fn cleanup(&self) {}
fn as_any(&self) -> &dyn Any;
}
impl AlgorithmResult {
pub fn node_values(values: FastHashMap<usize, f64>) -> Self {
Self::new("node_values", AlgorithmData::NodeValues(values))
}
pub fn node_list(nodes: Vec<usize>) -> Self {
Self::new("node_list", AlgorithmData::NodeList(nodes))
}
pub fn scalar(value: f64) -> Self {
Self::new("scalar", AlgorithmData::Scalar(value))
}
pub fn boolean(value: bool) -> Self {
Self::new("boolean", AlgorithmData::Boolean(value))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_info() {
let info = PluginInfo::new("test_plugin", "1.0.0", "Test Plugin Description")
.with_author("Test Author")
.with_required_capabilities(&[Capability::Parallel])
.with_supported_graph_types(&[GraphType::Directed])
.with_tags(&["test", "demo"]);
assert_eq!(info.name, "test_plugin");
assert_eq!(info.version, "1.0.0");
assert_eq!(info.author, Some("Test Author".to_string()));
assert_eq!(info.required_capabilities, vec![Capability::Parallel]);
assert_eq!(info.supported_graph_types, vec![GraphType::Directed]);
assert_eq!(info.tags, vec!["test", "demo"]);
}
#[test]
fn test_algorithm_data() {
use core::f64::consts::PI;
let mut values = FastHashMap::default();
values.insert(0, 1.0);
values.insert(1, 2.0);
let data = AlgorithmData::NodeValues(values.clone());
assert_eq!(data.as_node_values(), Some(&values));
assert_eq!(data.as_node_list(), None);
assert_eq!(data.as_scalar(), None);
let data = AlgorithmData::Scalar(PI);
assert_eq!(data.as_scalar(), Some(PI));
assert_eq!(data.as_node_values(), None);
}
#[test]
fn test_algorithm_result() {
let mut values = FastHashMap::default();
values.insert(0, 1.0);
let result = AlgorithmResult::node_values(values)
.with_metadata("iterations", "10")
.with_metadata("converged", "true");
assert_eq!(result.name, "node_values");
assert!(result.data.as_node_values().is_some());
assert_eq!(result.metadata.get("iterations"), Some(&"10".to_string()));
}
#[test]
fn test_plugin_priority() {
assert_eq!(PluginPriority::Low.as_u8(), 0);
assert_eq!(PluginPriority::Normal.as_u8(), 1);
assert_eq!(PluginPriority::High.as_u8(), 2);
assert_eq!(PluginPriority::Critical.as_u8(), 3);
assert!(PluginPriority::High > PluginPriority::Normal);
assert!(PluginPriority::Critical > PluginPriority::High);
}
#[test]
fn test_plugin_info_with_priority() {
let info = PluginInfo::new("test", "1.0.0", "Test").with_priority(PluginPriority::High);
assert_eq!(info.priority, PluginPriority::High);
}
#[test]
fn test_config_field() {
let field = ConfigField::new("damping", ConfigFieldType::Float)
.required(true)
.default_value("0.85")
.description("Damping factor");
assert_eq!(field.name, "damping");
assert_eq!(field.field_type, ConfigFieldType::Float);
assert!(field.required);
assert_eq!(field.default_value, Some("0.85".to_string()));
}
#[test]
fn test_execution_config() {
use std::time::Duration;
let config = ExecutionConfig::new()
.with_timeout(Duration::from_secs(120))
.with_priority(PluginPriority::High)
.with_parameter("key", "value")
.with_execution_id("test-123");
assert_eq!(config.timeout, Some(Duration::from_secs(120)));
assert_eq!(config.priority, PluginPriority::High);
assert_eq!(config.parameters.get("key"), Some(&"value".to_string()));
assert_eq!(config.execution_id, Some("test-123".to_string()));
}
#[test]
fn test_plugin_context_timeout() {
use crate::graph::Graph;
use crate::graph::traits::GraphOps;
use std::time::Duration;
let graph = Graph::<String, f64>::directed();
let mut ctx = PluginContext::new(&graph);
assert!(!ctx.is_timeout());
ctx.timeout = Some(Duration::from_millis(10));
std::thread::sleep(Duration::from_millis(50));
assert!(ctx.is_timeout());
}
#[test]
fn test_plugin_context_validate_config() {
use crate::graph::Graph;
use crate::graph::traits::GraphOps;
let graph = Graph::<String, f64>::directed();
let mut ctx = PluginContext::new(&graph);
let mut schema = FastHashMap::default();
schema.insert(
"damping".to_string(),
ConfigField::new("damping", ConfigFieldType::Float),
);
schema.insert(
"max_iter".to_string(),
ConfigField::new("max_iter", ConfigFieldType::Integer),
);
assert!(ctx.validate_config(&schema).is_ok());
schema.insert(
"required_field".to_string(),
ConfigField::new("required_field", ConfigFieldType::String).required(true),
);
assert!(ctx.validate_config(&schema).is_err());
ctx.config
.insert("required_field".to_string(), "value".to_string());
assert!(ctx.validate_config(&schema).is_ok());
ctx.config
.insert("damping".to_string(), "not_a_float".to_string());
assert!(ctx.validate_config(&schema).is_err());
ctx.config.insert("damping".to_string(), "0.85".to_string());
ctx.config
.insert("max_iter".to_string(), "not_an_int".to_string());
assert!(ctx.validate_config(&schema).is_err());
}
}