solti_model/domain/kind/
task.rs1use std::path::PathBuf;
6
7use serde::{Deserialize, Serialize};
8
9use crate::{Flag, SubprocessMode, TaskEnv};
10
11#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28#[non_exhaustive]
29pub enum TaskKind {
30 Subprocess(SubprocessSpec),
32
33 Wasm(WasmSpec),
35
36 Container(ContainerSpec),
38
39 Embedded,
44}
45
46impl TaskKind {
47 #[inline]
55 pub fn kind(&self) -> &'static str {
56 match self {
57 TaskKind::Subprocess(_) => "subprocess",
58 TaskKind::Container(_) => "container",
59 TaskKind::Embedded => "embedded",
60 TaskKind::Wasm(_) => "wasm",
61 }
62 }
63
64 pub fn validate(&self) -> crate::error::ModelResult<()> {
66 match self {
67 TaskKind::Subprocess(spec) => spec.mode.validate(),
68 TaskKind::Wasm(spec) => spec.validate(),
69 TaskKind::Container(spec) => spec.validate(),
70 TaskKind::Embedded => Ok(()),
71 }
72 }
73}
74
75impl WasmSpec {
76 pub fn validate(&self) -> crate::error::ModelResult<()> {
78 if self.module.as_os_str().is_empty() {
79 return Err(crate::error::ModelError::Invalid(
80 "wasm module path cannot be empty".into(),
81 ));
82 }
83 Ok(())
84 }
85}
86
87impl ContainerSpec {
88 pub fn validate(&self) -> crate::error::ModelResult<()> {
90 if self.image.trim().is_empty() {
91 return Err(crate::error::ModelError::Invalid(
92 "container image cannot be empty".into(),
93 ));
94 }
95 Ok(())
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use std::path::PathBuf;
103
104 #[test]
105 fn task_kind_validate_rejects_empty_container_image() {
106 let kind = TaskKind::Container(ContainerSpec {
107 image: "".into(),
108 command: None,
109 args: vec![],
110 env: Default::default(),
111 });
112 let err = kind.validate().unwrap_err();
113 assert!(err.to_string().contains("container image"));
114 }
115
116 #[test]
117 fn task_kind_validate_rejects_whitespace_container_image() {
118 let kind = TaskKind::Container(ContainerSpec {
119 image: " \t".into(),
120 command: None,
121 args: vec![],
122 env: Default::default(),
123 });
124 assert!(kind.validate().is_err());
125 }
126
127 #[test]
128 fn task_kind_validate_rejects_empty_wasm_module() {
129 let kind = TaskKind::Wasm(WasmSpec {
130 module: PathBuf::new(),
131 args: vec![],
132 env: Default::default(),
133 });
134 let err = kind.validate().unwrap_err();
135 assert!(err.to_string().contains("wasm module"));
136 }
137
138 #[test]
139 fn task_kind_validate_accepts_valid_container() {
140 let kind = TaskKind::Container(ContainerSpec {
141 image: "nginx:latest".into(),
142 command: None,
143 args: vec![],
144 env: Default::default(),
145 });
146 assert!(kind.validate().is_ok());
147 }
148
149 #[test]
150 fn task_kind_validate_accepts_embedded() {
151 assert!(TaskKind::Embedded.validate().is_ok());
152 }
153}
154
155#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct SubprocessSpec {
165 pub mode: SubprocessMode,
167 #[serde(default, skip_serializing_if = "TaskEnv::is_empty")]
169 pub env: TaskEnv,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub cwd: Option<PathBuf>,
173 #[serde(default)]
177 pub fail_on_non_zero: Flag,
178}
179
180#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct WasmSpec {
184 pub module: PathBuf,
186 #[serde(default, skip_serializing_if = "Vec::is_empty")]
188 pub args: Vec<String>,
189 #[serde(default, skip_serializing_if = "TaskEnv::is_empty")]
191 pub env: TaskEnv,
192}
193
194#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct ContainerSpec {
198 pub image: String,
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub command: Option<Vec<String>>,
203 #[serde(default, skip_serializing_if = "Vec::is_empty")]
205 pub args: Vec<String>,
206 #[serde(default, skip_serializing_if = "TaskEnv::is_empty")]
208 pub env: TaskEnv,
209}