1use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13
14#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "lowercase")]
47pub enum ConflictResolution {
48 Newest, Oldest, Exact, Compatible, }
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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153#[serde(rename_all = "lowercase")]
154pub enum BundleType {
155 Standalone, Composite, Collection, }
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#[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#[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
248pub struct CompositionManager;
250
251impl CompositionManager {
252 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(¤t) {
263 continue;
264 }
265 visited.insert(current.clone());
266
267 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 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 pub fn detect_version_conflicts(composite: &CompositePlugin) -> Vec<VersionConflict> {
296 let mut version_map: HashMap<String, Vec<String>> = HashMap::new();
297
298 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 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 pub fn validate_composite(composite: &CompositePlugin) -> ValidationResult {
326 let mut result = ValidationResult::new();
327
328 if composite.components.is_empty() {
330 result.add_error("Composite plugin has no components");
331 }
332
333 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 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 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 pub fn extract_components(composite: &CompositePlugin) -> Vec<PluginComponent> {
372 composite.components.clone()
373 }
374
375 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#[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#[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}