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<()> {
71 match self {
72 TaskKind::Subprocess(spec) => spec.mode.validate(),
73 TaskKind::Wasm(spec) => spec.validate(),
74 TaskKind::Container(spec) => spec.validate(),
75 TaskKind::Embedded => Ok(()),
76 }
77 }
78}
79
80impl WasmSpec {
81 pub fn validate(&self) -> crate::error::ModelResult<()> {
83 if self.module.as_os_str().is_empty() {
84 return Err(crate::error::ModelError::Invalid(
85 "wasm module path cannot be empty".into(),
86 ));
87 }
88 Ok(())
89 }
90}
91
92impl ContainerSpec {
93 pub fn validate(&self) -> crate::error::ModelResult<()> {
95 if self.image.trim().is_empty() {
96 return Err(crate::error::ModelError::Invalid(
97 "container image cannot be empty".into(),
98 ));
99 }
100 Ok(())
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use std::path::PathBuf;
108
109 #[test]
110 fn task_kind_validate_rejects_empty_container_image() {
111 let kind = TaskKind::Container(ContainerSpec {
112 image: "".into(),
113 command: None,
114 args: vec![],
115 env: Default::default(),
116 });
117 let err = kind.validate().unwrap_err();
118 assert!(err.to_string().contains("container image"));
119 }
120
121 #[test]
122 fn task_kind_validate_rejects_whitespace_container_image() {
123 let kind = TaskKind::Container(ContainerSpec {
124 image: " \t".into(),
125 command: None,
126 args: vec![],
127 env: Default::default(),
128 });
129 assert!(kind.validate().is_err());
130 }
131
132 #[test]
133 fn task_kind_validate_rejects_empty_wasm_module() {
134 let kind = TaskKind::Wasm(WasmSpec {
135 module: PathBuf::new(),
136 args: vec![],
137 env: Default::default(),
138 });
139 let err = kind.validate().unwrap_err();
140 assert!(err.to_string().contains("wasm module"));
141 }
142
143 #[test]
144 fn task_kind_validate_accepts_valid_container() {
145 let kind = TaskKind::Container(ContainerSpec {
146 image: "nginx:latest".into(),
147 command: None,
148 args: vec![],
149 env: Default::default(),
150 });
151 assert!(kind.validate().is_ok());
152 }
153
154 #[test]
155 fn task_kind_validate_accepts_embedded() {
156 assert!(TaskKind::Embedded.validate().is_ok());
157 }
158}
159
160#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct SubprocessSpec {
170 pub mode: SubprocessMode,
172 #[serde(default, skip_serializing_if = "TaskEnv::is_empty")]
174 pub env: TaskEnv,
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub cwd: Option<PathBuf>,
178 #[serde(default)]
182 pub fail_on_non_zero: Flag,
183}
184
185#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct WasmSpec {
189 pub module: PathBuf,
191 #[serde(default, skip_serializing_if = "Vec::is_empty")]
193 pub args: Vec<String>,
194 #[serde(default, skip_serializing_if = "TaskEnv::is_empty")]
196 pub env: TaskEnv,
197}
198
199#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct ContainerSpec {
203 pub image: String,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub command: Option<Vec<String>>,
208 #[serde(default, skip_serializing_if = "Vec::is_empty")]
210 pub args: Vec<String>,
211 #[serde(default, skip_serializing_if = "TaskEnv::is_empty")]
213 pub env: TaskEnv,
214}