goblin_engine/
script.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3use std::time::Duration;
4use crate::error::{GoblinError, Result};
5
6/// Configuration for a script that can be loaded from TOML
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ScriptConfig {
9    pub name: String,
10    pub command: String,
11    #[serde(default = "default_timeout")]
12    pub timeout: u64, // timeout in seconds
13    #[serde(default)]
14    pub test_command: Option<String>,
15    #[serde(default)]
16    pub require_test: bool,
17}
18
19/// Runtime representation of a script with resolved paths
20#[derive(Debug, Clone)]
21pub struct Script {
22    pub name: String,
23    pub command: String,
24    pub timeout: Duration,
25    pub test_command: Option<String>,
26    pub require_test: bool,
27    pub path: PathBuf,
28}
29
30impl Script {
31    /// Create a new script from configuration and path
32    pub fn new(config: ScriptConfig, path: PathBuf) -> Self {
33        Self {
34            name: config.name,
35            command: config.command,
36            timeout: Duration::from_secs(config.timeout),
37            test_command: config.test_command,
38            require_test: config.require_test,
39            path,
40        }
41    }
42
43    /// Load a script from a goblin.toml file
44    pub fn from_toml_file(path: &PathBuf) -> Result<Self> {
45        let toml_path = path.join("goblin.toml");
46        let content = std::fs::read_to_string(&toml_path)?;
47        let config: ScriptConfig = toml::from_str(&content)?;
48        
49        // Validate required fields
50        if config.name.trim().is_empty() {
51            return Err(GoblinError::config_error("Script name cannot be empty"));
52        }
53        if config.command.trim().is_empty() {
54            return Err(GoblinError::config_error("Script command cannot be empty"));
55        }
56
57        Ok(Self::new(config, path.clone()))
58    }
59
60    /// Load a script from a TOML string with a specified path
61    pub fn from_toml_str(toml_str: &str, path: PathBuf) -> Result<Self> {
62        let config: ScriptConfig = toml::from_str(toml_str)?;
63        
64        // Validate required fields
65        if config.name.trim().is_empty() {
66            return Err(GoblinError::config_error("Script name cannot be empty"));
67        }
68        if config.command.trim().is_empty() {
69            return Err(GoblinError::config_error("Script command cannot be empty"));
70        }
71
72        Ok(Self::new(config, path))
73    }
74
75    /// Get the working directory for this script
76    pub fn working_directory(&self) -> &PathBuf {
77        &self.path
78    }
79
80    /// Check if this script has a test command
81    pub fn has_test(&self) -> bool {
82        self.test_command.is_some() && !self.test_command.as_ref().unwrap().trim().is_empty()
83    }
84
85    /// Get the full command to execute for this script
86    pub fn get_command(&self, args: &[String]) -> String {
87        if args.is_empty() {
88            self.command.clone()
89        } else {
90            format!("{} {}", self.command, args.join(" "))
91        }
92    }
93
94    /// Get the test command if available
95    pub fn get_test_command(&self) -> Option<&str> {
96        self.test_command.as_deref().filter(|cmd| !cmd.trim().is_empty())
97    }
98
99    /// Validate the script configuration
100    pub fn validate(&self) -> Result<()> {
101        if self.name.trim().is_empty() {
102            return Err(GoblinError::config_error("Script name cannot be empty"));
103        }
104        
105        if self.command.trim().is_empty() {
106            return Err(GoblinError::config_error("Script command cannot be empty"));
107        }
108        
109        if !self.path.exists() {
110            return Err(GoblinError::config_error(format!(
111                "Script path does not exist: {}", 
112                self.path.display()
113            )));
114        }
115        
116        if !self.path.is_dir() {
117            return Err(GoblinError::config_error(format!(
118                "Script path is not a directory: {}", 
119                self.path.display()
120            )));
121        }
122
123        Ok(())
124    }
125}
126
127fn default_timeout() -> u64 {
128    500 // 500 seconds default timeout
129}
130
131impl From<ScriptConfig> for Script {
132    fn from(config: ScriptConfig) -> Self {
133        Self::new(config, PathBuf::new())
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_script_from_config() {
143        let config = ScriptConfig {
144            name: "test_script".to_string(),
145            command: "echo hello".to_string(),
146            timeout: 30,
147            test_command: Some("echo true".to_string()),
148            require_test: false,
149        };
150
151        let path = PathBuf::from("/tmp");
152        let script = Script::new(config, path.clone());
153
154        assert_eq!(script.name, "test_script");
155        assert_eq!(script.command, "echo hello");
156        assert_eq!(script.timeout, Duration::from_secs(30));
157        assert_eq!(script.test_command, Some("echo true".to_string()));
158        assert!(!script.require_test);
159        assert_eq!(script.path, path);
160    }
161
162    #[test]
163    fn test_script_from_toml_str() {
164        let toml_content = r#"
165            name = "test_script"
166            command = "deno run main.ts"
167            timeout = 100
168            test_command = "deno test"
169            require_test = true
170        "#;
171
172        let path = PathBuf::from("/tmp");
173        let script = Script::from_toml_str(toml_content, path).unwrap();
174
175        assert_eq!(script.name, "test_script");
176        assert_eq!(script.command, "deno run main.ts");
177        assert_eq!(script.timeout, Duration::from_secs(100));
178        assert_eq!(script.test_command, Some("deno test".to_string()));
179        assert!(script.require_test);
180    }
181
182    #[test]
183    fn test_script_default_values() {
184        let toml_content = r#"
185            name = "simple_script"
186            command = "echo hello"
187        "#;
188
189        let path = PathBuf::from("/tmp");
190        let script = Script::from_toml_str(toml_content, path).unwrap();
191
192        assert_eq!(script.timeout, Duration::from_secs(500)); // default timeout
193        assert_eq!(script.test_command, None);
194        assert!(!script.require_test); // default false
195    }
196
197    #[test]
198    fn test_script_get_command_with_args() {
199        let script = Script {
200            name: "test".to_string(),
201            command: "echo".to_string(),
202            timeout: Duration::from_secs(30),
203            test_command: None,
204            require_test: false,
205            path: PathBuf::new(),
206        };
207
208        assert_eq!(script.get_command(&[]), "echo");
209        assert_eq!(
210            script.get_command(&["hello".to_string(), "world".to_string()]),
211            "echo hello world"
212        );
213    }
214
215    #[test]
216    fn test_script_has_test() {
217        let script_with_test = Script {
218            name: "test".to_string(),
219            command: "echo".to_string(),
220            timeout: Duration::from_secs(30),
221            test_command: Some("test command".to_string()),
222            require_test: false,
223            path: PathBuf::new(),
224        };
225
226        let script_without_test = Script {
227            name: "test".to_string(),
228            command: "echo".to_string(),
229            timeout: Duration::from_secs(30),
230            test_command: None,
231            require_test: false,
232            path: PathBuf::new(),
233        };
234
235        let script_with_empty_test = Script {
236            name: "test".to_string(),
237            command: "echo".to_string(),
238            timeout: Duration::from_secs(30),
239            test_command: Some("".to_string()),
240            require_test: false,
241            path: PathBuf::new(),
242        };
243
244        assert!(script_with_test.has_test());
245        assert!(!script_without_test.has_test());
246        assert!(!script_with_empty_test.has_test());
247    }
248}