Skip to main content

telltale_language/extensions/
discovery.rs

1//! Extension Discovery and Registration System
2//!
3//! This module provides utilities for discovering and registering extensions
4//! in a clean, composable way. It supports extension versioning, dependency
5//! management, and compatibility checking.
6
7use super::{ExtensionRegistry, GrammarExtension, ParseError};
8use serde::{Deserialize, Serialize};
9use std::collections::{BTreeMap, BTreeSet};
10use std::path::{Path, PathBuf};
11
12/// Extension metadata for discovery and versioning
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ExtensionMetadata {
15    pub name: String,
16    pub version: String,
17    pub description: String,
18    pub author: String,
19    pub dependencies: Vec<String>,
20    pub required_telltale_version: Option<String>,
21    pub priority: Option<u32>,
22    /// Documentation fields
23    pub overview: Option<String>,
24    pub syntax_guide: Option<String>,
25    pub use_cases: Option<Vec<String>>,
26    pub keywords: Option<Vec<String>>,
27}
28
29/// Extension package containing metadata and implementation
30#[derive(Debug)]
31pub struct ExtensionPackage {
32    pub metadata: ExtensionMetadata,
33    pub extension: Box<dyn GrammarExtension>,
34    pub source_path: Option<PathBuf>,
35}
36
37/// Registry for extension discovery and management
38#[derive(Debug, Default)]
39pub struct ExtensionDiscovery {
40    discovered_extensions: BTreeMap<String, ExtensionPackage>,
41    search_paths: Vec<PathBuf>,
42}
43
44impl ExtensionDiscovery {
45    /// Create a new extension discovery manager
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Add a search path for extensions
51    pub fn add_search_path<P: AsRef<Path>>(&mut self, path: P) {
52        self.search_paths.push(path.as_ref().to_path_buf());
53    }
54
55    /// Manually register an extension with metadata
56    pub fn register_extension(
57        &mut self,
58        metadata: ExtensionMetadata,
59        extension: Box<dyn GrammarExtension>,
60    ) -> Result<(), ParseError> {
61        // Validate metadata
62        if metadata.name.is_empty() {
63            return Err(ParseError::InvalidSyntax {
64                details: "Extension name cannot be empty".to_string(),
65            });
66        }
67
68        // Check for conflicts
69        if self.discovered_extensions.contains_key(&metadata.name) {
70            return Err(ParseError::RegistrationFailed {
71                extension: metadata.name.clone(),
72                rule: "discovery".to_string(),
73                details: format!(
74                    "Extension '{}' is already registered in discovery system",
75                    metadata.name
76                ),
77            });
78        }
79
80        let package = ExtensionPackage {
81            metadata,
82            extension,
83            source_path: None,
84        };
85
86        self.discovered_extensions
87            .insert(package.metadata.name.clone(), package);
88        Ok(())
89    }
90
91    /// Get all discovered extensions
92    pub fn get_extensions(&self) -> &BTreeMap<String, ExtensionPackage> {
93        &self.discovered_extensions
94    }
95
96    /// Check if an extension is available
97    pub fn has_extension(&self, name: &str) -> bool {
98        self.discovered_extensions.contains_key(name)
99    }
100
101    /// Get extension metadata by name
102    pub fn get_metadata(&self, name: &str) -> Option<&ExtensionMetadata> {
103        self.discovered_extensions
104            .get(name)
105            .map(|pkg| &pkg.metadata)
106    }
107
108    /// Resolve extension dependencies using topological sort
109    ///
110    /// Returns extensions in dependency order (dependencies before dependents)
111    pub fn resolve_dependencies(
112        &self,
113        extension_names: &[String],
114    ) -> Result<Vec<String>, ParseError> {
115        let mut resolved = Vec::new();
116        let mut visited = BTreeSet::new();
117        let mut visiting = BTreeSet::new(); // For cycle detection
118
119        // Helper function for DFS topological sort
120        fn visit(
121            name: &str,
122            extensions: &BTreeMap<String, ExtensionPackage>,
123            visited: &mut BTreeSet<String>,
124            visiting: &mut BTreeSet<String>,
125            resolved: &mut Vec<String>,
126        ) -> Result<(), ParseError> {
127            if visited.contains(name) {
128                return Ok(());
129            }
130
131            if visiting.contains(name) {
132                return Err(ParseError::Conflict {
133                    message: format!("Circular dependency detected involving '{}'", name),
134                });
135            }
136
137            visiting.insert(name.to_string());
138
139            if let Some(package) = extensions.get(name) {
140                // Process dependencies first
141                for dep in &package.metadata.dependencies {
142                    visit(dep, extensions, visited, visiting, resolved)?;
143                }
144            } else {
145                return Err(ParseError::MissingDependency {
146                    extension: "dependency_resolution".to_string(),
147                    dependency: name.to_string(),
148                });
149            }
150
151            visiting.remove(name);
152            visited.insert(name.to_string());
153            resolved.push(name.to_string());
154
155            Ok(())
156        }
157
158        // Visit all requested extensions
159        for ext_name in extension_names {
160            visit(
161                ext_name,
162                &self.discovered_extensions,
163                &mut visited,
164                &mut visiting,
165                &mut resolved,
166            )?;
167        }
168
169        Ok(resolved)
170    }
171
172    /// Create a configured extension registry
173    pub fn create_registry(
174        &self,
175        extension_names: &[String],
176    ) -> Result<ExtensionRegistry, ParseError> {
177        let resolved = self.resolve_dependencies(extension_names)?;
178        let mut registry = ExtensionRegistry::new();
179
180        // Register extensions in dependency order
181        for ext_name in resolved {
182            if let Some(package) = self.discovered_extensions.get(&ext_name) {
183                // Clone the extension (this requires extensions to be cloneable or
184                // we need a different approach for ownership)
185                registry.register_grammar(ClonableExtensionWrapper::new(
186                    &*package.extension,
187                    &package.metadata,
188                ))?;
189
190                // Add dependencies to registry
191                for dep in &package.metadata.dependencies {
192                    registry.add_dependency(&ext_name, dep);
193                }
194            }
195        }
196
197        // Note: We skip validate_dependencies() here because:
198        // 1. Dependencies were already resolved and validated by resolve_dependencies()
199        // 2. ClonableExtensionWrapper has a limitation where extension_id() returns
200        //    a static string, not the dynamic name, so registry validation would fail
201        //
202        // The dependency ordering is guaranteed by the topological sort above.
203
204        Ok(registry)
205    }
206
207    /// Validate extension compatibility
208    pub fn check_compatibility(&self, extension_names: &[String]) -> Result<(), ParseError> {
209        let resolved = self.resolve_dependencies(extension_names)?;
210
211        // Check version compatibility
212        for ext_name in &resolved {
213            if let Some(package) = self.discovered_extensions.get(ext_name) {
214                if let Some(required_version) = &package.metadata.required_telltale_version {
215                    // Compare against baseline 0.5.0 (could use env!("CARGO_PKG_VERSION"))
216                    if required_version != "0.5.0" {
217                        return Err(ParseError::IncompatibleExtensions {
218                            details: format!(
219                                "Extension '{}' requires telltale version '{}', but current version is '0.5.0'. Please update the extension or telltale to compatible versions.",
220                                ext_name, required_version
221                            ),
222                        });
223                    }
224                }
225            }
226        }
227
228        Ok(())
229    }
230
231    /// Load extension from a directory or file.
232    ///
233    /// **Note**: This method loads extension metadata from TOML files but creates
234    /// a `MetadataOnlyExtension` that does not provide actual grammar rules.
235    /// For production use, prefer static registration via `register_extension()`
236    /// with a concrete `GrammarExtension` implementation.
237    ///
238    /// This is primarily useful for:
239    /// - Testing extension discovery and dependency resolution
240    /// - Development workflows where grammar rules aren't needed
241    /// - Future integration with dynamic loading (not currently implemented)
242    pub fn load_from_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), ParseError> {
243        let path = path.as_ref();
244
245        // Look for extension metadata file
246        let metadata_path = path.join("extension.toml");
247        if metadata_path.exists() {
248            let metadata_str =
249                std::fs::read_to_string(&metadata_path).map_err(|e| ParseError::InvalidSyntax {
250                    details: format!("Failed to read extension metadata: {}", e),
251                })?;
252
253            let metadata: ExtensionMetadata =
254                toml::from_str(&metadata_str).map_err(|e| ParseError::InvalidSyntax {
255                    details: format!("Invalid extension metadata: {}", e),
256                })?;
257
258            // Dynamic library loading not supported; create metadata-only extension
259            let extension = Box::new(MetadataOnlyExtension::new(&metadata));
260
261            let package = ExtensionPackage {
262                metadata: metadata.clone(),
263                extension,
264                source_path: Some(path.to_path_buf()),
265            };
266
267            self.discovered_extensions.insert(metadata.name, package);
268        }
269
270        Ok(())
271    }
272
273    /// Create a registry with commonly used extensions
274    pub fn with_common_extensions() -> Result<ExtensionRegistry, ParseError> {
275        let mut discovery = Self::new();
276
277        // Register built-in extensions
278        discovery.register_extension(
279            ExtensionMetadata {
280                name: "timeout".to_string(),
281                version: "0.5.0".to_string(),
282                description: "Timeout support for choreographic protocols".to_string(),
283                author: "Telltale Team".to_string(),
284                dependencies: vec![],
285                required_telltale_version: Some("0.5.0".to_string()),
286                priority: Some(100),
287                overview: Some("Adds timeout semantics to choreographic protocols".to_string()),
288                syntax_guide: Some("Use `timeout(duration) { ... }` syntax".to_string()),
289                use_cases: Some(vec![
290                    "Network protocols".to_string(),
291                    "Real-time systems".to_string(),
292                ]),
293                keywords: Some(vec!["timeout".to_string(), "timing".to_string()]),
294            },
295            Box::new(super::timeout::TimeoutGrammarExtension),
296        )?;
297
298        discovery.register_extension(
299            ExtensionMetadata {
300                name: "aura_annotations".to_string(),
301                version: "0.1.0".to_string(),
302                description: "Aura-style annotations for capability tracking".to_string(),
303                author: "Aura Project".to_string(),
304                dependencies: vec![],
305                required_telltale_version: Some("0.5.0".to_string()),
306                priority: Some(110),
307                overview: Some(
308                    "Adds Aura-specific annotations for capabilities and flow control".to_string(),
309                ),
310                syntax_guide: Some(
311                    "Use Role[annotation=value] syntax in communications".to_string(),
312                ),
313                use_cases: Some(vec![
314                    "Capability verification".to_string(),
315                    "Flow control".to_string(),
316                ]),
317                keywords: Some(vec![
318                    "aura".to_string(),
319                    "capabilities".to_string(),
320                    "annotations".to_string(),
321                ]),
322            },
323            Box::new(AuraAnnotationExtension),
324        )?;
325
326        discovery.create_registry(&["timeout".to_string(), "aura_annotations".to_string()])
327    }
328
329    /// Helper to create a minimal registry for 3rd party integration
330    pub fn for_third_party() -> Self {
331        Self::new()
332    }
333
334    /// Helper to register built-in telltale extensions
335    pub fn with_builtin_only() -> Result<ExtensionRegistry, ParseError> {
336        let mut discovery = Self::new();
337
338        discovery.register_extension(
339            ExtensionMetadata {
340                name: "timeout".to_string(),
341                version: "0.5.0".to_string(),
342                description: "Timeout support for choreographic protocols".to_string(),
343                author: "Telltale Team".to_string(),
344                dependencies: vec![],
345                required_telltale_version: Some("0.5.0".to_string()),
346                priority: Some(100),
347                overview: Some("Adds timeout semantics to choreographic protocols".to_string()),
348                syntax_guide: Some("Use `timeout(duration) { ... }` syntax".to_string()),
349                use_cases: Some(vec![
350                    "Network protocols".to_string(),
351                    "Real-time systems".to_string(),
352                ]),
353                keywords: Some(vec!["timeout".to_string(), "timing".to_string()]),
354            },
355            Box::new(super::timeout::TimeoutGrammarExtension),
356        )?;
357
358        discovery.create_registry(&["timeout".to_string()])
359    }
360
361    /// Helper to validate extension metadata before registration
362    pub fn validate_metadata(metadata: &ExtensionMetadata) -> Result<(), ParseError> {
363        if metadata.name.is_empty() {
364            return Err(ParseError::InvalidSyntax {
365                details: "Extension name cannot be empty".to_string(),
366            });
367        }
368
369        if metadata.version.is_empty() {
370            return Err(ParseError::InvalidSyntax {
371                details: "Extension version cannot be empty".to_string(),
372            });
373        }
374
375        if metadata.name.contains(' ') {
376            return Err(ParseError::InvalidSyntax {
377                details: "Extension name cannot contain spaces".to_string(),
378            });
379        }
380
381        Ok(())
382    }
383
384    /// List all available extensions with their metadata
385    pub fn list_extensions(&self) -> Vec<&ExtensionMetadata> {
386        self.discovered_extensions
387            .values()
388            .map(|pkg| &pkg.metadata)
389            .collect()
390    }
391
392    /// Find extensions by author
393    pub fn find_by_author(&self, author: &str) -> Vec<&ExtensionMetadata> {
394        self.discovered_extensions
395            .values()
396            .filter_map(|pkg| {
397                if pkg.metadata.author == author {
398                    Some(&pkg.metadata)
399                } else {
400                    None
401                }
402            })
403            .collect()
404    }
405}
406
407/// Wrapper to make extensions cloneable for registry management
408///
409/// Note: Some fields are stored for future use when dynamic grammar injection
410/// is fully implemented. Currently only `priority` is used.
411#[derive(Debug, Clone)]
412struct ClonableExtensionWrapper {
413    #[allow(dead_code)] // Stored for future dynamic grammar support
414    id: String,
415    #[allow(dead_code)] // Stored for future dynamic grammar support
416    rules: Vec<String>,
417    #[allow(dead_code)] // Stored for future dynamic grammar support
418    grammar: String,
419    priority: u32,
420}
421
422impl ClonableExtensionWrapper {
423    fn new(extension: &dyn GrammarExtension, metadata: &ExtensionMetadata) -> Self {
424        Self {
425            id: metadata.name.clone(),
426            rules: extension
427                .statement_rules()
428                .iter()
429                .map(|s| (*s).to_string())
430                .collect(),
431            grammar: extension.grammar_rules().to_string(),
432            priority: metadata.priority.unwrap_or(extension.priority()),
433        }
434    }
435}
436
437impl GrammarExtension for ClonableExtensionWrapper {
438    fn grammar_rules(&self) -> &'static str {
439        // Wrapper cannot provide dynamic rules; returns empty (use inner extension directly)
440        ""
441    }
442
443    fn statement_rules(&self) -> Vec<&'static str> {
444        // Wrapper cannot provide dynamic rules; returns empty
445        vec![]
446    }
447
448    fn priority(&self) -> u32 {
449        self.priority
450    }
451
452    fn extension_id(&self) -> &'static str {
453        // This needs a different approach for dynamic extensions
454        "cloneable_wrapper"
455    }
456}
457
458/// Metadata-only extension loaded from configuration files.
459///
460/// Does not provide grammar rules - exists for testing extension discovery
461/// and dependency resolution. Dynamic library loading is not supported.
462///
463/// **For production use**, implement `GrammarExtension` directly and
464/// register via `ExtensionDiscovery::register_extension()`.
465#[derive(Debug, Clone)]
466struct MetadataOnlyExtension {
467    #[allow(dead_code)] // Stored for error messages and debugging
468    name: String,
469    priority: u32,
470}
471
472impl MetadataOnlyExtension {
473    fn new(metadata: &ExtensionMetadata) -> Self {
474        Self {
475            name: metadata.name.clone(),
476            priority: metadata.priority.unwrap_or(100),
477        }
478    }
479}
480
481impl GrammarExtension for MetadataOnlyExtension {
482    fn grammar_rules(&self) -> &'static str {
483        ""
484    }
485
486    fn statement_rules(&self) -> Vec<&'static str> {
487        vec![]
488    }
489
490    fn priority(&self) -> u32 {
491        self.priority
492    }
493
494    fn extension_id(&self) -> &'static str {
495        "placeholder"
496    }
497}
498
499/// Aura annotation extension implementation
500#[derive(Debug, Clone)]
501struct AuraAnnotationExtension;
502
503impl GrammarExtension for AuraAnnotationExtension {
504    fn grammar_rules(&self) -> &'static str {
505        r#"
506aura_annotations_stmt = { role_ref ~ "[" ~ aura_annotations_list ~ "]" ~ "->" ~ role_ref ~ ":" ~ message ~ ";" }
507aura_annotations_list = { aura_annotations_item ~ ("," ~ aura_annotations_item)* }
508aura_annotations_item = { ident ~ "=" ~ annotation_value }
509"#
510    }
511
512    fn statement_rules(&self) -> Vec<&'static str> {
513        vec!["aura_annotations_stmt"]
514    }
515
516    fn priority(&self) -> u32 {
517        110
518    }
519
520    fn extension_id(&self) -> &'static str {
521        "aura_annotations"
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    #[test]
530    fn test_extension_discovery() {
531        let mut discovery = ExtensionDiscovery::new();
532
533        let metadata = ExtensionMetadata {
534            name: "test_ext".to_string(),
535            version: "1.0.0".to_string(),
536            description: "Test extension".to_string(),
537            author: "Test Author".to_string(),
538            dependencies: vec![],
539            required_telltale_version: Some("0.5.0".to_string()),
540            priority: Some(100),
541            overview: None,
542            syntax_guide: None,
543            use_cases: None,
544            keywords: None,
545        };
546
547        let extension = Box::new(MetadataOnlyExtension::new(&metadata));
548        assert!(discovery.register_extension(metadata, extension).is_ok());
549        assert!(discovery.has_extension("test_ext"));
550    }
551
552    #[test]
553    fn test_dependency_resolution() {
554        let mut discovery = ExtensionDiscovery::new();
555
556        // Add base extension
557        let base_metadata = ExtensionMetadata {
558            name: "base".to_string(),
559            version: "1.0.0".to_string(),
560            description: "Base extension".to_string(),
561            author: "Test".to_string(),
562            dependencies: vec![],
563            required_telltale_version: Some("0.5.0".to_string()),
564            priority: Some(100),
565            overview: None,
566            syntax_guide: None,
567            use_cases: None,
568            keywords: None,
569        };
570        discovery
571            .register_extension(
572                base_metadata.clone(),
573                Box::new(MetadataOnlyExtension::new(&base_metadata)),
574            )
575            .unwrap();
576
577        // Add dependent extension
578        let dep_metadata = ExtensionMetadata {
579            name: "dependent".to_string(),
580            version: "1.0.0".to_string(),
581            description: "Dependent extension".to_string(),
582            author: "Test".to_string(),
583            dependencies: vec!["base".to_string()],
584            required_telltale_version: Some("0.5.0".to_string()),
585            priority: Some(100),
586            overview: None,
587            syntax_guide: None,
588            use_cases: None,
589            keywords: None,
590        };
591        discovery
592            .register_extension(
593                dep_metadata.clone(),
594                Box::new(MetadataOnlyExtension::new(&dep_metadata)),
595            )
596            .unwrap();
597
598        let resolved = discovery
599            .resolve_dependencies(&["dependent".to_string()])
600            .unwrap();
601        assert!(resolved.contains(&"base".to_string()));
602        assert!(resolved.contains(&"dependent".to_string()));
603    }
604}