use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::{Flag, SubprocessMode, TaskEnv};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum TaskKind {
Subprocess(SubprocessSpec),
Wasm(WasmSpec),
Container(ContainerSpec),
Embedded,
}
impl TaskKind {
#[inline]
pub fn kind(&self) -> &'static str {
match self {
TaskKind::Subprocess(_) => "subprocess",
TaskKind::Container(_) => "container",
TaskKind::Embedded => "embedded",
TaskKind::Wasm(_) => "wasm",
}
}
pub fn validate(&self) -> crate::error::ModelResult<()> {
match self {
TaskKind::Subprocess(spec) => spec.mode.validate(),
TaskKind::Wasm(spec) => spec.validate(),
TaskKind::Container(spec) => spec.validate(),
TaskKind::Embedded => Ok(()),
}
}
}
impl WasmSpec {
pub fn validate(&self) -> crate::error::ModelResult<()> {
if self.module.as_os_str().is_empty() {
return Err(crate::error::ModelError::Invalid(
"wasm module path cannot be empty".into(),
));
}
Ok(())
}
}
impl ContainerSpec {
pub fn validate(&self) -> crate::error::ModelResult<()> {
if self.image.trim().is_empty() {
return Err(crate::error::ModelError::Invalid(
"container image cannot be empty".into(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn task_kind_validate_rejects_empty_container_image() {
let kind = TaskKind::Container(ContainerSpec {
image: "".into(),
command: None,
args: vec![],
env: Default::default(),
});
let err = kind.validate().unwrap_err();
assert!(err.to_string().contains("container image"));
}
#[test]
fn task_kind_validate_rejects_whitespace_container_image() {
let kind = TaskKind::Container(ContainerSpec {
image: " \t".into(),
command: None,
args: vec![],
env: Default::default(),
});
assert!(kind.validate().is_err());
}
#[test]
fn task_kind_validate_rejects_empty_wasm_module() {
let kind = TaskKind::Wasm(WasmSpec {
module: PathBuf::new(),
args: vec![],
env: Default::default(),
});
let err = kind.validate().unwrap_err();
assert!(err.to_string().contains("wasm module"));
}
#[test]
fn task_kind_validate_accepts_valid_container() {
let kind = TaskKind::Container(ContainerSpec {
image: "nginx:latest".into(),
command: None,
args: vec![],
env: Default::default(),
});
assert!(kind.validate().is_ok());
}
#[test]
fn task_kind_validate_accepts_embedded() {
assert!(TaskKind::Embedded.validate().is_ok());
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubprocessSpec {
pub mode: SubprocessMode,
#[serde(default, skip_serializing_if = "TaskEnv::is_empty")]
pub env: TaskEnv,
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<PathBuf>,
#[serde(default)]
pub fail_on_non_zero: Flag,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WasmSpec {
pub module: PathBuf,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "TaskEnv::is_empty")]
pub env: TaskEnv,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContainerSpec {
pub image: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "TaskEnv::is_empty")]
pub env: TaskEnv,
}