use super::{ExtensionRegistry, GrammarExtension, ParseError};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtensionMetadata {
pub name: String,
pub version: String,
pub description: String,
pub author: String,
pub dependencies: Vec<String>,
pub required_telltale_version: Option<String>,
pub priority: Option<u32>,
pub overview: Option<String>,
pub syntax_guide: Option<String>,
pub use_cases: Option<Vec<String>>,
pub keywords: Option<Vec<String>>,
}
#[derive(Debug)]
pub struct ExtensionPackage {
pub metadata: ExtensionMetadata,
pub extension: Box<dyn GrammarExtension>,
pub source_path: Option<PathBuf>,
}
#[derive(Debug, Default)]
pub struct ExtensionDiscovery {
discovered_extensions: BTreeMap<String, ExtensionPackage>,
search_paths: Vec<PathBuf>,
}
impl ExtensionDiscovery {
pub fn new() -> Self {
Self::default()
}
pub fn add_search_path<P: AsRef<Path>>(&mut self, path: P) {
self.search_paths.push(path.as_ref().to_path_buf());
}
pub fn register_extension(
&mut self,
metadata: ExtensionMetadata,
extension: Box<dyn GrammarExtension>,
) -> Result<(), ParseError> {
if metadata.name.is_empty() {
return Err(ParseError::InvalidSyntax {
details: "Extension name cannot be empty".to_string(),
});
}
if self.discovered_extensions.contains_key(&metadata.name) {
return Err(ParseError::RegistrationFailed {
extension: metadata.name.clone(),
rule: "discovery".to_string(),
details: format!(
"Extension '{}' is already registered in discovery system",
metadata.name
),
});
}
let package = ExtensionPackage {
metadata,
extension,
source_path: None,
};
self.discovered_extensions
.insert(package.metadata.name.clone(), package);
Ok(())
}
pub fn get_extensions(&self) -> &BTreeMap<String, ExtensionPackage> {
&self.discovered_extensions
}
pub fn has_extension(&self, name: &str) -> bool {
self.discovered_extensions.contains_key(name)
}
pub fn get_metadata(&self, name: &str) -> Option<&ExtensionMetadata> {
self.discovered_extensions
.get(name)
.map(|pkg| &pkg.metadata)
}
pub fn resolve_dependencies(
&self,
extension_names: &[String],
) -> Result<Vec<String>, ParseError> {
let mut resolved = Vec::new();
let mut visited = BTreeSet::new();
let mut visiting = BTreeSet::new();
fn visit(
name: &str,
extensions: &BTreeMap<String, ExtensionPackage>,
visited: &mut BTreeSet<String>,
visiting: &mut BTreeSet<String>,
resolved: &mut Vec<String>,
) -> Result<(), ParseError> {
if visited.contains(name) {
return Ok(());
}
if visiting.contains(name) {
return Err(ParseError::Conflict {
message: format!("Circular dependency detected involving '{}'", name),
});
}
visiting.insert(name.to_string());
if let Some(package) = extensions.get(name) {
for dep in &package.metadata.dependencies {
visit(dep, extensions, visited, visiting, resolved)?;
}
} else {
return Err(ParseError::MissingDependency {
extension: "dependency_resolution".to_string(),
dependency: name.to_string(),
});
}
visiting.remove(name);
visited.insert(name.to_string());
resolved.push(name.to_string());
Ok(())
}
for ext_name in extension_names {
visit(
ext_name,
&self.discovered_extensions,
&mut visited,
&mut visiting,
&mut resolved,
)?;
}
Ok(resolved)
}
pub fn create_registry(
&self,
extension_names: &[String],
) -> Result<ExtensionRegistry, ParseError> {
let resolved = self.resolve_dependencies(extension_names)?;
let mut registry = ExtensionRegistry::new();
for ext_name in resolved {
if let Some(package) = self.discovered_extensions.get(&ext_name) {
registry.register_grammar(ClonableExtensionWrapper::new(
&*package.extension,
&package.metadata,
))?;
for dep in &package.metadata.dependencies {
registry.add_dependency(&ext_name, dep);
}
}
}
Ok(registry)
}
pub fn check_compatibility(&self, extension_names: &[String]) -> Result<(), ParseError> {
let resolved = self.resolve_dependencies(extension_names)?;
for ext_name in &resolved {
if let Some(package) = self.discovered_extensions.get(ext_name) {
if let Some(required_version) = &package.metadata.required_telltale_version {
if required_version != "0.5.0" {
return Err(ParseError::IncompatibleExtensions {
details: format!(
"Extension '{}' requires telltale version '{}', but current version is '0.5.0'. Please update the extension or telltale to compatible versions.",
ext_name, required_version
),
});
}
}
}
}
Ok(())
}
pub fn load_from_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), ParseError> {
let path = path.as_ref();
let metadata_path = path.join("extension.toml");
if metadata_path.exists() {
let metadata_str =
std::fs::read_to_string(&metadata_path).map_err(|e| ParseError::InvalidSyntax {
details: format!("Failed to read extension metadata: {}", e),
})?;
let metadata: ExtensionMetadata =
toml::from_str(&metadata_str).map_err(|e| ParseError::InvalidSyntax {
details: format!("Invalid extension metadata: {}", e),
})?;
let extension = Box::new(MetadataOnlyExtension::new(&metadata));
let package = ExtensionPackage {
metadata: metadata.clone(),
extension,
source_path: Some(path.to_path_buf()),
};
self.discovered_extensions.insert(metadata.name, package);
}
Ok(())
}
pub fn with_common_extensions() -> Result<ExtensionRegistry, ParseError> {
let mut discovery = Self::new();
discovery.register_extension(
ExtensionMetadata {
name: "timeout".to_string(),
version: "0.5.0".to_string(),
description: "Timeout support for choreographic protocols".to_string(),
author: "Telltale Team".to_string(),
dependencies: vec![],
required_telltale_version: Some("0.5.0".to_string()),
priority: Some(100),
overview: Some("Adds timeout semantics to choreographic protocols".to_string()),
syntax_guide: Some("Use `timeout(duration) { ... }` syntax".to_string()),
use_cases: Some(vec![
"Network protocols".to_string(),
"Real-time systems".to_string(),
]),
keywords: Some(vec!["timeout".to_string(), "timing".to_string()]),
},
Box::new(super::timeout::TimeoutGrammarExtension),
)?;
discovery.register_extension(
ExtensionMetadata {
name: "aura_annotations".to_string(),
version: "0.1.0".to_string(),
description: "Aura-style annotations for capability tracking".to_string(),
author: "Aura Project".to_string(),
dependencies: vec![],
required_telltale_version: Some("0.5.0".to_string()),
priority: Some(110),
overview: Some(
"Adds Aura-specific annotations for capabilities and flow control".to_string(),
),
syntax_guide: Some(
"Use Role[annotation=value] syntax in communications".to_string(),
),
use_cases: Some(vec![
"Capability verification".to_string(),
"Flow control".to_string(),
]),
keywords: Some(vec![
"aura".to_string(),
"capabilities".to_string(),
"annotations".to_string(),
]),
},
Box::new(AuraAnnotationExtension),
)?;
discovery.create_registry(&["timeout".to_string(), "aura_annotations".to_string()])
}
pub fn for_third_party() -> Self {
Self::new()
}
pub fn with_builtin_only() -> Result<ExtensionRegistry, ParseError> {
let mut discovery = Self::new();
discovery.register_extension(
ExtensionMetadata {
name: "timeout".to_string(),
version: "0.5.0".to_string(),
description: "Timeout support for choreographic protocols".to_string(),
author: "Telltale Team".to_string(),
dependencies: vec![],
required_telltale_version: Some("0.5.0".to_string()),
priority: Some(100),
overview: Some("Adds timeout semantics to choreographic protocols".to_string()),
syntax_guide: Some("Use `timeout(duration) { ... }` syntax".to_string()),
use_cases: Some(vec![
"Network protocols".to_string(),
"Real-time systems".to_string(),
]),
keywords: Some(vec!["timeout".to_string(), "timing".to_string()]),
},
Box::new(super::timeout::TimeoutGrammarExtension),
)?;
discovery.create_registry(&["timeout".to_string()])
}
pub fn validate_metadata(metadata: &ExtensionMetadata) -> Result<(), ParseError> {
if metadata.name.is_empty() {
return Err(ParseError::InvalidSyntax {
details: "Extension name cannot be empty".to_string(),
});
}
if metadata.version.is_empty() {
return Err(ParseError::InvalidSyntax {
details: "Extension version cannot be empty".to_string(),
});
}
if metadata.name.contains(' ') {
return Err(ParseError::InvalidSyntax {
details: "Extension name cannot contain spaces".to_string(),
});
}
Ok(())
}
pub fn list_extensions(&self) -> Vec<&ExtensionMetadata> {
self.discovered_extensions
.values()
.map(|pkg| &pkg.metadata)
.collect()
}
pub fn find_by_author(&self, author: &str) -> Vec<&ExtensionMetadata> {
self.discovered_extensions
.values()
.filter_map(|pkg| {
if pkg.metadata.author == author {
Some(&pkg.metadata)
} else {
None
}
})
.collect()
}
}
#[derive(Debug, Clone)]
struct ClonableExtensionWrapper {
#[allow(dead_code)] id: String,
#[allow(dead_code)] rules: Vec<String>,
#[allow(dead_code)] grammar: String,
priority: u32,
}
impl ClonableExtensionWrapper {
fn new(extension: &dyn GrammarExtension, metadata: &ExtensionMetadata) -> Self {
Self {
id: metadata.name.clone(),
rules: extension
.statement_rules()
.iter()
.map(|s| (*s).to_string())
.collect(),
grammar: extension.grammar_rules().to_string(),
priority: metadata.priority.unwrap_or(extension.priority()),
}
}
}
impl GrammarExtension for ClonableExtensionWrapper {
fn grammar_rules(&self) -> &'static str {
""
}
fn statement_rules(&self) -> Vec<&'static str> {
vec![]
}
fn priority(&self) -> u32 {
self.priority
}
fn extension_id(&self) -> &'static str {
"cloneable_wrapper"
}
}
#[derive(Debug, Clone)]
struct MetadataOnlyExtension {
#[allow(dead_code)] name: String,
priority: u32,
}
impl MetadataOnlyExtension {
fn new(metadata: &ExtensionMetadata) -> Self {
Self {
name: metadata.name.clone(),
priority: metadata.priority.unwrap_or(100),
}
}
}
impl GrammarExtension for MetadataOnlyExtension {
fn grammar_rules(&self) -> &'static str {
""
}
fn statement_rules(&self) -> Vec<&'static str> {
vec![]
}
fn priority(&self) -> u32 {
self.priority
}
fn extension_id(&self) -> &'static str {
"placeholder"
}
}
#[derive(Debug, Clone)]
struct AuraAnnotationExtension;
impl GrammarExtension for AuraAnnotationExtension {
fn grammar_rules(&self) -> &'static str {
r#"
aura_annotations_stmt = { role_ref ~ "[" ~ aura_annotations_list ~ "]" ~ "->" ~ role_ref ~ ":" ~ message ~ ";" }
aura_annotations_list = { aura_annotations_item ~ ("," ~ aura_annotations_item)* }
aura_annotations_item = { ident ~ "=" ~ annotation_value }
"#
}
fn statement_rules(&self) -> Vec<&'static str> {
vec!["aura_annotations_stmt"]
}
fn priority(&self) -> u32 {
110
}
fn extension_id(&self) -> &'static str {
"aura_annotations"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extension_discovery() {
let mut discovery = ExtensionDiscovery::new();
let metadata = ExtensionMetadata {
name: "test_ext".to_string(),
version: "1.0.0".to_string(),
description: "Test extension".to_string(),
author: "Test Author".to_string(),
dependencies: vec![],
required_telltale_version: Some("0.5.0".to_string()),
priority: Some(100),
overview: None,
syntax_guide: None,
use_cases: None,
keywords: None,
};
let extension = Box::new(MetadataOnlyExtension::new(&metadata));
assert!(discovery.register_extension(metadata, extension).is_ok());
assert!(discovery.has_extension("test_ext"));
}
#[test]
fn test_dependency_resolution() {
let mut discovery = ExtensionDiscovery::new();
let base_metadata = ExtensionMetadata {
name: "base".to_string(),
version: "1.0.0".to_string(),
description: "Base extension".to_string(),
author: "Test".to_string(),
dependencies: vec![],
required_telltale_version: Some("0.5.0".to_string()),
priority: Some(100),
overview: None,
syntax_guide: None,
use_cases: None,
keywords: None,
};
discovery
.register_extension(
base_metadata.clone(),
Box::new(MetadataOnlyExtension::new(&base_metadata)),
)
.unwrap();
let dep_metadata = ExtensionMetadata {
name: "dependent".to_string(),
version: "1.0.0".to_string(),
description: "Dependent extension".to_string(),
author: "Test".to_string(),
dependencies: vec!["base".to_string()],
required_telltale_version: Some("0.5.0".to_string()),
priority: Some(100),
overview: None,
syntax_guide: None,
use_cases: None,
keywords: None,
};
discovery
.register_extension(
dep_metadata.clone(),
Box::new(MetadataOnlyExtension::new(&dep_metadata)),
)
.unwrap();
let resolved = discovery
.resolve_dependencies(&["dependent".to_string()])
.unwrap();
assert!(resolved.contains(&"base".to_string()));
assert!(resolved.contains(&"dependent".to_string()));
}
}