use crate::{Cap, CapUrn, CapSet, StdinSource};
use crate::media_urn::MediaUrn;
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, thiserror::Error)]
pub enum CapMatrixError {
#[error("No cap sets found for capability: {0}")]
NoSetsFound(String),
#[error("Invalid capability URN: {0}")]
InvalidUrn(String),
#[error("Registry error: {0}")]
RegistryError(String),
}
#[derive(Debug, Clone)]
pub struct CapGraphEdge {
pub from_spec: String,
pub to_spec: String,
pub cap: Cap,
pub registry_name: String,
pub specificity: usize,
}
#[derive(Debug, Clone)]
pub struct CapGraph {
edges: Vec<CapGraphEdge>,
outgoing: HashMap<String, Vec<usize>>,
incoming: HashMap<String, Vec<usize>>,
nodes: HashSet<String>,
}
impl CapGraph {
pub fn new() -> Self {
Self {
edges: Vec::new(),
outgoing: HashMap::new(),
incoming: HashMap::new(),
nodes: HashSet::new(),
}
}
pub fn add_cap(&mut self, cap: &Cap, registry_name: &str) {
let from_spec = cap.urn.in_spec().to_string();
let to_spec = cap.urn.out_spec().to_string();
let specificity = cap.urn.specificity();
self.nodes.insert(from_spec.clone());
self.nodes.insert(to_spec.clone());
let edge_index = self.edges.len();
let edge = CapGraphEdge {
from_spec: from_spec.clone(),
to_spec: to_spec.clone(),
cap: cap.clone(),
registry_name: registry_name.to_string(),
specificity,
};
self.edges.push(edge);
self.outgoing.entry(from_spec).or_default().push(edge_index);
self.incoming.entry(to_spec).or_default().push(edge_index);
}
pub fn build_from_registries(
registries: &[(String, std::sync::Arc<std::sync::RwLock<CapMatrix>>)]
) -> Result<Self, CapMatrixError> {
let mut graph = Self::new();
for (registry_name, registry_arc) in registries {
let registry = registry_arc.read()
.map_err(|_| CapMatrixError::RegistryError(
format!("Failed to acquire read lock for registry '{}'", registry_name)
))?;
for entry in registry.sets.values() {
for cap in &entry.capabilities {
graph.add_cap(cap, registry_name);
}
}
}
Ok(graph)
}
pub fn get_nodes(&self) -> &HashSet<String> {
&self.nodes
}
pub fn get_edges(&self) -> &[CapGraphEdge] {
&self.edges
}
pub fn get_outgoing(&self, spec: &str) -> Vec<&CapGraphEdge> {
let provided_urn = match MediaUrn::from_string(spec) {
Ok(urn) => urn,
Err(_) => return Vec::new(),
};
self.edges
.iter()
.filter(|edge| {
match MediaUrn::from_string(&edge.from_spec) {
Ok(requirement_urn) => provided_urn.matches(&requirement_urn).expect("MediaUrn prefix mismatch impossible"),
Err(_) => false,
}
})
.collect()
}
pub fn get_incoming(&self, spec: &str) -> Vec<&CapGraphEdge> {
let requirement_urn = match MediaUrn::from_string(spec) {
Ok(urn) => urn,
Err(_) => return Vec::new(),
};
self.edges
.iter()
.filter(|edge| {
match MediaUrn::from_string(&edge.to_spec) {
Ok(produced_urn) => produced_urn.matches(&requirement_urn).expect("MediaUrn prefix mismatch impossible"),
Err(_) => false,
}
})
.collect()
}
pub fn has_direct_edge(&self, from_spec: &str, to_spec: &str) -> bool {
let to_requirement = match MediaUrn::from_string(to_spec) {
Ok(urn) => urn,
Err(_) => return false,
};
self.get_outgoing(from_spec)
.iter()
.any(|edge| {
match MediaUrn::from_string(&edge.to_spec) {
Ok(produced_urn) => produced_urn.matches(&to_requirement).expect("MediaUrn prefix mismatch impossible"),
Err(_) => false,
}
})
}
pub fn get_direct_edges(&self, from_spec: &str, to_spec: &str) -> Vec<&CapGraphEdge> {
let to_requirement = match MediaUrn::from_string(to_spec) {
Ok(urn) => urn,
Err(_) => return Vec::new(),
};
let mut edges: Vec<&CapGraphEdge> = self.get_outgoing(from_spec)
.into_iter()
.filter(|edge| {
match MediaUrn::from_string(&edge.to_spec) {
Ok(produced_urn) => produced_urn.matches(&to_requirement).expect("MediaUrn prefix mismatch impossible"),
Err(_) => false,
}
})
.collect();
edges.sort_by(|a, b| b.specificity.cmp(&a.specificity));
edges
}
pub fn can_convert(&self, from_spec: &str, to_spec: &str) -> bool {
if from_spec == to_spec {
return true;
}
let to_requirement = match MediaUrn::from_string(to_spec) {
Ok(urn) => urn,
Err(_) => return false,
};
let initial_edges = self.get_outgoing(from_spec);
if initial_edges.is_empty() {
return false;
}
let mut visited = HashSet::new();
let mut queue: VecDeque<String> = VecDeque::new();
for edge in &initial_edges {
if let Ok(produced_urn) = MediaUrn::from_string(&edge.to_spec) {
if produced_urn.matches(&to_requirement).expect("MediaUrn prefix mismatch impossible") {
return true;
}
}
if !visited.contains(&edge.to_spec) {
visited.insert(edge.to_spec.clone());
queue.push_back(edge.to_spec.clone());
}
}
while let Some(current) = queue.pop_front() {
for edge in self.get_outgoing(¤t) {
if let Ok(produced_urn) = MediaUrn::from_string(&edge.to_spec) {
if produced_urn.matches(&to_requirement).expect("MediaUrn prefix mismatch impossible") {
return true;
}
}
if !visited.contains(&edge.to_spec) {
visited.insert(edge.to_spec.clone());
queue.push_back(edge.to_spec.clone());
}
}
}
false
}
pub fn find_path(&self, from_spec: &str, to_spec: &str) -> Option<Vec<&CapGraphEdge>> {
if from_spec == to_spec {
return Some(Vec::new());
}
let to_requirement = match MediaUrn::from_string(to_spec) {
Ok(urn) => urn,
Err(_) => return None,
};
let mut visited: HashMap<String, Option<(String, usize)>> = HashMap::new();
let mut queue: VecDeque<String> = VecDeque::new();
let initial_edges = self.get_outgoing(from_spec);
if initial_edges.is_empty() {
return None;
}
for edge in &initial_edges {
let edge_idx = self.edges.iter().position(|e| std::ptr::eq(e, *edge))?;
if let Ok(produced_urn) = MediaUrn::from_string(&edge.to_spec) {
if produced_urn.matches(&to_requirement).expect("MediaUrn prefix mismatch impossible") {
return Some(vec![&self.edges[edge_idx]]);
}
}
if !visited.contains_key(&edge.to_spec) {
visited.insert(edge.to_spec.clone(), Some((from_spec.to_string(), edge_idx)));
queue.push_back(edge.to_spec.clone());
}
}
while let Some(current) = queue.pop_front() {
for edge in self.get_outgoing(¤t) {
let edge_idx = self.edges.iter().position(|e| std::ptr::eq(e, edge))?;
if let Ok(produced_urn) = MediaUrn::from_string(&edge.to_spec) {
if produced_urn.matches(&to_requirement).expect("MediaUrn prefix mismatch impossible") {
let mut path_indices = vec![edge_idx];
let mut backtrack = current.clone();
while let Some(Some((prev, prev_edge_idx))) = visited.get(&backtrack) {
path_indices.push(*prev_edge_idx);
backtrack = prev.clone();
}
path_indices.reverse();
return Some(path_indices.iter().map(|&i| &self.edges[i]).collect());
}
}
if !visited.contains_key(&edge.to_spec) {
visited.insert(edge.to_spec.clone(), Some((current.clone(), edge_idx)));
queue.push_back(edge.to_spec.clone());
}
}
}
None
}
pub fn find_all_paths(
&self,
from_spec: &str,
to_spec: &str,
max_depth: usize,
) -> Vec<Vec<&CapGraphEdge>> {
let to_requirement = match MediaUrn::from_string(to_spec) {
Ok(urn) => urn,
Err(_) => return Vec::new(),
};
let initial_edges = self.get_outgoing(from_spec);
if initial_edges.is_empty() {
return Vec::new();
}
let mut all_paths = Vec::new();
let mut current_path: Vec<usize> = Vec::new();
let mut visited = HashSet::new();
self.dfs_find_paths(
from_spec,
&to_requirement,
max_depth,
&mut current_path,
&mut visited,
&mut all_paths,
);
all_paths.sort_by(|a, b| a.len().cmp(&b.len()));
all_paths
.into_iter()
.map(|indices| indices.into_iter().map(|i| &self.edges[i]).collect())
.collect()
}
fn dfs_find_paths(
&self,
current: &str,
target: &MediaUrn,
remaining_depth: usize,
current_path: &mut Vec<usize>,
visited: &mut HashSet<String>,
all_paths: &mut Vec<Vec<usize>>,
) {
if remaining_depth == 0 {
return;
}
for edge in self.get_outgoing(current) {
let edge_idx = match self.edges.iter().position(|e| std::ptr::eq(e, edge)) {
Some(idx) => idx,
None => continue,
};
let output_satisfies = match MediaUrn::from_string(&edge.to_spec) {
Ok(produced) => produced.matches(target).expect("MediaUrn prefix mismatch impossible"),
Err(_) => false,
};
if output_satisfies {
let mut path = current_path.clone();
path.push(edge_idx);
all_paths.push(path);
} else if !visited.contains(&edge.to_spec) {
visited.insert(edge.to_spec.clone());
current_path.push(edge_idx);
self.dfs_find_paths(
&edge.to_spec,
target,
remaining_depth - 1,
current_path,
visited,
all_paths,
);
current_path.pop();
visited.remove(&edge.to_spec);
}
}
}
pub fn find_best_path(&self, from_spec: &str, to_spec: &str, max_depth: usize) -> Option<Vec<&CapGraphEdge>> {
let all_paths = self.find_all_paths(from_spec, to_spec, max_depth);
all_paths
.into_iter()
.max_by_key(|path| path.iter().map(|e| e.specificity).sum::<usize>())
}
pub fn get_input_specs(&self) -> Vec<&str> {
self.outgoing.keys().map(|s| s.as_str()).collect()
}
pub fn get_output_specs(&self) -> Vec<&str> {
self.incoming.keys().map(|s| s.as_str()).collect()
}
pub fn stats(&self) -> CapGraphStats {
CapGraphStats {
node_count: self.nodes.len(),
edge_count: self.edges.len(),
input_spec_count: self.outgoing.len(),
output_spec_count: self.incoming.len(),
}
}
}
impl Default for CapGraph {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct CapGraphStats {
pub node_count: usize,
pub edge_count: usize,
pub input_spec_count: usize,
pub output_spec_count: usize,
}
#[derive(Debug)]
pub struct CapMatrix {
pub(crate) sets: HashMap<String, CapSetEntry>,
pub(crate) media_registry: std::sync::Arc<crate::media_registry::MediaUrnRegistry>,
}
#[derive(Debug)]
pub(crate) struct CapSetEntry {
pub(crate) name: String,
pub(crate) host: std::sync::Arc<dyn CapSet>,
pub(crate) capabilities: Vec<Cap>,
}
impl CapMatrix {
pub fn new(media_registry: std::sync::Arc<crate::media_registry::MediaUrnRegistry>) -> Self {
Self {
sets: HashMap::new(),
media_registry,
}
}
pub fn register_cap_set(
&mut self,
name: String,
host: Box<dyn CapSet>,
capabilities: Vec<Cap>,
) -> Result<(), CapMatrixError> {
let entry = CapSetEntry {
name: name.clone(),
host: std::sync::Arc::from(host),
capabilities,
};
self.sets.insert(name, entry);
Ok(())
}
pub fn find_cap_sets(&self, request_urn: &str) -> Result<Vec<&dyn CapSet>, CapMatrixError> {
let request = CapUrn::from_string(request_urn)
.map_err(|e| CapMatrixError::InvalidUrn(format!("{}: {}", request_urn, e)))?;
let mut matching_sets = Vec::new();
for entry in self.sets.values() {
for cap in &entry.capabilities {
if cap.urn.matches(&request) {
matching_sets.push(entry.host.as_ref());
break; }
}
}
if matching_sets.is_empty() {
return Err(CapMatrixError::NoSetsFound(request_urn.to_string()));
}
Ok(matching_sets)
}
pub fn find_best_cap_set(&self, request_urn: &str) -> Result<(std::sync::Arc<dyn CapSet>, &Cap), CapMatrixError> {
let request = CapUrn::from_string(request_urn)
.map_err(|e| CapMatrixError::InvalidUrn(format!("{}: {}", request_urn, e)))?;
let mut best_match: Option<(std::sync::Arc<dyn CapSet>, &Cap, usize)> = None;
for entry in self.sets.values() {
for cap in &entry.capabilities {
if cap.urn.matches(&request) {
let specificity = cap.urn.specificity();
match best_match {
None => {
best_match = Some((entry.host.clone(), cap, specificity));
}
Some((_, _, current_specificity)) => {
if specificity > current_specificity {
best_match = Some((entry.host.clone(), cap, specificity));
}
}
}
break; }
}
}
match best_match {
Some((host, cap, _)) => Ok((host, cap)),
None => Err(CapMatrixError::NoSetsFound(request_urn.to_string())),
}
}
pub fn get_host_names(&self) -> Vec<String> {
self.sets.keys().cloned().collect()
}
pub fn get_all_capabilities(&self) -> Vec<&Cap> {
self.sets.values()
.flat_map(|entry| &entry.capabilities)
.collect()
}
pub fn get_capabilities_for_host(&self, host_name: &str) -> Option<&[Cap]> {
self.sets.get(host_name).map(|entry| entry.capabilities.as_slice())
}
pub fn iter_hosts_and_caps(&self) -> impl Iterator<Item = (&str, &[Cap])> {
self.sets.iter().map(|(name, entry)| (name.as_str(), entry.capabilities.as_slice()))
}
pub fn can_handle(&self, request_urn: &str) -> bool {
self.find_cap_sets(request_urn).is_ok()
}
pub fn unregister_cap_set(&mut self, name: &str) -> bool {
self.sets.remove(name).is_some()
}
pub fn clear(&mut self) {
self.sets.clear();
}
}
use crate::CapCaller;
#[derive(Debug, Clone)]
pub struct BestCapSetMatch {
pub cap: Cap,
pub specificity: usize,
pub registry_name: String,
}
#[derive(Debug)]
pub struct CapCube {
registries: Vec<(String, std::sync::Arc<std::sync::RwLock<CapMatrix>>)>,
media_registry: std::sync::Arc<crate::media_registry::MediaUrnRegistry>,
}
#[derive(Debug)]
pub struct CompositeCapSet {
registries: Vec<(String, std::sync::Arc<std::sync::RwLock<CapMatrix>>)>,
}
impl CompositeCapSet {
fn new(registries: Vec<(String, std::sync::Arc<std::sync::RwLock<CapMatrix>>)>) -> Self {
Self { registries }
}
pub fn graph(&self) -> Result<CapGraph, CapMatrixError> {
CapGraph::build_from_registries(&self.registries)
}
pub fn registries(&self) -> &[(String, std::sync::Arc<std::sync::RwLock<CapMatrix>>)] {
&self.registries
}
}
impl CapSet for CompositeCapSet {
fn execute_cap(
&self,
cap_urn: &str,
positional_args: &[String],
named_args: &[(String, String)],
stdin_source: Option<StdinSource>
) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<(Option<Vec<u8>>, Option<String>)>> + Send + '_>> {
let cap_urn = cap_urn.to_string();
let positional_args = positional_args.to_vec();
let named_args = named_args.to_vec();
let best_cap_set: std::sync::Arc<dyn CapSet> = {
let request = match CapUrn::from_string(&cap_urn) {
Ok(r) => r,
Err(e) => {
return Box::pin(async move {
Err(anyhow::anyhow!("Invalid cap URN '{}': {}", cap_urn, e))
});
}
};
let mut best_match: Option<(std::sync::Arc<dyn CapSet>, usize)> = None;
for (_registry_name, registry_arc) in &self.registries {
let registry = match registry_arc.read() {
Ok(r) => r,
Err(_) => continue,
};
for entry in registry.sets.values() {
for cap in &entry.capabilities {
if cap.urn.matches(&request) {
let specificity = cap.urn.specificity();
match &best_match {
None => {
best_match = Some((entry.host.clone(), specificity));
}
Some((_, current_specificity)) => {
if specificity > *current_specificity {
best_match = Some((entry.host.clone(), specificity));
}
}
}
break;
}
}
}
}
match best_match {
Some((host_arc, _)) => host_arc,
None => {
return Box::pin(async move {
Err(anyhow::anyhow!("No capability host found for '{}'", cap_urn))
});
}
}
};
Box::pin(async move {
best_cap_set.execute_cap(&cap_urn, &positional_args, &named_args, stdin_source).await
})
}
}
impl CapCube {
pub fn new(media_registry: std::sync::Arc<crate::media_registry::MediaUrnRegistry>) -> Self {
Self {
registries: Vec::new(),
media_registry,
}
}
pub fn add_registry(&mut self, name: String, registry: std::sync::Arc<std::sync::RwLock<CapMatrix>>) {
self.registries.push((name, registry));
}
pub fn remove_registry(&mut self, name: &str) -> Option<std::sync::Arc<std::sync::RwLock<CapMatrix>>> {
if let Some(pos) = self.registries.iter().position(|(n, _)| n == name) {
Some(self.registries.remove(pos).1)
} else {
None
}
}
pub fn get_registry(&self, name: &str) -> Option<std::sync::Arc<std::sync::RwLock<CapMatrix>>> {
self.registries.iter()
.find(|(n, _)| n == name)
.map(|(_, r)| r.clone())
}
pub fn can(&self, cap_urn: &str) -> Result<CapCaller, CapMatrixError> {
let best_match = self.find_best_cap_set(cap_urn)?;
let composite_host = CompositeCapSet::new(self.registries.clone());
Ok(CapCaller::new(
cap_urn.to_string(),
Box::new(composite_host),
best_match.cap,
self.media_registry.clone(),
))
}
pub fn find_best_cap_set(&self, request_urn: &str) -> Result<BestCapSetMatch, CapMatrixError> {
let request = CapUrn::from_string(request_urn)
.map_err(|e| CapMatrixError::InvalidUrn(format!("{}: {}", request_urn, e)))?;
let mut best_overall: Option<BestCapSetMatch> = None;
for (registry_name, registry_arc) in &self.registries {
let registry = registry_arc.read()
.map_err(|_| CapMatrixError::RegistryError("Failed to acquire read lock".to_string()))?;
if let Some((cap, specificity)) = Self::find_best_in_registry(®istry, &request) {
let candidate = BestCapSetMatch {
cap: cap.clone(),
specificity,
registry_name: registry_name.clone(),
};
match &best_overall {
None => {
best_overall = Some(candidate);
}
Some(current_best) => {
if specificity > current_best.specificity {
best_overall = Some(candidate);
}
}
}
}
}
best_overall.ok_or_else(|| CapMatrixError::NoSetsFound(request_urn.to_string()))
}
pub fn can_handle(&self, request_urn: &str) -> bool {
self.find_best_cap_set(request_urn).is_ok()
}
pub fn get_registry_names(&self) -> Vec<&str> {
self.registries.iter().map(|(n, _)| n.as_str()).collect()
}
pub fn graph(&self) -> Result<CapGraph, CapMatrixError> {
CapGraph::build_from_registries(&self.registries)
}
fn find_best_in_registry<'a>(
registry: &'a CapMatrix,
request: &CapUrn
) -> Option<(&'a Cap, usize)> {
let mut best: Option<(&Cap, usize)> = None;
for entry in registry.sets.values() {
for cap in &entry.capabilities {
if cap.urn.matches(request) {
let specificity = cap.urn.specificity();
match best {
None => {
best = Some((cap, specificity));
}
Some((_, current_specificity)) => {
if specificity > current_specificity {
best = Some((cap, specificity));
}
}
}
break; }
}
}
best
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CapOutput;
use crate::standard::media::{MEDIA_STRING, MEDIA_OBJECT, MEDIA_BINARY};
use crate::media_registry::MediaUrnRegistry;
use std::pin::Pin;
use std::future::Future;
use std::collections::HashMap;
use tempfile::TempDir;
fn test_media_registry() -> (std::sync::Arc<MediaUrnRegistry>, TempDir) {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().join("media");
let registry = MediaUrnRegistry::for_testing(cache_dir).unwrap();
(std::sync::Arc::new(registry), temp_dir)
}
fn test_urn(tags: &str) -> String {
format!(r#"cap:in="media:void";out="media:form=map";{}"#, tags)
}
#[derive(Debug)]
struct MockCapSet {
name: String,
}
impl CapSet for MockCapSet {
fn execute_cap(
&self,
_cap_urn: &str,
_positional_args: &[String],
_named_args: &[(String, String)],
_stdin_source: Option<StdinSource>
) -> Pin<Box<dyn Future<Output = anyhow::Result<(Option<Vec<u8>>, Option<String>)>> + Send + '_>> {
Box::pin(async move {
Ok((None, Some(format!("Mock response from {}", self.name))))
})
}
}
#[tokio::test]
async fn test_register_and_find_cap_set() {
let (media_registry, _temp_dir) = test_media_registry();
let mut registry = CapMatrix::new(media_registry);
let host = Box::new(MockCapSet {
name: "test-host".to_string(),
});
let cap = Cap {
urn: CapUrn::from_string(&test_urn("op=test;basic")).unwrap(),
title: "Test Basic Capability".to_string(),
cap_description: Some("Test capability".to_string()),
metadata: HashMap::new(),
command: "test".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: Some(CapOutput::new(MEDIA_STRING, "Test output")),
metadata_json: None,
registered_by: None,
};
registry.register_cap_set("test-host".to_string(), host, vec![cap]).unwrap();
let sets = registry.find_cap_sets(&test_urn("op=test;basic")).unwrap();
assert_eq!(sets.len(), 1);
let sets = registry.find_cap_sets(&test_urn("op=test;basic;model=gpt-4")).unwrap();
assert_eq!(sets.len(), 1);
assert!(registry.find_cap_sets(&test_urn("op=different")).is_err());
}
#[tokio::test]
async fn test_best_cap_set_selection() {
let (media_registry, _temp_dir) = test_media_registry();
let mut registry = CapMatrix::new(media_registry);
let general_host = Box::new(MockCapSet {
name: "general".to_string(),
});
let general_cap = Cap {
urn: CapUrn::from_string(&test_urn("op=generate")).unwrap(),
title: "General Generation Capability".to_string(),
cap_description: Some("General generation".to_string()),
metadata: HashMap::new(),
command: "generate".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: Some(CapOutput::new(MEDIA_STRING, "General output")),
metadata_json: None,
registered_by: None,
};
let specific_host = Box::new(MockCapSet {
name: "specific".to_string(),
});
let specific_cap = Cap {
urn: CapUrn::from_string(&test_urn("op=generate;text;model=gpt-4")).unwrap(),
title: "Specific Text Generation Capability".to_string(),
cap_description: Some("Specific text generation".to_string()),
metadata: HashMap::new(),
command: "generate".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: Some(CapOutput::new(MEDIA_STRING, "Specific output")),
metadata_json: None,
registered_by: None,
};
registry.register_cap_set("general".to_string(), general_host, vec![general_cap]).unwrap();
registry.register_cap_set("specific".to_string(), specific_host, vec![specific_cap]).unwrap();
let (_best_host, _best_cap) = registry.find_best_cap_set(&test_urn("op=generate;text;model=gpt-4;temperature=low")).unwrap();
let all_sets = registry.find_cap_sets(&test_urn("op=generate;text;model=gpt-4;temperature=low")).unwrap();
assert_eq!(all_sets.len(), 2);
}
#[tokio::test]
async fn test_invalid_urn_handling() {
let (media_registry, _temp_dir) = test_media_registry();
let registry = CapMatrix::new(media_registry);
let result = registry.find_cap_sets("invalid-urn");
assert!(matches!(result, Err(CapMatrixError::InvalidUrn(_))));
}
#[tokio::test]
async fn test_can_handle() {
let (media_registry, _temp_dir) = test_media_registry();
let mut registry = CapMatrix::new(media_registry);
assert!(!registry.can_handle(&test_urn("op=test")));
let host = Box::new(MockCapSet {
name: "test".to_string(),
});
let cap = Cap {
urn: CapUrn::from_string(&test_urn("op=test")).unwrap(),
title: "Test Capability".to_string(),
cap_description: Some("Test".to_string()),
metadata: HashMap::new(),
command: "test".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
registry.register_cap_set("test".to_string(), host, vec![cap]).unwrap();
assert!(registry.can_handle(&test_urn("op=test")));
assert!(registry.can_handle(&test_urn("op=test;extra=param")));
assert!(!registry.can_handle(&test_urn("op=different")));
}
use std::sync::{Arc, RwLock};
fn make_cap(urn: &str, title: &str) -> Cap {
Cap {
urn: CapUrn::from_string(urn).unwrap(),
title: title.to_string(),
cap_description: Some(title.to_string()),
metadata: HashMap::new(),
command: "test".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: Some(CapOutput::new(MEDIA_STRING, "output")),
metadata_json: None,
registered_by: None,
}
}
#[tokio::test]
async fn test_cap_cube_more_specific_wins() {
let (media_registry, _temp_dir) = test_media_registry();
let mut provider_registry = CapMatrix::new(media_registry.clone());
let mut plugin_registry = CapMatrix::new(media_registry.clone());
let provider_host = Box::new(MockCapSet { name: "provider".to_string() });
let provider_cap = make_cap(
r#"cap:in="media:binary";op=generate_thumbnail;out="media:binary""#,
"Provider Thumbnail Generator (generic)"
);
provider_registry.register_cap_set(
"provider".to_string(),
provider_host,
vec![provider_cap]
).unwrap();
let plugin_host = Box::new(MockCapSet { name: "plugin".to_string() });
let plugin_cap = make_cap(
r#"cap:ext=pdf;in="media:binary";op=generate_thumbnail;out="media:binary""#,
"Plugin PDF Thumbnail Generator (specific)"
);
plugin_registry.register_cap_set(
"plugin".to_string(),
plugin_host,
vec![plugin_cap]
).unwrap();
let mut composite = CapCube::new(media_registry.clone());
composite.add_registry("providers".to_string(), Arc::new(RwLock::new(provider_registry)));
composite.add_registry("plugins".to_string(), Arc::new(RwLock::new(plugin_registry)));
let request = r#"cap:ext=pdf;in="media:binary";op=generate_thumbnail;out="media:binary""#;
let best = composite.find_best_cap_set(request).unwrap();
assert_eq!(best.registry_name, "plugins", "More specific plugin should win over less specific provider");
assert_eq!(best.specificity, 4, "Plugin cap has 4 specific tags");
assert_eq!(best.cap.title, "Plugin PDF Thumbnail Generator (specific)");
}
#[tokio::test]
async fn test_cap_cube_tie_goes_to_first() {
let (media_registry, _temp_dir) = test_media_registry();
let mut registry1 = CapMatrix::new(media_registry.clone());
let mut registry2 = CapMatrix::new(media_registry.clone());
let host1 = Box::new(MockCapSet { name: "host1".to_string() });
let cap1 = make_cap(&test_urn("op=generate;ext=pdf"), "Registry 1 Cap");
registry1.register_cap_set("host1".to_string(), host1, vec![cap1]).unwrap();
let host2 = Box::new(MockCapSet { name: "host2".to_string() });
let cap2 = make_cap(&test_urn("op=generate;ext=pdf"), "Registry 2 Cap");
registry2.register_cap_set("host2".to_string(), host2, vec![cap2]).unwrap();
let mut composite = CapCube::new(media_registry.clone());
composite.add_registry("first".to_string(), Arc::new(RwLock::new(registry1)));
composite.add_registry("second".to_string(), Arc::new(RwLock::new(registry2)));
let best = composite.find_best_cap_set(&test_urn("op=generate;ext=pdf")).unwrap();
assert_eq!(best.registry_name, "first", "On tie, first registry should win");
assert_eq!(best.cap.title, "Registry 1 Cap");
}
#[tokio::test]
async fn test_cap_cube_polls_all() {
let (media_registry, _temp_dir) = test_media_registry();
let mut registry1 = CapMatrix::new(media_registry.clone());
let mut registry2 = CapMatrix::new(media_registry.clone());
let mut registry3 = CapMatrix::new(media_registry.clone());
let host1 = Box::new(MockCapSet { name: "host1".to_string() });
let cap1 = make_cap(&test_urn("op=different"), "Registry 1");
registry1.register_cap_set("host1".to_string(), host1, vec![cap1]).unwrap();
let host2 = Box::new(MockCapSet { name: "host2".to_string() });
let cap2 = make_cap(&test_urn("op=generate"), "Registry 2");
registry2.register_cap_set("host2".to_string(), host2, vec![cap2]).unwrap();
let host3 = Box::new(MockCapSet { name: "host3".to_string() });
let cap3 = make_cap(&test_urn("op=generate;ext=pdf;format=thumbnail"), "Registry 3");
registry3.register_cap_set("host3".to_string(), host3, vec![cap3]).unwrap();
let mut composite = CapCube::new(media_registry.clone());
composite.add_registry("r1".to_string(), Arc::new(RwLock::new(registry1)));
composite.add_registry("r2".to_string(), Arc::new(RwLock::new(registry2)));
composite.add_registry("r3".to_string(), Arc::new(RwLock::new(registry3)));
let best = composite.find_best_cap_set(&test_urn("op=generate;ext=pdf;format=thumbnail")).unwrap();
assert_eq!(best.registry_name, "r3", "Most specific registry should win");
}
#[tokio::test]
async fn test_cap_cube_no_match() {
let (media_registry, _temp_dir) = test_media_registry();
let registry = CapMatrix::new(media_registry.clone());
let mut composite = CapCube::new(media_registry.clone());
composite.add_registry("empty".to_string(), Arc::new(RwLock::new(registry)));
let result = composite.find_best_cap_set(&test_urn("op=nonexistent"));
assert!(matches!(result, Err(CapMatrixError::NoSetsFound(_))));
}
#[tokio::test]
async fn test_cap_cube_fallback_scenario() {
let (media_registry, _temp_dir) = test_media_registry();
let mut provider_registry = CapMatrix::new(media_registry.clone());
let mut plugin_registry = CapMatrix::new(media_registry.clone());
let provider_host = Box::new(MockCapSet { name: "provider_fallback".to_string() });
let provider_cap = make_cap(
r#"cap:in="media:binary";op=generate_thumbnail;out="media:binary""#,
"Generic Thumbnail Provider"
);
provider_registry.register_cap_set(
"provider_fallback".to_string(),
provider_host,
vec![provider_cap]
).unwrap();
let plugin_host = Box::new(MockCapSet { name: "pdf_plugin".to_string() });
let plugin_cap = make_cap(
r#"cap:ext=pdf;in="media:binary";op=generate_thumbnail;out="media:binary""#,
"PDF Thumbnail Plugin"
);
plugin_registry.register_cap_set(
"pdf_plugin".to_string(),
plugin_host,
vec![plugin_cap]
).unwrap();
let mut composite = CapCube::new(media_registry.clone());
composite.add_registry("providers".to_string(), Arc::new(RwLock::new(provider_registry)));
composite.add_registry("plugins".to_string(), Arc::new(RwLock::new(plugin_registry)));
let request = r#"cap:ext=pdf;in="media:binary";op=generate_thumbnail;out="media:binary""#;
let best = composite.find_best_cap_set(request).unwrap();
assert_eq!(best.registry_name, "plugins");
assert_eq!(best.cap.title, "PDF Thumbnail Plugin");
assert_eq!(best.specificity, 4);
let request_wav = r#"cap:ext=wav;in="media:binary";op=generate_thumbnail;out="media:binary""#;
let best_wav = composite.find_best_cap_set(request_wav).unwrap();
assert_eq!(best_wav.registry_name, "providers");
assert_eq!(best_wav.cap.title, "Generic Thumbnail Provider");
}
#[tokio::test]
async fn test_composite_can_method() {
let (media_registry, _temp_dir) = test_media_registry();
let mut provider_registry = CapMatrix::new(media_registry.clone());
let provider_host = Box::new(MockCapSet { name: "test_provider".to_string() });
let provider_cap = make_cap(
&test_urn("op=generate;ext=pdf"),
"Test Provider"
);
provider_registry.register_cap_set(
"test_provider".to_string(),
provider_host,
vec![provider_cap]
).unwrap();
let mut composite = CapCube::new(media_registry.clone());
composite.add_registry("providers".to_string(), Arc::new(RwLock::new(provider_registry)));
let _caller = composite.can(&test_urn("op=generate;ext=pdf")).unwrap();
assert!(composite.can_handle(&test_urn("op=generate;ext=pdf")));
assert!(!composite.can_handle(&test_urn("op=nonexistent")));
}
#[test]
fn test_cap_graph_basic_construction() {
let mut graph = CapGraph::new();
let media_binary = "media:bytes";
let cap = Cap {
urn: CapUrn::from_string(&format!(r#"cap:in="{}";op=extract_text;out="{}""#, media_binary, MEDIA_STRING)).unwrap(),
title: "Text Extractor".to_string(),
cap_description: Some("Extract text from binary".to_string()),
metadata: HashMap::new(),
command: "extract".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: Some(CapOutput::new(MEDIA_STRING, "output")),
metadata_json: None,
registered_by: None,
};
graph.add_cap(&cap, "test_registry");
assert!(graph.get_nodes().len() >= 2, "Should have at least 2 nodes");
assert!(graph.get_edges().len() >= 1, "Should have at least 1 edge");
assert!(graph.has_direct_edge(media_binary, MEDIA_STRING), "Should have edge from binary to string");
}
#[test]
fn test_cap_graph_outgoing_incoming() {
let mut graph = CapGraph::new();
let cap1 = Cap {
urn: CapUrn::from_string(&format!(r#"cap:in="{}";op=extract_text;out="{}""#, MEDIA_BINARY, MEDIA_STRING)).unwrap(),
title: "Text Extractor".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "extract".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
let cap2 = Cap {
urn: CapUrn::from_string(&format!(r#"cap:in="{}";op=parse_json;out="{}""#, MEDIA_BINARY, MEDIA_OBJECT)).unwrap(),
title: "JSON Parser".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "parse".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
graph.add_cap(&cap1, "registry1");
graph.add_cap(&cap2, "registry2");
let outgoing = graph.get_outgoing(MEDIA_BINARY);
assert_eq!(outgoing.len(), 2);
let incoming_str = graph.get_incoming(MEDIA_STRING);
assert_eq!(incoming_str.len(), 1);
let incoming_obj = graph.get_incoming(MEDIA_OBJECT);
assert_eq!(incoming_obj.len(), 1);
}
#[test]
fn test_cap_graph_can_convert() {
let mut graph = CapGraph::new();
let cap1 = Cap {
urn: CapUrn::from_string(&format!(r#"cap:in="{}";op=extract;out="{}""#, MEDIA_BINARY, MEDIA_STRING)).unwrap(),
title: "Binary to Str".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "convert".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
let cap2 = Cap {
urn: CapUrn::from_string(&format!(r#"cap:in="{}";op=parse;out="{}""#, MEDIA_STRING, MEDIA_OBJECT)).unwrap(),
title: "Str to Obj".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "parse".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
graph.add_cap(&cap1, "registry");
graph.add_cap(&cap2, "registry");
assert!(graph.can_convert(MEDIA_BINARY, MEDIA_STRING));
assert!(graph.can_convert(MEDIA_STRING, MEDIA_OBJECT));
assert!(graph.can_convert(MEDIA_BINARY, MEDIA_OBJECT));
assert!(graph.can_convert(MEDIA_BINARY, MEDIA_BINARY));
assert!(!graph.can_convert(MEDIA_OBJECT, MEDIA_BINARY));
assert!(!graph.can_convert(MEDIA_BINARY, "unknown:spec.v1"));
}
#[test]
fn test_cap_graph_find_path() {
let mut graph = CapGraph::new();
let cap1 = Cap {
urn: CapUrn::from_string(r#"cap:in="media:binary";op=extract;out="media:string""#).unwrap(),
title: "Binary to Str".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "extract".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
let cap2 = Cap {
urn: CapUrn::from_string(r#"cap:in="media:string";op=parse;out="media:object""#).unwrap(),
title: "Str to Obj".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "parse".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
graph.add_cap(&cap1, "registry");
graph.add_cap(&cap2, "registry");
let path = graph.find_path("media:binary", "media:object").unwrap();
assert_eq!(path.len(), 2);
assert_eq!(path[0].from_spec, "media:binary");
assert_eq!(path[0].to_spec, "media:string");
assert_eq!(path[1].from_spec, "media:string");
assert_eq!(path[1].to_spec, "media:object");
let direct = graph.find_path("media:binary", "media:string").unwrap();
assert_eq!(direct.len(), 1);
let no_path = graph.find_path("media:object", "media:binary");
assert!(no_path.is_none());
let same = graph.find_path("media:binary", "media:binary").unwrap();
assert!(same.is_empty());
}
#[test]
fn test_cap_graph_find_all_paths() {
let mut graph = CapGraph::new();
let cap1 = Cap {
urn: CapUrn::from_string(r#"cap:in="media:binary";op=step1;out="media:string""#).unwrap(),
title: "A to B".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "step1".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
let cap2 = Cap {
urn: CapUrn::from_string(r#"cap:in="media:string";op=step2;out="media:object""#).unwrap(),
title: "B to C".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "step2".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
let cap3 = Cap {
urn: CapUrn::from_string(r#"cap:in="media:binary";op=direct;out="media:object""#).unwrap(),
title: "A to C Direct".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "direct".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
graph.add_cap(&cap1, "registry");
graph.add_cap(&cap2, "registry");
graph.add_cap(&cap3, "registry");
let all_paths = graph.find_all_paths("media:binary", "media:object", 5);
assert_eq!(all_paths.len(), 2);
assert_eq!(all_paths[0].len(), 1); assert_eq!(all_paths[1].len(), 2); }
#[test]
fn test_cap_graph_get_direct_edges_sorted() {
let mut graph = CapGraph::new();
let cap1 = Cap {
urn: CapUrn::from_string(r#"cap:in="media:binary";op=generic;out="media:string""#).unwrap(),
title: "Generic".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "generic".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
let cap2 = Cap {
urn: CapUrn::from_string(r#"cap:ext=pdf;in="media:binary";op=specific;out="media:string""#).unwrap(),
title: "Specific PDF".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "specific".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
graph.add_cap(&cap1, "registry");
graph.add_cap(&cap2, "registry");
let edges = graph.get_direct_edges("media:binary", "media:string");
assert_eq!(edges.len(), 2);
assert_eq!(edges[0].cap.title, "Specific PDF"); assert_eq!(edges[1].cap.title, "Generic"); }
#[tokio::test]
async fn test_cap_cube_graph_integration() {
let (media_registry, _temp_dir) = test_media_registry();
let mut provider_registry = CapMatrix::new(media_registry.clone());
let mut plugin_registry = CapMatrix::new(media_registry.clone());
let provider_host = Box::new(MockCapSet { name: "provider".to_string() });
let provider_cap = Cap {
urn: CapUrn::from_string(r#"cap:in="media:binary";op=extract;out="media:string""#).unwrap(),
title: "Provider Text Extractor".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "extract".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: Some(CapOutput::new(MEDIA_STRING, "output")),
metadata_json: None,
registered_by: None,
};
provider_registry.register_cap_set(
"provider".to_string(),
provider_host,
vec![provider_cap]
).unwrap();
let plugin_host = Box::new(MockCapSet { name: "plugin".to_string() });
let plugin_cap = Cap {
urn: CapUrn::from_string(r#"cap:in="media:string";op=parse;out="media:object""#).unwrap(),
title: "Plugin JSON Parser".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "parse".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
plugin_registry.register_cap_set(
"plugin".to_string(),
plugin_host,
vec![plugin_cap]
).unwrap();
let mut cube = CapCube::new(media_registry.clone());
cube.add_registry("providers".to_string(), Arc::new(RwLock::new(provider_registry)));
cube.add_registry("plugins".to_string(), Arc::new(RwLock::new(plugin_registry)));
let graph = cube.graph().unwrap();
assert!(graph.get_nodes().contains("media:binary"));
assert!(graph.get_nodes().contains("media:string"));
assert!(graph.get_nodes().contains("media:object"));
assert_eq!(graph.get_edges().len(), 2);
assert!(graph.can_convert("media:binary", "media:string"));
assert!(graph.can_convert("media:string", "media:object"));
assert!(graph.can_convert("media:binary", "media:object"));
let path = graph.find_path("media:binary", "media:object").unwrap();
assert_eq!(path.len(), 2);
let provider_edges: Vec<_> = graph.get_edges().iter()
.filter(|e| e.registry_name == "providers")
.collect();
assert_eq!(provider_edges.len(), 1);
let plugin_edges: Vec<_> = graph.get_edges().iter()
.filter(|e| e.registry_name == "plugins")
.collect();
assert_eq!(plugin_edges.len(), 1);
}
#[test]
fn test_cap_graph_stats() {
let mut graph = CapGraph::new();
let cap1 = Cap {
urn: CapUrn::from_string(r#"cap:in="media:binary";op=a;out="media:string""#).unwrap(),
title: "Cap 1".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "a".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
let cap2 = Cap {
urn: CapUrn::from_string(r#"cap:in="media:string";op=b;out="media:object""#).unwrap(),
title: "Cap 2".to_string(),
cap_description: None,
metadata: HashMap::new(),
command: "b".to_string(),
media_specs: HashMap::new(),
args: vec![],
output: None,
metadata_json: None,
registered_by: None,
};
graph.add_cap(&cap1, "registry");
graph.add_cap(&cap2, "registry");
let stats = graph.stats();
assert_eq!(stats.node_count, 3); assert_eq!(stats.edge_count, 2);
assert_eq!(stats.input_spec_count, 2); assert_eq!(stats.output_spec_count, 2); }
}