shimexe_core/
config.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::{Path, PathBuf};
4use std::sync::{Arc, Mutex};
5use std::time::{Duration, SystemTime};
6
7use crate::error::{Result, ShimError};
8use crate::template::ArgsConfig;
9use crate::utils::expand_env_vars;
10
11/// Configuration cache entry
12#[derive(Debug, Clone)]
13struct CacheEntry {
14    config: ShimConfig,
15    last_modified: SystemTime,
16    cached_at: SystemTime,
17}
18
19/// Configuration cache for improved performance
20#[derive(Debug, Clone)]
21pub struct ConfigCache {
22    cache: Arc<Mutex<HashMap<PathBuf, CacheEntry>>>,
23    ttl: Duration,
24}
25
26impl ConfigCache {
27    /// Create a new configuration cache with specified TTL
28    pub fn new(ttl: Duration) -> Self {
29        Self {
30            cache: Arc::new(Mutex::new(HashMap::new())),
31            ttl,
32        }
33    }
34
35    /// Get configuration from cache or load from file
36    pub fn get_or_load<P: AsRef<Path>>(&self, path: P) -> Result<ShimConfig> {
37        let path = path.as_ref().to_path_buf();
38        let now = SystemTime::now();
39
40        // Check cache first
41        if let Ok(cache) = self.cache.lock() {
42            if let Some(entry) = cache.get(&path) {
43                // Check if cache entry is still valid
44                if now.duration_since(entry.cached_at).unwrap_or(Duration::MAX) < self.ttl {
45                    // Check if file hasn't been modified
46                    if let Ok(metadata) = std::fs::metadata(&path) {
47                        if let Ok(modified) = metadata.modified() {
48                            if modified <= entry.last_modified {
49                                return Ok(entry.config.clone());
50                            }
51                        }
52                    }
53                }
54            }
55        }
56
57        // Load from file and update cache
58        let config = ShimConfig::from_file(&path)?;
59        let last_modified = std::fs::metadata(&path)
60            .and_then(|m| m.modified())
61            .unwrap_or(now);
62
63        if let Ok(mut cache) = self.cache.lock() {
64            cache.insert(
65                path,
66                CacheEntry {
67                    config: config.clone(),
68                    last_modified,
69                    cached_at: now,
70                },
71            );
72        }
73
74        Ok(config)
75    }
76
77    /// Invalidate cache entry for a specific path
78    pub fn invalidate<P: AsRef<Path>>(&self, path: P) {
79        let path = path.as_ref().to_path_buf();
80        if let Ok(mut cache) = self.cache.lock() {
81            cache.remove(&path);
82        }
83    }
84
85    /// Clear all cache entries
86    pub fn clear(&self) {
87        if let Ok(mut cache) = self.cache.lock() {
88            cache.clear();
89        }
90    }
91
92    /// Get cache statistics
93    pub fn stats(&self) -> (usize, usize) {
94        if let Ok(cache) = self.cache.lock() {
95            let total = cache.len();
96            let now = SystemTime::now();
97            let valid = cache
98                .values()
99                .filter(|entry| {
100                    now.duration_since(entry.cached_at).unwrap_or(Duration::MAX) < self.ttl
101                })
102                .count();
103            (total, valid)
104        } else {
105            (0, 0)
106        }
107    }
108}
109
110impl Default for ConfigCache {
111    /// Create a new configuration cache with default TTL (5 minutes)
112    fn default() -> Self {
113        Self::new(Duration::from_secs(300))
114    }
115}
116
117/// Main shim configuration structure
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ShimConfig {
120    /// Core shim configuration
121    pub shim: ShimCore,
122    /// Advanced argument configuration
123    #[serde(default)]
124    pub args: ArgsConfig,
125    /// Environment variables to set
126    #[serde(default)]
127    pub env: HashMap<String, String>,
128    /// Optional metadata
129    #[serde(default)]
130    pub metadata: ShimMetadata,
131    /// Auto-update configuration
132    #[serde(default)]
133    pub auto_update: Option<AutoUpdate>,
134}
135
136/// Core shim configuration
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct ShimCore {
139    /// Name of the shim
140    pub name: String,
141    /// Path to the target executable
142    pub path: String,
143    /// Default arguments to pass to the executable
144    #[serde(default)]
145    pub args: Vec<String>,
146    /// Working directory for the executable
147    #[serde(default)]
148    pub cwd: Option<String>,
149    /// Original download URL (for HTTP-based shims)
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub download_url: Option<String>,
152    /// Type of source (file, archive, url)
153    #[serde(default)]
154    pub source_type: SourceType,
155    /// For archives: list of extracted executables
156    #[serde(default, skip_serializing_if = "Vec::is_empty")]
157    pub extracted_executables: Vec<ExtractedExecutable>,
158}
159
160/// Source type for the shim
161#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
162#[serde(rename_all = "lowercase")]
163pub enum SourceType {
164    /// Regular file or executable
165    #[default]
166    File,
167    /// Archive file (zip, tar.gz, etc.)
168    Archive,
169    /// HTTP URL
170    Url,
171}
172
173/// Information about an extracted executable from an archive
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ExtractedExecutable {
176    /// Name of the executable (without extension)
177    pub name: String,
178    /// Relative path within the extracted archive
179    pub path: String,
180    /// Full path to the executable
181    pub full_path: String,
182    /// Whether this executable is the primary one for this shim
183    #[serde(default)]
184    pub is_primary: bool,
185}
186
187/// Optional metadata for the shim
188#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189pub struct ShimMetadata {
190    /// Description of the shim
191    pub description: Option<String>,
192    /// Version of the shim configuration
193    pub version: Option<String>,
194    /// Author of the shim
195    pub author: Option<String>,
196    /// Tags for categorization
197    #[serde(default)]
198    pub tags: Vec<String>,
199}
200
201/// Auto-update configuration for the shim
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct AutoUpdate {
204    /// Enable auto-update
205    #[serde(default)]
206    pub enabled: bool,
207    /// Update provider type
208    pub provider: UpdateProvider,
209    /// Download URL template with version placeholder
210    pub download_url: String,
211    /// Version check URL or pattern
212    pub version_check: VersionCheck,
213    /// Update frequency in hours (0 = check every run)
214    #[serde(default)]
215    pub check_interval_hours: u64,
216    /// Pre-update command to run
217    pub pre_update_command: Option<String>,
218    /// Post-update command to run
219    pub post_update_command: Option<String>,
220}
221
222/// Update provider types
223#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(rename_all = "lowercase")]
225pub enum UpdateProvider {
226    /// GitHub releases
227    Github {
228        /// Repository owner/name
229        repo: String,
230        /// Asset name pattern (supports {version}, {os}, {arch} placeholders)
231        asset_pattern: String,
232        /// Include pre-releases
233        #[serde(default)]
234        include_prerelease: bool,
235    },
236    /// Direct HTTPS download
237    Https {
238        /// Base URL for downloads
239        base_url: String,
240        /// URL pattern for version checking
241        version_url: Option<String>,
242    },
243    /// Custom provider
244    Custom {
245        /// Custom update script or command
246        update_command: String,
247        /// Custom version check command
248        version_command: String,
249    },
250}
251
252/// Version check configuration
253#[derive(Debug, Clone, Serialize, Deserialize)]
254#[serde(rename_all = "lowercase")]
255pub enum VersionCheck {
256    /// GitHub API for latest release
257    GithubLatest {
258        /// Repository owner/name
259        repo: String,
260        /// Include pre-releases
261        #[serde(default)]
262        include_prerelease: bool,
263    },
264    /// HTTP endpoint returning version
265    Http {
266        /// URL to check for version
267        url: String,
268        /// JSON path to extract version (e.g., "$.version")
269        json_path: Option<String>,
270        /// Regex pattern to extract version
271        regex_pattern: Option<String>,
272    },
273    /// Semantic version comparison
274    Semver {
275        /// Current version
276        current: String,
277        /// Version check URL
278        check_url: String,
279    },
280    /// Custom version check command
281    Command {
282        /// Command to run for version check
283        command: String,
284        /// Arguments for the command
285        args: Vec<String>,
286    },
287}
288
289impl ShimConfig {
290    /// Create a new shim configuration with minimal required fields
291    pub fn new(name: impl Into<String>, path: impl Into<String>) -> Self {
292        Self {
293            shim: ShimCore {
294                name: name.into(),
295                path: path.into(),
296                args: Vec::new(),
297                cwd: None,
298                download_url: None,
299                source_type: SourceType::File,
300                extracted_executables: Vec::new(),
301            },
302            args: Default::default(),
303            env: HashMap::new(),
304            metadata: ShimMetadata::default(),
305            auto_update: None,
306        }
307    }
308
309    /// Load shim configuration from a TOML file
310    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
311        let content = std::fs::read_to_string(&path).map_err(ShimError::Io)?;
312
313        let config: ShimConfig = toml::from_str(&content).map_err(ShimError::TomlParse)?;
314
315        config.validate()?;
316        Ok(config)
317    }
318
319    /// Load shim configuration from a TOML file asynchronously
320    pub async fn from_file_async<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
321        let content = tokio::fs::read_to_string(&path)
322            .await
323            .map_err(ShimError::Io)?;
324
325        let config: ShimConfig = toml::from_str(&content).map_err(ShimError::TomlParse)?;
326
327        config.validate()?;
328        Ok(config)
329    }
330
331    /// Save shim configuration to a TOML file
332    pub fn to_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
333        // Check if file already exists and has the same content to avoid unnecessary writes
334        if let Ok(existing_content) = std::fs::read_to_string(&path) {
335            let new_content = toml::to_string_pretty(self).map_err(ShimError::TomlSerialize)?;
336            if existing_content.trim() == new_content.trim() {
337                return Ok(()); // No changes needed
338            }
339        }
340
341        let content = toml::to_string_pretty(self).map_err(ShimError::TomlSerialize)?;
342        std::fs::write(path, content).map_err(ShimError::Io)?;
343
344        Ok(())
345    }
346
347    /// Save shim configuration to a TOML file asynchronously
348    pub async fn to_file_async<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
349        // Check if file already exists and has the same content to avoid unnecessary writes
350        if let Ok(existing_content) = tokio::fs::read_to_string(&path).await {
351            let new_content = toml::to_string_pretty(self).map_err(ShimError::TomlSerialize)?;
352            if existing_content.trim() == new_content.trim() {
353                return Ok(()); // No changes needed
354            }
355        }
356
357        let content = toml::to_string_pretty(self).map_err(ShimError::TomlSerialize)?;
358        tokio::fs::write(path, content)
359            .await
360            .map_err(ShimError::Io)?;
361
362        Ok(())
363    }
364
365    /// Load multiple configuration files concurrently
366    pub async fn from_files_concurrent<P: AsRef<std::path::Path>>(
367        paths: Vec<P>,
368    ) -> Vec<Result<Self>> {
369        use futures_util::future::join_all;
370
371        let futures = paths
372            .into_iter()
373            .map(|path| Self::from_file_async(path))
374            .collect::<Vec<_>>();
375
376        join_all(futures).await
377    }
378
379    /// Save multiple configurations concurrently
380    pub async fn to_files_concurrent<P: AsRef<std::path::Path>>(
381        configs_and_paths: Vec<(&Self, P)>,
382    ) -> Vec<Result<()>> {
383        use futures_util::future::join_all;
384
385        let futures = configs_and_paths
386            .into_iter()
387            .map(|(config, path)| config.to_file_async(path))
388            .collect::<Vec<_>>();
389
390        join_all(futures).await
391    }
392
393    /// Validate the configuration
394    pub fn validate(&self) -> Result<()> {
395        if self.shim.name.is_empty() {
396            return Err(ShimError::Config("Shim name cannot be empty".to_string()));
397        }
398
399        if self.shim.path.is_empty() {
400            return Err(ShimError::Config("Shim path cannot be empty".to_string()));
401        }
402
403        Ok(())
404    }
405
406    /// Expand environment variables in the configuration
407    pub fn expand_env_vars(&mut self) -> Result<()> {
408        // Expand path
409        self.shim.path = expand_env_vars(&self.shim.path)?;
410
411        // Expand args
412        for arg in &mut self.shim.args {
413            *arg = expand_env_vars(arg)?;
414        }
415
416        // Expand cwd if present
417        if let Some(ref mut cwd) = self.shim.cwd {
418            *cwd = expand_env_vars(cwd)?;
419        }
420
421        // Expand environment variables
422        for value in self.env.values_mut() {
423            *value = expand_env_vars(value)?;
424        }
425
426        Ok(())
427    }
428
429    /// Get the resolved executable path
430    pub fn get_executable_path(&self) -> Result<PathBuf> {
431        let expanded_path = expand_env_vars(&self.shim.path)?;
432
433        match self.shim.source_type {
434            SourceType::Archive => {
435                // For archives, use the primary executable or the first one
436                if let Some(primary_exe) = self
437                    .shim
438                    .extracted_executables
439                    .iter()
440                    .find(|exe| exe.is_primary)
441                    .or_else(|| self.shim.extracted_executables.first())
442                {
443                    let path = PathBuf::from(&primary_exe.full_path);
444                    if path.exists() {
445                        Ok(path)
446                    } else {
447                        Err(ShimError::ExecutableNotFound(format!(
448                            "Extracted executable not found: {}. Re-extraction may be required.",
449                            primary_exe.full_path
450                        )))
451                    }
452                } else {
453                    Err(ShimError::ExecutableNotFound(
454                        "No extracted executables found in archive configuration".to_string(),
455                    ))
456                }
457            }
458            SourceType::Url => {
459                // Check if we have a download_url (indicating this was originally from HTTP)
460                if let Some(ref download_url) = self.shim.download_url {
461                    // This shim was created from an HTTP URL
462                    let filename =
463                        crate::downloader::Downloader::extract_filename_from_url(download_url)
464                            .ok_or_else(|| {
465                                ShimError::Config(format!(
466                                    "Could not extract filename from download URL: {}",
467                                    download_url
468                                ))
469                            })?;
470
471                    // Try to find the downloaded file in the expected location
472                    // First try relative to home directory
473                    if let Some(home_dir) = dirs::home_dir() {
474                        let download_path = home_dir
475                            .join(".shimexe")
476                            .join(&self.shim.name)
477                            .join("bin")
478                            .join(&filename);
479
480                        if download_path.exists() {
481                            return Ok(download_path);
482                        }
483                    }
484
485                    // If not found, return an error indicating download is needed
486                    Err(ShimError::ExecutableNotFound(format!(
487                        "Executable not found for download URL: {}. Download may be required.",
488                        download_url
489                    )))
490                } else if crate::downloader::Downloader::is_url(&expanded_path) {
491                    // Legacy: path is still a URL (for backward compatibility)
492                    let filename =
493                        crate::downloader::Downloader::extract_filename_from_url(&expanded_path)
494                            .ok_or_else(|| {
495                                ShimError::Config(format!(
496                                    "Could not extract filename from URL: {}",
497                                    expanded_path
498                                ))
499                            })?;
500
501                    // Try to find the downloaded file in the expected location
502                    if let Some(home_dir) = dirs::home_dir() {
503                        let download_path = home_dir
504                            .join(".shimexe")
505                            .join(&self.shim.name)
506                            .join("bin")
507                            .join(&filename);
508
509                        if download_path.exists() {
510                            return Ok(download_path);
511                        }
512                    }
513
514                    // If not found, return an error indicating download is needed
515                    Err(ShimError::ExecutableNotFound(format!(
516                        "Executable not found for URL: {}. Download may be required.",
517                        expanded_path
518                    )))
519                } else {
520                    Err(ShimError::Config(
521                        "URL source type specified but no download URL found".to_string(),
522                    ))
523                }
524            }
525            SourceType::File => {
526                let path = PathBuf::from(expanded_path);
527
528                if path.is_absolute() {
529                    Ok(path)
530                } else {
531                    // Try to find in PATH
532                    which::which(&path)
533                        .map_err(|_| ShimError::ExecutableNotFound(self.shim.path.clone()))
534                }
535            }
536        }
537    }
538
539    /// Get the download URL for this shim (if it was created from HTTP)
540    pub fn get_download_url(&self) -> Option<&String> {
541        self.shim.download_url.as_ref()
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548    use std::io::Write;
549    use tempfile::NamedTempFile;
550
551    #[test]
552    fn test_shim_config_from_file() {
553        let mut temp_file = NamedTempFile::new().unwrap();
554        writeln!(
555            temp_file,
556            r#"
557[shim]
558name = "test"
559path = "echo"
560args = ["hello"]
561
562[env]
563TEST_VAR = "test_value"
564
565[metadata]
566description = "Test shim"
567version = "1.0.0"
568        "#
569        )
570        .unwrap();
571
572        let config = ShimConfig::from_file(temp_file.path()).unwrap();
573        assert_eq!(config.shim.name, "test");
574        assert_eq!(config.shim.path, "echo");
575        assert_eq!(config.shim.args, vec!["hello"]);
576        assert_eq!(config.env.get("TEST_VAR"), Some(&"test_value".to_string()));
577        assert_eq!(config.metadata.description, Some("Test shim".to_string()));
578    }
579
580    #[test]
581    fn test_shim_config_basic_structure() {
582        let mut temp_file = NamedTempFile::new().unwrap();
583        writeln!(
584            temp_file,
585            r#"
586[shim]
587name = "test"
588path = "echo"
589
590[args]
591mode = "template"
592
593[metadata]
594description = "Test shim"
595        "#
596        )
597        .unwrap();
598
599        let config = ShimConfig::from_file(temp_file.path()).unwrap();
600        assert_eq!(config.shim.name, "test");
601        assert_eq!(config.args.mode, crate::template::ArgsMode::Template);
602        assert_eq!(config.metadata.description, Some("Test shim".to_string()));
603    }
604
605    #[test]
606    fn test_shim_config_validation() {
607        // Valid config
608        let config = ShimConfig {
609            shim: ShimCore {
610                name: "test".to_string(),
611                path: "echo".to_string(),
612                args: vec![],
613                cwd: None,
614                download_url: None,
615                source_type: SourceType::File,
616                extracted_executables: vec![],
617            },
618            args: Default::default(),
619            env: HashMap::new(),
620            metadata: Default::default(),
621            auto_update: None,
622        };
623        assert!(config.validate().is_ok());
624
625        // Invalid config - empty name
626        let invalid_config = ShimConfig {
627            shim: ShimCore {
628                name: "".to_string(),
629                path: "echo".to_string(),
630                args: vec![],
631                cwd: None,
632                download_url: None,
633                source_type: SourceType::File,
634                extracted_executables: vec![],
635            },
636            args: Default::default(),
637            env: HashMap::new(),
638            metadata: Default::default(),
639            auto_update: None,
640        };
641        assert!(invalid_config.validate().is_err());
642
643        // Invalid config - empty path
644        let invalid_config = ShimConfig {
645            shim: ShimCore {
646                name: "test".to_string(),
647                path: "".to_string(),
648                args: vec![],
649                cwd: None,
650                download_url: None,
651                source_type: SourceType::File,
652                extracted_executables: vec![],
653            },
654            args: Default::default(),
655            env: HashMap::new(),
656            metadata: Default::default(),
657            auto_update: None,
658        };
659        assert!(invalid_config.validate().is_err());
660    }
661
662    #[test]
663    fn test_shim_config_to_file() {
664        let config = ShimConfig {
665            shim: ShimCore {
666                name: "test".to_string(),
667                path: "echo".to_string(),
668                args: vec!["hello".to_string()],
669                cwd: None,
670                download_url: None,
671                source_type: SourceType::File,
672                extracted_executables: vec![],
673            },
674            args: Default::default(),
675            env: {
676                let mut env = HashMap::new();
677                env.insert("TEST_VAR".to_string(), "test_value".to_string());
678                env
679            },
680            metadata: ShimMetadata {
681                description: Some("Test shim".to_string()),
682                version: Some("1.0.0".to_string()),
683                author: None,
684                tags: vec![],
685            },
686            auto_update: None,
687        };
688
689        let temp_file = NamedTempFile::new().unwrap();
690        config.to_file(temp_file.path()).unwrap();
691
692        // Read back and verify
693        let loaded_config = ShimConfig::from_file(temp_file.path()).unwrap();
694        assert_eq!(loaded_config.shim.name, config.shim.name);
695        assert_eq!(loaded_config.shim.path, config.shim.path);
696        assert_eq!(loaded_config.shim.args, config.shim.args);
697        assert_eq!(loaded_config.env, config.env);
698    }
699
700    #[test]
701    fn test_expand_env_vars() {
702        std::env::set_var("TEST_VAR", "test_value");
703
704        let mut config = ShimConfig {
705            shim: ShimCore {
706                name: "test".to_string(),
707                path: "${TEST_VAR}/bin/test".to_string(),
708                args: vec!["${TEST_VAR}".to_string()],
709                cwd: Some("${TEST_VAR}/work".to_string()),
710                download_url: None,
711                source_type: SourceType::File,
712                extracted_executables: vec![],
713            },
714            args: Default::default(),
715            env: {
716                let mut env = HashMap::new();
717                env.insert("EXPANDED".to_string(), "${TEST_VAR}_expanded".to_string());
718                env
719            },
720            metadata: Default::default(),
721            auto_update: None,
722        };
723
724        config.expand_env_vars().unwrap();
725
726        assert_eq!(config.shim.path, "test_value/bin/test");
727        assert_eq!(config.shim.args[0], "test_value");
728        assert_eq!(config.shim.cwd, Some("test_value/work".to_string()));
729        assert_eq!(
730            config.env.get("EXPANDED"),
731            Some(&"test_value_expanded".to_string())
732        );
733
734        std::env::remove_var("TEST_VAR");
735    }
736
737    #[test]
738    fn test_shim_config_with_args_template() {
739        let mut temp_file = NamedTempFile::new().unwrap();
740        write!(
741            temp_file,
742            r#"
743[shim]
744name = "test"
745path = "echo"
746
747[args]
748mode = "template"
749template = [
750    "{{{{if env('DEBUG') == 'true'}}}}--verbose{{{{endif}}}}",
751    "{{{{args('--version')}}}}"
752]
753
754[metadata]
755description = "Test shim with template args"
756        "#
757        )
758        .unwrap();
759
760        let config = ShimConfig::from_file(temp_file.path()).unwrap();
761        assert_eq!(config.shim.name, "test");
762        assert_eq!(config.args.mode, crate::template::ArgsMode::Template);
763        assert!(config.args.template.is_some());
764
765        let template = config.args.template.unwrap();
766        assert_eq!(template.len(), 2);
767        assert_eq!(
768            template[0],
769            "{{if env('DEBUG') == 'true'}}--verbose{{endif}}"
770        );
771        assert_eq!(template[1], "{{args('--version')}}");
772    }
773
774    #[test]
775    fn test_shim_config_with_args_modes() {
776        // Test merge mode
777        let mut temp_file = NamedTempFile::new().unwrap();
778        writeln!(
779            temp_file,
780            r#"
781[shim]
782name = "test"
783path = "echo"
784
785[args]
786mode = "merge"
787default = ["--default"]
788prefix = ["--prefix"]
789suffix = ["--suffix"]
790        "#
791        )
792        .unwrap();
793
794        let config = ShimConfig::from_file(temp_file.path()).unwrap();
795        assert_eq!(config.args.mode, crate::template::ArgsMode::Merge);
796        assert_eq!(config.args.default, vec!["--default"]);
797        assert_eq!(config.args.prefix, vec!["--prefix"]);
798        assert_eq!(config.args.suffix, vec!["--suffix"]);
799    }
800
801    #[test]
802    fn test_shim_config_with_inline_template() {
803        let mut temp_file = NamedTempFile::new().unwrap();
804        write!(
805            temp_file,
806            r#"
807[shim]
808name = "test"
809path = "echo"
810
811[args]
812inline = "{{{{env('CONFIG_PATH', '/default/config')}}}} {{{{args('--help')}}}}"
813        "#
814        )
815        .unwrap();
816
817        let config = ShimConfig::from_file(temp_file.path()).unwrap();
818        assert!(config.args.inline.is_some());
819        assert_eq!(
820            config.args.inline.unwrap(),
821            "{{env('CONFIG_PATH', '/default/config')}} {{args('--help')}}"
822        );
823    }
824
825    #[test]
826    fn test_shim_config_with_download_url() {
827        let mut temp_file = NamedTempFile::new().unwrap();
828        writeln!(
829            temp_file,
830            r#"
831[shim]
832name = "test-tool"
833path = "/home/user/.shimexe/test-tool/bin/test-tool.exe"
834download_url = "https://example.com/test-tool.exe"
835
836[metadata]
837description = "Test shim with download URL"
838        "#
839        )
840        .unwrap();
841
842        let config = ShimConfig::from_file(temp_file.path()).unwrap();
843        assert_eq!(config.shim.name, "test-tool");
844        assert_eq!(
845            config.shim.path,
846            "/home/user/.shimexe/test-tool/bin/test-tool.exe"
847        );
848        assert_eq!(
849            config.shim.download_url,
850            Some("https://example.com/test-tool.exe".to_string())
851        );
852        assert_eq!(
853            config.get_download_url(),
854            Some(&"https://example.com/test-tool.exe".to_string())
855        );
856    }
857
858    #[test]
859    fn test_shim_config_without_download_url() {
860        let mut temp_file = NamedTempFile::new().unwrap();
861        writeln!(
862            temp_file,
863            r#"
864[shim]
865name = "local-tool"
866path = "/usr/bin/local-tool"
867
868[metadata]
869description = "Test shim without download URL"
870        "#
871        )
872        .unwrap();
873
874        let config = ShimConfig::from_file(temp_file.path()).unwrap();
875        assert_eq!(config.shim.name, "local-tool");
876        assert_eq!(config.shim.path, "/usr/bin/local-tool");
877        assert_eq!(config.shim.download_url, None);
878        assert_eq!(config.get_download_url(), None);
879    }
880}