Skip to main content

plugin_packager/
optional_deps.rs

1// Copyright 2024 Vincents AI
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4/// Optional dependencies and feature gates support
5///
6/// This module provides support for optional plugin dependencies:
7/// - Optional dependency flagging
8/// - Feature-gated dependencies
9/// - Platform-specific dependencies
10/// - Conditional installation logic
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14/// Represents an optional dependency
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct OptionalDependency {
17    pub name: String,
18    pub version: String,
19    pub optional: bool,
20    pub description: Option<String>,
21    pub default_enabled: bool,
22    pub platforms: Vec<String>, // "linux", "macos", "windows", "unix", "all"
23    pub features: Vec<String>,  // Which features require this dep
24}
25
26impl OptionalDependency {
27    pub fn new(name: &str, version: &str) -> Self {
28        Self {
29            name: name.to_string(),
30            version: version.to_string(),
31            optional: true,
32            description: None,
33            default_enabled: false,
34            platforms: vec!["all".to_string()],
35            features: Vec::new(),
36        }
37    }
38
39    pub fn required(mut self) -> Self {
40        self.optional = false;
41        self
42    }
43
44    pub fn with_description(mut self, desc: &str) -> Self {
45        self.description = Some(desc.to_string());
46        self
47    }
48
49    pub fn with_default_enabled(mut self) -> Self {
50        self.default_enabled = true;
51        self
52    }
53
54    pub fn with_platforms(mut self, platforms: Vec<&str>) -> Self {
55        self.platforms = platforms.iter().map(|p| p.to_string()).collect();
56        self
57    }
58
59    pub fn with_features(mut self, features: Vec<&str>) -> Self {
60        self.features = features.iter().map(|f| f.to_string()).collect();
61        self
62    }
63
64    pub fn is_platform_applicable(&self, current_platform: &str) -> bool {
65        if self.platforms.contains(&"all".to_string()) {
66            return true;
67        }
68
69        // Check for OS families
70        let is_unix_like = cfg!(unix);
71        if current_platform == "unix" && is_unix_like {
72            return true;
73        }
74
75        self.platforms.contains(&current_platform.to_string())
76    }
77
78    pub fn is_enabled(&self, enabled_features: &[String]) -> bool {
79        if self.optional && self.default_enabled {
80            return true;
81        }
82
83        // Check if any required feature is enabled
84        for feature in &self.features {
85            if enabled_features.contains(feature) {
86                return true;
87            }
88        }
89
90        false
91    }
92}
93
94/// Feature gate for conditional dependencies
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct FeatureGate {
97    pub name: String,
98    pub description: Option<String>,
99    pub enabled: bool,
100    pub dependencies: Vec<String>, // Dependencies required for this feature
101}
102
103impl FeatureGate {
104    pub fn new(name: &str) -> Self {
105        Self {
106            name: name.to_string(),
107            description: None,
108            enabled: false,
109            dependencies: Vec::new(),
110        }
111    }
112
113    pub fn with_description(mut self, desc: &str) -> Self {
114        self.description = Some(desc.to_string());
115        self
116    }
117
118    pub fn with_dependencies(mut self, deps: Vec<&str>) -> Self {
119        self.dependencies = deps.iter().map(|d| d.to_string()).collect();
120        self
121    }
122
123    pub fn enable(mut self) -> Self {
124        self.enabled = true;
125        self
126    }
127}
128
129/// Platform-specific dependency configuration
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct PlatformSpecific {
132    pub dependency: String,
133    pub version: String,
134    pub platforms: Vec<String>, // Target platforms
135    pub required: bool,
136}
137
138impl PlatformSpecific {
139    pub fn new(dependency: &str, version: &str, platforms: Vec<&str>) -> Self {
140        Self {
141            dependency: dependency.to_string(),
142            version: version.to_string(),
143            platforms: platforms.iter().map(|p| p.to_string()).collect(),
144            required: false,
145        }
146    }
147
148    pub fn required(mut self) -> Self {
149        self.required = true;
150        self
151    }
152
153    pub fn matches_current_platform(&self) -> bool {
154        for platform in &self.platforms {
155            match platform.as_str() {
156                "linux" => {
157                    if cfg!(target_os = "linux") {
158                        return true;
159                    }
160                }
161                "macos" => {
162                    if cfg!(target_os = "macos") {
163                        return true;
164                    }
165                }
166                "windows" => {
167                    if cfg!(target_os = "windows") {
168                        return true;
169                    }
170                }
171                "unix" => {
172                    if cfg!(unix) {
173                        return true;
174                    }
175                }
176                "all" => return true,
177                _ => {}
178            }
179        }
180        false
181    }
182}
183
184/// Conditional dependency requirement
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct DependencyCondition {
187    pub condition_type: ConditionType,
188    pub expression: String, // e.g., "feature(logging) && platform(linux)"
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(rename_all = "lowercase")]
193pub enum ConditionType {
194    Feature,
195    Platform,
196    And,
197    Or,
198    Not,
199}
200
201impl ConditionType {
202    pub fn as_str(&self) -> &'static str {
203        match self {
204            ConditionType::Feature => "feature",
205            ConditionType::Platform => "platform",
206            ConditionType::And => "and",
207            ConditionType::Or => "or",
208            ConditionType::Not => "not",
209        }
210    }
211}
212
213impl DependencyCondition {
214    pub fn new(cond_type: ConditionType, expression: &str) -> Self {
215        Self {
216            condition_type: cond_type,
217            expression: expression.to_string(),
218        }
219    }
220
221    pub fn is_valid_syntax(&self) -> bool {
222        !self.expression.is_empty() && self.expression.len() < 256
223    }
224
225    pub fn evaluate(
226        &self,
227        enabled_features: &[String],
228        current_platform: &str,
229    ) -> Result<bool, String> {
230        if !self.is_valid_syntax() {
231            return Err("Invalid condition syntax".to_string());
232        }
233
234        match self.condition_type {
235            ConditionType::Feature => {
236                let feature_name = self.expression.trim();
237                Ok(enabled_features.contains(&feature_name.to_string()))
238            }
239            ConditionType::Platform => {
240                Ok(self.expression == current_platform || self.expression == "all")
241            }
242            ConditionType::And => {
243                // Simple AND evaluation for feature(x) && platform(y)
244                Ok(self.expression.contains(current_platform))
245            }
246            ConditionType::Or => {
247                // Simple OR evaluation
248                Ok(true)
249            }
250            ConditionType::Not => {
251                // Simple NOT evaluation
252                Ok(!enabled_features.contains(&self.expression.to_string()))
253            }
254        }
255    }
256}
257
258/// Optional dependency manager
259pub struct OptionalDependencyManager;
260
261impl OptionalDependencyManager {
262    /// Evaluate all conditions for optional dependencies
263    pub fn evaluate_conditions(
264        dependencies: &[OptionalDependency],
265        enabled_features: &[String],
266        current_platform: &str,
267    ) -> Vec<OptionalDependency> {
268        dependencies
269            .iter()
270            .filter(|dep| {
271                dep.is_platform_applicable(current_platform) && dep.is_enabled(enabled_features)
272            })
273            .cloned()
274            .collect()
275    }
276
277    /// Filter dependencies by platform
278    pub fn filter_by_platform(
279        dependencies: &[OptionalDependency],
280        platform: &str,
281    ) -> Vec<OptionalDependency> {
282        dependencies
283            .iter()
284            .filter(|dep| dep.is_platform_applicable(platform))
285            .cloned()
286            .collect()
287    }
288
289    /// Resolve feature gates and their dependencies
290    pub fn resolve_features(features: &[FeatureGate]) -> HashMap<String, Vec<String>> {
291        let mut result = HashMap::new();
292
293        for feature in features {
294            if feature.enabled {
295                result.insert(feature.name.clone(), feature.dependencies.clone());
296            }
297        }
298
299        result
300    }
301
302    /// Validate condition expressions
303    pub fn validate_conditions(conditions: &[DependencyCondition]) -> Result<(), Vec<String>> {
304        let mut errors = Vec::new();
305
306        for (idx, cond) in conditions.iter().enumerate() {
307            if !cond.is_valid_syntax() {
308                errors.push(format!(
309                    "Invalid condition at index {}: {}",
310                    idx, cond.expression
311                ));
312            }
313        }
314
315        if errors.is_empty() {
316            Ok(())
317        } else {
318            Err(errors)
319        }
320    }
321
322    /// Calculate total optional dependencies needed
323    pub fn calculate_optional_count(
324        dependencies: &[OptionalDependency],
325        enabled_features: &[String],
326    ) -> (usize, usize) {
327        let total_optional = dependencies.iter().filter(|d| d.optional).count();
328        let enabled = Self::evaluate_conditions(dependencies, enabled_features, "all")
329            .iter()
330            .filter(|d| d.optional)
331            .count();
332
333        (enabled, total_optional)
334    }
335
336    /// Get optional dependencies for specific feature
337    pub fn get_for_feature(
338        dependencies: &[OptionalDependency],
339        feature: &str,
340    ) -> Vec<OptionalDependency> {
341        dependencies
342            .iter()
343            .filter(|dep| dep.features.contains(&feature.to_string()))
344            .cloned()
345            .collect()
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn test_optional_dependency_creation() {
355        let dep = OptionalDependency::new("plugin-a", "1.0.0");
356        assert_eq!(dep.name, "plugin-a");
357        assert!(dep.optional);
358        assert!(!dep.default_enabled);
359    }
360
361    #[test]
362    fn test_optional_dependency_required() {
363        let dep = OptionalDependency::new("plugin-a", "1.0.0").required();
364        assert!(!dep.optional);
365    }
366
367    #[test]
368    fn test_optional_dependency_with_description() {
369        let dep =
370            OptionalDependency::new("plugin-a", "1.0.0").with_description("A test dependency");
371        assert!(dep.description.is_some());
372    }
373
374    #[test]
375    fn test_optional_dependency_with_default_enabled() {
376        let dep = OptionalDependency::new("plugin-a", "1.0.0").with_default_enabled();
377        assert!(dep.default_enabled);
378    }
379
380    #[test]
381    fn test_optional_dependency_is_platform_applicable() {
382        let dep =
383            OptionalDependency::new("plugin-a", "1.0.0").with_platforms(vec!["linux", "macos"]);
384        assert!(dep.is_platform_applicable("linux"));
385        assert!(dep.is_platform_applicable("macos"));
386        assert!(!dep.is_platform_applicable("windows"));
387    }
388
389    #[test]
390    fn test_optional_dependency_is_enabled() {
391        let dep = OptionalDependency::new("plugin-a", "1.0.0").with_features(vec!["logging"]);
392        let features = vec!["logging".to_string()];
393        assert!(dep.is_enabled(&features));
394    }
395
396    #[test]
397    fn test_feature_gate_creation() {
398        let gate = FeatureGate::new("logging");
399        assert_eq!(gate.name, "logging");
400        assert!(!gate.enabled);
401    }
402
403    #[test]
404    fn test_feature_gate_enable() {
405        let gate = FeatureGate::new("logging").enable();
406        assert!(gate.enabled);
407    }
408
409    #[test]
410    fn test_feature_gate_with_dependencies() {
411        let gate = FeatureGate::new("logging").with_dependencies(vec!["slog", "serde"]);
412        assert_eq!(gate.dependencies.len(), 2);
413    }
414
415    #[test]
416    fn test_platform_specific_creation() {
417        let ps = PlatformSpecific::new("openssl", "1.1.0", vec!["linux"]);
418        assert_eq!(ps.dependency, "openssl");
419        assert_eq!(ps.platforms.len(), 1);
420    }
421
422    #[test]
423    fn test_platform_specific_required() {
424        let ps = PlatformSpecific::new("openssl", "1.1.0", vec!["linux"]).required();
425        assert!(ps.required);
426    }
427
428    #[test]
429    fn test_condition_type_to_str() {
430        assert_eq!(ConditionType::Feature.as_str(), "feature");
431        assert_eq!(ConditionType::Platform.as_str(), "platform");
432        assert_eq!(ConditionType::And.as_str(), "and");
433        assert_eq!(ConditionType::Or.as_str(), "or");
434        assert_eq!(ConditionType::Not.as_str(), "not");
435    }
436
437    #[test]
438    fn test_dependency_condition_creation() {
439        let cond = DependencyCondition::new(ConditionType::Feature, "logging");
440        assert_eq!(cond.condition_type, ConditionType::Feature);
441    }
442
443    #[test]
444    fn test_dependency_condition_is_valid_syntax() {
445        let cond = DependencyCondition::new(ConditionType::Feature, "logging");
446        assert!(cond.is_valid_syntax());
447    }
448
449    #[test]
450    fn test_dependency_condition_evaluate_feature() {
451        let cond = DependencyCondition::new(ConditionType::Feature, "logging");
452        let features = vec!["logging".to_string()];
453        let result = cond.evaluate(&features, "linux");
454        assert!(result.is_ok());
455        assert!(result.unwrap());
456    }
457
458    #[test]
459    fn test_dependency_condition_evaluate_platform() {
460        let cond = DependencyCondition::new(ConditionType::Platform, "linux");
461        let features = vec![];
462        let result = cond.evaluate(&features, "linux");
463        assert!(result.is_ok());
464        assert!(result.unwrap());
465    }
466
467    #[test]
468    fn test_optional_dependency_manager_evaluate_conditions() {
469        let deps = vec![OptionalDependency::new("plugin-a", "1.0.0")
470            .with_features(vec!["logging"])
471            .with_platforms(vec!["linux"])];
472        let features = vec!["logging".to_string()];
473        let evaluated = OptionalDependencyManager::evaluate_conditions(&deps, &features, "linux");
474        assert_eq!(evaluated.len(), 1);
475    }
476
477    #[test]
478    fn test_optional_dependency_manager_filter_by_platform() {
479        let deps = vec![
480            OptionalDependency::new("plugin-a", "1.0.0").with_platforms(vec!["linux", "macos"]),
481            OptionalDependency::new("plugin-b", "1.0.0").with_platforms(vec!["windows"]),
482        ];
483        let filtered = OptionalDependencyManager::filter_by_platform(&deps, "linux");
484        assert_eq!(filtered.len(), 1);
485    }
486
487    #[test]
488    fn test_optional_dependency_manager_resolve_features() {
489        let features = vec![FeatureGate::new("logging")
490            .enable()
491            .with_dependencies(vec!["slog"])];
492        let resolved = OptionalDependencyManager::resolve_features(&features);
493        assert_eq!(resolved.len(), 1);
494    }
495
496    #[test]
497    fn test_optional_dependency_manager_validate_conditions() {
498        let conditions = vec![
499            DependencyCondition::new(ConditionType::Feature, "logging"),
500            DependencyCondition::new(ConditionType::Platform, "linux"),
501        ];
502        let result = OptionalDependencyManager::validate_conditions(&conditions);
503        assert!(result.is_ok());
504    }
505
506    #[test]
507    fn test_optional_dependency_manager_validate_conditions_invalid() {
508        let conditions = vec![DependencyCondition::new(ConditionType::Feature, "")];
509        let result = OptionalDependencyManager::validate_conditions(&conditions);
510        assert!(result.is_err());
511    }
512
513    #[test]
514    fn test_optional_dependency_manager_calculate_optional_count() {
515        let deps = vec![
516            OptionalDependency::new("plugin-a", "1.0.0").with_features(vec!["logging"]),
517            OptionalDependency::new("plugin-b", "1.0.0").with_features(vec!["metrics"]),
518        ];
519        let features = vec!["logging".to_string()];
520        let (enabled, total) =
521            OptionalDependencyManager::calculate_optional_count(&deps, &features);
522        assert_eq!(enabled, 1);
523        assert_eq!(total, 2);
524    }
525
526    #[test]
527    fn test_optional_dependency_manager_get_for_feature() {
528        let deps = vec![
529            OptionalDependency::new("plugin-a", "1.0.0").with_features(vec!["logging"]),
530            OptionalDependency::new("plugin-b", "1.0.0").with_features(vec!["logging", "metrics"]),
531            OptionalDependency::new("plugin-c", "1.0.0").with_features(vec!["metrics"]),
532        ];
533        let for_logging = OptionalDependencyManager::get_for_feature(&deps, "logging");
534        assert_eq!(for_logging.len(), 2);
535    }
536
537    #[test]
538    fn test_optional_dependency_serialization() {
539        let dep = OptionalDependency::new("plugin-a", "1.0.0");
540        let json = serde_json::to_string(&dep).unwrap();
541        let deserialized: OptionalDependency = serde_json::from_str(&json).unwrap();
542        assert_eq!(deserialized.name, dep.name);
543    }
544
545    #[test]
546    fn test_feature_gate_serialization() {
547        let gate = FeatureGate::new("logging");
548        let json = serde_json::to_string(&gate).unwrap();
549        let deserialized: FeatureGate = serde_json::from_str(&json).unwrap();
550        assert_eq!(deserialized.name, gate.name);
551    }
552
553    #[test]
554    fn test_platform_specific_serialization() {
555        let ps = PlatformSpecific::new("openssl", "1.1.0", vec!["linux"]);
556        let json = serde_json::to_string(&ps).unwrap();
557        let deserialized: PlatformSpecific = serde_json::from_str(&json).unwrap();
558        assert_eq!(deserialized.dependency, ps.dependency);
559    }
560}