use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;
use crate::error::{GoblinError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptConfig {
pub name: String,
pub command: String,
#[serde(default = "default_timeout")]
pub timeout: u64, #[serde(default)]
pub test_command: Option<String>,
#[serde(default)]
pub require_test: bool,
}
#[derive(Debug, Clone)]
pub struct Script {
pub name: String,
pub command: String,
pub timeout: Duration,
pub test_command: Option<String>,
pub require_test: bool,
pub path: PathBuf,
}
impl Script {
pub fn new(config: ScriptConfig, path: PathBuf) -> Self {
Self {
name: config.name,
command: config.command,
timeout: Duration::from_secs(config.timeout),
test_command: config.test_command,
require_test: config.require_test,
path,
}
}
pub fn from_toml_file(path: &PathBuf) -> Result<Self> {
let toml_path = path.join("goblin.toml");
let content = std::fs::read_to_string(&toml_path)?;
let config: ScriptConfig = toml::from_str(&content)?;
if config.name.trim().is_empty() {
return Err(GoblinError::config_error("Script name cannot be empty"));
}
if config.command.trim().is_empty() {
return Err(GoblinError::config_error("Script command cannot be empty"));
}
Ok(Self::new(config, path.clone()))
}
pub fn from_toml_str(toml_str: &str, path: PathBuf) -> Result<Self> {
let config: ScriptConfig = toml::from_str(toml_str)?;
if config.name.trim().is_empty() {
return Err(GoblinError::config_error("Script name cannot be empty"));
}
if config.command.trim().is_empty() {
return Err(GoblinError::config_error("Script command cannot be empty"));
}
Ok(Self::new(config, path))
}
pub fn working_directory(&self) -> &PathBuf {
&self.path
}
pub fn has_test(&self) -> bool {
self.test_command.is_some() && !self.test_command.as_ref().unwrap().trim().is_empty()
}
pub fn get_command(&self, args: &[String]) -> String {
if args.is_empty() {
self.command.clone()
} else {
format!("{} {}", self.command, args.join(" "))
}
}
pub fn get_test_command(&self) -> Option<&str> {
self.test_command.as_deref().filter(|cmd| !cmd.trim().is_empty())
}
pub fn validate(&self) -> Result<()> {
if self.name.trim().is_empty() {
return Err(GoblinError::config_error("Script name cannot be empty"));
}
if self.command.trim().is_empty() {
return Err(GoblinError::config_error("Script command cannot be empty"));
}
if !self.path.exists() {
return Err(GoblinError::config_error(format!(
"Script path does not exist: {}",
self.path.display()
)));
}
if !self.path.is_dir() {
return Err(GoblinError::config_error(format!(
"Script path is not a directory: {}",
self.path.display()
)));
}
Ok(())
}
}
fn default_timeout() -> u64 {
500 }
impl From<ScriptConfig> for Script {
fn from(config: ScriptConfig) -> Self {
Self::new(config, PathBuf::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_script_from_config() {
let config = ScriptConfig {
name: "test_script".to_string(),
command: "echo hello".to_string(),
timeout: 30,
test_command: Some("echo true".to_string()),
require_test: false,
};
let path = PathBuf::from("/tmp");
let script = Script::new(config, path.clone());
assert_eq!(script.name, "test_script");
assert_eq!(script.command, "echo hello");
assert_eq!(script.timeout, Duration::from_secs(30));
assert_eq!(script.test_command, Some("echo true".to_string()));
assert!(!script.require_test);
assert_eq!(script.path, path);
}
#[test]
fn test_script_from_toml_str() {
let toml_content = r#"
name = "test_script"
command = "deno run main.ts"
timeout = 100
test_command = "deno test"
require_test = true
"#;
let path = PathBuf::from("/tmp");
let script = Script::from_toml_str(toml_content, path).unwrap();
assert_eq!(script.name, "test_script");
assert_eq!(script.command, "deno run main.ts");
assert_eq!(script.timeout, Duration::from_secs(100));
assert_eq!(script.test_command, Some("deno test".to_string()));
assert!(script.require_test);
}
#[test]
fn test_script_default_values() {
let toml_content = r#"
name = "simple_script"
command = "echo hello"
"#;
let path = PathBuf::from("/tmp");
let script = Script::from_toml_str(toml_content, path).unwrap();
assert_eq!(script.timeout, Duration::from_secs(500)); assert_eq!(script.test_command, None);
assert!(!script.require_test); }
#[test]
fn test_script_get_command_with_args() {
let script = Script {
name: "test".to_string(),
command: "echo".to_string(),
timeout: Duration::from_secs(30),
test_command: None,
require_test: false,
path: PathBuf::new(),
};
assert_eq!(script.get_command(&[]), "echo");
assert_eq!(
script.get_command(&["hello".to_string(), "world".to_string()]),
"echo hello world"
);
}
#[test]
fn test_script_has_test() {
let script_with_test = Script {
name: "test".to_string(),
command: "echo".to_string(),
timeout: Duration::from_secs(30),
test_command: Some("test command".to_string()),
require_test: false,
path: PathBuf::new(),
};
let script_without_test = Script {
name: "test".to_string(),
command: "echo".to_string(),
timeout: Duration::from_secs(30),
test_command: None,
require_test: false,
path: PathBuf::new(),
};
let script_with_empty_test = Script {
name: "test".to_string(),
command: "echo".to_string(),
timeout: Duration::from_secs(30),
test_command: Some("".to_string()),
require_test: false,
path: PathBuf::new(),
};
assert!(script_with_test.has_test());
assert!(!script_without_test.has_test());
assert!(!script_with_empty_test.has_test());
}
}