1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3use std::time::Duration;
4use crate::error::{GoblinError, Result};
5
6#[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, #[serde(default)]
14 pub test_command: Option<String>,
15 #[serde(default)]
16 pub require_test: bool,
17}
18
19#[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 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 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 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 pub fn from_toml_str(toml_str: &str, path: PathBuf) -> Result<Self> {
62 let config: ScriptConfig = toml::from_str(toml_str)?;
63
64 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 pub fn working_directory(&self) -> &PathBuf {
77 &self.path
78 }
79
80 pub fn has_test(&self) -> bool {
82 self.test_command.is_some() && !self.test_command.as_ref().unwrap().trim().is_empty()
83 }
84
85 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 pub fn get_test_command(&self) -> Option<&str> {
96 self.test_command.as_deref().filter(|cmd| !cmd.trim().is_empty())
97 }
98
99 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 }
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)); assert_eq!(script.test_command, None);
194 assert!(!script.require_test); }
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}