Skip to main content

plugin_packager/
composition.rs

1// Copyright 2024 Vincents AI
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4/// Plugin composition and meta-package support
5///
6/// This module provides support for composite plugins and plugin bundling:
7/// - Composite plugin definitions
8/// - Plugin bundling and aggregation
9/// - Transitive dependency resolution
10/// - Version conflict resolution
11use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13
14/// A plugin component reference
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct PluginComponent {
17    pub name: String,
18    pub version: String,
19    pub required: bool,
20    pub description: Option<String>,
21}
22
23impl PluginComponent {
24    pub fn new(name: &str, version: &str) -> Self {
25        Self {
26            name: name.to_string(),
27            version: version.to_string(),
28            required: true,
29            description: None,
30        }
31    }
32
33    pub fn optional(mut self) -> Self {
34        self.required = false;
35        self
36    }
37
38    pub fn with_description(mut self, description: &str) -> Self {
39        self.description = Some(description.to_string());
40        self
41    }
42}
43
44/// Version conflict resolution strategy
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "lowercase")]
47pub enum ConflictResolution {
48    Newest,     // Use newest version
49    Oldest,     // Use oldest version
50    Exact,      // Require exact match (fail on conflict)
51    Compatible, // Use compatible version range
52}
53
54impl ConflictResolution {
55    pub fn as_str(&self) -> &'static str {
56        match self {
57            ConflictResolution::Newest => "newest",
58            ConflictResolution::Oldest => "oldest",
59            ConflictResolution::Exact => "exact",
60            ConflictResolution::Compatible => "compatible",
61        }
62    }
63}
64
65/// Version conflict information
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct VersionConflict {
68    pub plugin_name: String,
69    pub versions: Vec<String>,
70    pub requested_by: Vec<String>,
71    pub resolved_version: String,
72    pub resolution_strategy: ConflictResolution,
73}
74
75impl VersionConflict {
76    pub fn new(
77        plugin_name: &str,
78        versions: Vec<String>,
79        resolved_version: &str,
80        strategy: ConflictResolution,
81    ) -> Self {
82        Self {
83            plugin_name: plugin_name.to_string(),
84            versions,
85            requested_by: Vec::new(),
86            resolved_version: resolved_version.to_string(),
87            resolution_strategy: strategy,
88        }
89    }
90}
91
92/// Composite plugin definition
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct CompositePlugin {
95    pub name: String,
96    pub version: String,
97    pub description: String,
98    pub components: Vec<PluginComponent>,
99    pub transitive_dependencies: HashMap<String, String>,
100    pub conflict_resolution: ConflictResolution,
101}
102
103impl CompositePlugin {
104    pub fn new(name: &str, version: &str, description: &str) -> Self {
105        Self {
106            name: name.to_string(),
107            version: version.to_string(),
108            description: description.to_string(),
109            components: Vec::new(),
110            transitive_dependencies: HashMap::new(),
111            conflict_resolution: ConflictResolution::Newest,
112        }
113    }
114
115    pub fn add_component(&mut self, component: PluginComponent) {
116        self.components.push(component);
117    }
118
119    pub fn add_transitive_dependency(&mut self, name: &str, version: &str) {
120        self.transitive_dependencies
121            .insert(name.to_string(), version.to_string());
122    }
123
124    pub fn required_components(&self) -> Vec<&PluginComponent> {
125        self.components.iter().filter(|c| c.required).collect()
126    }
127
128    pub fn optional_components(&self) -> Vec<&PluginComponent> {
129        self.components.iter().filter(|c| !c.required).collect()
130    }
131
132    pub fn component_count(&self) -> usize {
133        self.components.len()
134    }
135
136    pub fn total_dependencies(&self) -> usize {
137        self.component_count() + self.transitive_dependencies.len()
138    }
139}
140
141/// Bundle specification for packaging
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct PluginBundle {
144    pub name: String,
145    pub version: String,
146    pub bundle_type: BundleType,
147    pub plugins: Vec<String>,
148    pub metadata: BundleMetadata,
149}
150
151/// Type of plugin bundle
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153#[serde(rename_all = "lowercase")]
154pub enum BundleType {
155    Standalone, // Single plugin
156    Composite,  // Multiple plugins as one unit
157    Collection, // Related plugins without strict deps
158}
159
160impl BundleType {
161    pub fn as_str(&self) -> &'static str {
162        match self {
163            BundleType::Standalone => "standalone",
164            BundleType::Composite => "composite",
165            BundleType::Collection => "collection",
166        }
167    }
168}
169
170/// Bundle metadata
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct BundleMetadata {
173    pub author: String,
174    pub license: String,
175    pub created_at: String,
176    pub compatible_versions: Vec<String>,
177}
178
179impl PluginBundle {
180    pub fn new(name: &str, version: &str, bundle_type: BundleType) -> Self {
181        Self {
182            name: name.to_string(),
183            version: version.to_string(),
184            bundle_type,
185            plugins: Vec::new(),
186            metadata: BundleMetadata {
187                author: "unknown".to_string(),
188                license: "unknown".to_string(),
189                created_at: chrono::Local::now()
190                    .format("%Y-%m-%dT%H:%M:%SZ")
191                    .to_string(),
192                compatible_versions: Vec::new(),
193            },
194        }
195    }
196
197    pub fn add_plugin(&mut self, name: String) {
198        self.plugins.push(name);
199    }
200
201    pub fn plugin_count(&self) -> usize {
202        self.plugins.len()
203    }
204}
205
206/// Dependency resolution result
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct DependencyResolutionResult {
209    pub requested: Vec<String>,
210    pub resolved: HashMap<String, String>,
211    pub conflicts: Vec<VersionConflict>,
212    pub unresolvable: Vec<String>,
213    pub success: bool,
214}
215
216impl DependencyResolutionResult {
217    pub fn new() -> Self {
218        Self {
219            requested: Vec::new(),
220            resolved: HashMap::new(),
221            conflicts: Vec::new(),
222            unresolvable: Vec::new(),
223            success: true,
224        }
225    }
226
227    pub fn add_conflict(&mut self, conflict: VersionConflict) {
228        self.conflicts.push(conflict);
229        self.success = false;
230    }
231
232    pub fn add_unresolvable(&mut self, name: String) {
233        self.unresolvable.push(name);
234        self.success = false;
235    }
236
237    pub fn resolved_count(&self) -> usize {
238        self.resolved.len()
239    }
240}
241
242impl Default for DependencyResolutionResult {
243    fn default() -> Self {
244        Self::new()
245    }
246}
247
248/// Plugin composition manager
249pub struct CompositionManager;
250
251impl CompositionManager {
252    /// Resolve transitive dependencies for a composite plugin
253    pub fn resolve_transitive_dependencies(
254        composite: &CompositePlugin,
255        dependency_graph: &HashMap<String, Vec<String>>,
256    ) -> DependencyResolutionResult {
257        let mut result = DependencyResolutionResult::new();
258        let mut visited = HashSet::new();
259        let mut to_process = vec![composite.name.clone()];
260
261        while let Some(current) = to_process.pop() {
262            if visited.contains(&current) {
263                continue;
264            }
265            visited.insert(current.clone());
266
267            // Get components that match current plugin
268            let matching_components: Vec<_> = composite
269                .components
270                .iter()
271                .filter(|c| c.name == current)
272                .collect();
273
274            for component in matching_components {
275                result
276                    .resolved
277                    .insert(component.name.clone(), component.version.clone());
278                result.requested.push(component.name.clone());
279
280                // Add to processing queue
281                if let Some(deps) = dependency_graph.get(&component.name) {
282                    for dep in deps {
283                        if !visited.contains(dep) {
284                            to_process.push(dep.clone());
285                        }
286                    }
287                }
288            }
289        }
290
291        result
292    }
293
294    /// Detect version conflicts in composite plugin
295    pub fn detect_version_conflicts(composite: &CompositePlugin) -> Vec<VersionConflict> {
296        let mut version_map: HashMap<String, Vec<String>> = HashMap::new();
297
298        // Collect all versions for each plugin
299        for component in &composite.components {
300            version_map
301                .entry(component.name.clone())
302                .or_default()
303                .push(component.version.clone());
304        }
305
306        let mut conflicts = Vec::new();
307
308        // Detect conflicts
309        for (plugin_name, versions) in version_map {
310            if versions.len() > 1 {
311                let resolved = versions[0].clone();
312                conflicts.push(VersionConflict::new(
313                    &plugin_name,
314                    versions,
315                    &resolved,
316                    composite.conflict_resolution,
317                ));
318            }
319        }
320
321        conflicts
322    }
323
324    /// Validate composite plugin integrity
325    pub fn validate_composite(composite: &CompositePlugin) -> ValidationResult {
326        let mut result = ValidationResult::new();
327
328        // Check minimum components
329        if composite.components.is_empty() {
330            result.add_error("Composite plugin has no components");
331        }
332
333        // Check for conflicts
334        let conflicts = Self::detect_version_conflicts(composite);
335        if !conflicts.is_empty() {
336            result.add_warning(&format!("Found {} version conflicts", conflicts.len()));
337        }
338
339        // Check required components
340        let required_count = composite.required_components().len();
341        if required_count == 0 {
342            result.add_warning("No required components in composite plugin");
343        }
344
345        result
346    }
347
348    /// Merge multiple composite plugins
349    pub fn merge_composites(plugins: &[&CompositePlugin]) -> Result<CompositePlugin, String> {
350        if plugins.is_empty() {
351            return Err("No plugins to merge".to_string());
352        }
353
354        let merged_name = format!("merged-{}", chrono::Utc::now().timestamp_millis());
355        let mut merged = CompositePlugin::new(&merged_name, "1.0.0", "Merged composite plugin");
356
357        for plugin in plugins {
358            for component in &plugin.components {
359                merged.add_component(component.clone());
360            }
361
362            for (name, version) in &plugin.transitive_dependencies {
363                merged.add_transitive_dependency(name.as_str(), version.as_str());
364            }
365        }
366
367        Ok(merged)
368    }
369
370    /// Extract components from a composite plugin
371    pub fn extract_components(composite: &CompositePlugin) -> Vec<PluginComponent> {
372        composite.components.clone()
373    }
374
375    /// Calculate composite plugin size
376    pub fn calculate_size(composite: &CompositePlugin) -> CompositeSize {
377        CompositeSize {
378            components: composite.components.len(),
379            required: composite.required_components().len(),
380            optional: composite.optional_components().len(),
381            transitive_deps: composite.transitive_dependencies.len(),
382            total: composite.total_dependencies(),
383        }
384    }
385}
386
387/// Composite plugin size metrics
388#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct CompositeSize {
390    pub components: usize,
391    pub required: usize,
392    pub optional: usize,
393    pub transitive_deps: usize,
394    pub total: usize,
395}
396
397/// Validation result
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct ValidationResult {
400    pub valid: bool,
401    pub errors: Vec<String>,
402    pub warnings: Vec<String>,
403}
404
405impl ValidationResult {
406    pub fn new() -> Self {
407        Self {
408            valid: true,
409            errors: Vec::new(),
410            warnings: Vec::new(),
411        }
412    }
413
414    pub fn add_error(&mut self, error: &str) {
415        self.errors.push(error.to_string());
416        self.valid = false;
417    }
418
419    pub fn add_warning(&mut self, warning: &str) {
420        self.warnings.push(warning.to_string());
421    }
422
423    pub fn is_valid(&self) -> bool {
424        self.valid && self.errors.is_empty()
425    }
426}
427
428impl Default for ValidationResult {
429    fn default() -> Self {
430        Self::new()
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn test_plugin_component_creation() {
440        let component = PluginComponent::new("plugin-a", "1.0.0");
441        assert_eq!(component.name, "plugin-a");
442        assert_eq!(component.version, "1.0.0");
443        assert!(component.required);
444    }
445
446    #[test]
447    fn test_plugin_component_optional() {
448        let component = PluginComponent::new("plugin-a", "1.0.0").optional();
449        assert!(!component.required);
450    }
451
452    #[test]
453    fn test_plugin_component_with_description() {
454        let component = PluginComponent::new("plugin-a", "1.0.0").with_description("A test plugin");
455        assert!(component.description.is_some());
456    }
457
458    #[test]
459    fn test_conflict_resolution_to_str() {
460        assert_eq!(ConflictResolution::Newest.as_str(), "newest");
461        assert_eq!(ConflictResolution::Oldest.as_str(), "oldest");
462        assert_eq!(ConflictResolution::Exact.as_str(), "exact");
463    }
464
465    #[test]
466    fn test_version_conflict_creation() {
467        let conflict = VersionConflict::new(
468            "plugin-a",
469            vec!["1.0.0".to_string(), "2.0.0".to_string()],
470            "2.0.0",
471            ConflictResolution::Newest,
472        );
473        assert_eq!(conflict.plugin_name, "plugin-a");
474        assert_eq!(conflict.versions.len(), 2);
475    }
476
477    #[test]
478    fn test_composite_plugin_creation() {
479        let composite = CompositePlugin::new("composite", "1.0.0", "A composite plugin");
480        assert_eq!(composite.name, "composite");
481        assert_eq!(composite.component_count(), 0);
482    }
483
484    #[test]
485    fn test_composite_plugin_add_component() {
486        let mut composite = CompositePlugin::new("composite", "1.0.0", "A composite plugin");
487        let component = PluginComponent::new("plugin-a", "1.0.0");
488        composite.add_component(component);
489        assert_eq!(composite.component_count(), 1);
490    }
491
492    #[test]
493    fn test_composite_plugin_required_components() {
494        let mut composite = CompositePlugin::new("composite", "1.0.0", "A composite plugin");
495        composite.add_component(PluginComponent::new("plugin-a", "1.0.0"));
496        composite.add_component(PluginComponent::new("plugin-b", "1.0.0").optional());
497        let required = composite.required_components();
498        assert_eq!(required.len(), 1);
499    }
500
501    #[test]
502    fn test_composite_plugin_optional_components() {
503        let mut composite = CompositePlugin::new("composite", "1.0.0", "A composite plugin");
504        composite.add_component(PluginComponent::new("plugin-a", "1.0.0"));
505        composite.add_component(PluginComponent::new("plugin-b", "1.0.0").optional());
506        let optional = composite.optional_components();
507        assert_eq!(optional.len(), 1);
508    }
509
510    #[test]
511    fn test_composite_plugin_add_transitive_dependency() {
512        let mut composite = CompositePlugin::new("composite", "1.0.0", "A composite plugin");
513        composite.add_transitive_dependency("dep-a", "1.0.0");
514        assert_eq!(composite.transitive_dependencies.len(), 1);
515    }
516
517    #[test]
518    fn test_bundle_type_to_str() {
519        assert_eq!(BundleType::Standalone.as_str(), "standalone");
520        assert_eq!(BundleType::Composite.as_str(), "composite");
521        assert_eq!(BundleType::Collection.as_str(), "collection");
522    }
523
524    #[test]
525    fn test_plugin_bundle_creation() {
526        let bundle = PluginBundle::new("bundle", "1.0.0", BundleType::Composite);
527        assert_eq!(bundle.name, "bundle");
528        assert_eq!(bundle.plugin_count(), 0);
529    }
530
531    #[test]
532    fn test_plugin_bundle_add_plugin() {
533        let mut bundle = PluginBundle::new("bundle", "1.0.0", BundleType::Composite);
534        bundle.add_plugin("plugin-a".to_string());
535        assert_eq!(bundle.plugin_count(), 1);
536    }
537
538    #[test]
539    fn test_dependency_resolution_result_creation() {
540        let result = DependencyResolutionResult::new();
541        assert!(result.success);
542        assert_eq!(result.resolved_count(), 0);
543    }
544
545    #[test]
546    fn test_dependency_resolution_result_add_conflict() {
547        let mut result = DependencyResolutionResult::new();
548        let conflict = VersionConflict::new(
549            "plugin-a",
550            vec!["1.0.0".to_string()],
551            "1.0.0",
552            ConflictResolution::Newest,
553        );
554        result.add_conflict(conflict);
555        assert!(!result.success);
556    }
557
558    #[test]
559    fn test_dependency_resolution_result_add_unresolvable() {
560        let mut result = DependencyResolutionResult::new();
561        result.add_unresolvable("plugin-x".to_string());
562        assert!(!result.success);
563    }
564
565    #[test]
566    fn test_composition_manager_detect_conflicts() {
567        let mut composite = CompositePlugin::new("composite", "1.0.0", "A composite plugin");
568        composite.add_component(PluginComponent::new("plugin-a", "1.0.0"));
569        composite.add_component(PluginComponent::new("plugin-a", "2.0.0"));
570        let conflicts = CompositionManager::detect_version_conflicts(&composite);
571        assert_eq!(conflicts.len(), 1);
572    }
573
574    #[test]
575    fn test_composition_manager_validate_composite() {
576        let mut composite = CompositePlugin::new("composite", "1.0.0", "A composite plugin");
577        composite.add_component(PluginComponent::new("plugin-a", "1.0.0"));
578        let result = CompositionManager::validate_composite(&composite);
579        assert!(result.is_valid());
580    }
581
582    #[test]
583    fn test_composition_manager_validate_empty_composite() {
584        let composite = CompositePlugin::new("composite", "1.0.0", "A composite plugin");
585        let result = CompositionManager::validate_composite(&composite);
586        assert!(!result.is_valid());
587    }
588
589    #[test]
590    fn test_composition_manager_calculate_size() {
591        let mut composite = CompositePlugin::new("composite", "1.0.0", "A composite plugin");
592        composite.add_component(PluginComponent::new("plugin-a", "1.0.0"));
593        composite.add_component(PluginComponent::new("plugin-b", "1.0.0").optional());
594        composite.add_transitive_dependency("dep-a", "1.0.0");
595        let size = CompositionManager::calculate_size(&composite);
596        assert_eq!(size.components, 2);
597        assert_eq!(size.required, 1);
598        assert_eq!(size.optional, 1);
599    }
600
601    #[test]
602    fn test_composite_plugin_serialization() {
603        let composite = CompositePlugin::new("composite", "1.0.0", "A composite plugin");
604        let json = serde_json::to_string(&composite).unwrap();
605        let deserialized: CompositePlugin = serde_json::from_str(&json).unwrap();
606        assert_eq!(deserialized.name, composite.name);
607    }
608
609    #[test]
610    fn test_plugin_bundle_serialization() {
611        let bundle = PluginBundle::new("bundle", "1.0.0", BundleType::Composite);
612        let json = serde_json::to_string(&bundle).unwrap();
613        let deserialized: PluginBundle = serde_json::from_str(&json).unwrap();
614        assert_eq!(deserialized.name, bundle.name);
615    }
616
617    #[test]
618    fn test_validation_result_creation() {
619        let result = ValidationResult::new();
620        assert!(result.valid);
621    }
622
623    #[test]
624    fn test_validation_result_add_error() {
625        let mut result = ValidationResult::new();
626        result.add_error("Test error");
627        assert!(!result.valid);
628    }
629
630    #[test]
631    fn test_validation_result_add_warning() {
632        let mut result = ValidationResult::new();
633        result.add_warning("Test warning");
634        assert!(result.valid);
635    }
636}