1use std::{error::Error, marker::PhantomData};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6 shared::{
7 errors::FieldError,
8 helpers::{ensure_value_is_not_empty, merge_errors},
9 macros::states,
10 node::EnvVar,
11 },
12 types::{Arg, Command, Image},
13};
14
15states! {
16 WithName,
17 WithOutName
18}
19
20states! {
21 WithCmd,
22 WithOutCmd
23}
24
25pub trait Cmd {}
26impl Cmd for WithOutCmd {}
27impl Cmd for WithCmd {}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct CustomProcess {
36 name: String,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 image: Option<Image>,
41 command: Command,
43 #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
45 args: Vec<Arg>,
46 #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
48 env: Vec<EnvVar>,
49}
50
51impl Default for CustomProcess {
52 fn default() -> Self {
53 Self {
54 name: "".into(),
55 image: None,
56 command: Command::default(), args: vec![],
58 env: vec![],
59 }
60 }
61}
62
63impl CustomProcess {
64 pub fn name(&self) -> &str {
66 &self.name
67 }
68
69 pub fn image(&self) -> Option<&Image> {
71 self.image.as_ref()
72 }
73
74 pub fn command(&self) -> &Command {
76 &self.command
77 }
78
79 pub fn args(&self) -> Vec<&Arg> {
81 self.args.iter().collect()
82 }
83
84 pub fn env(&self) -> Vec<&EnvVar> {
86 self.env.iter().collect()
87 }
88}
89
90pub struct CustomProcessBuilder<N, C> {
92 config: CustomProcess,
93 errors: Vec<anyhow::Error>,
94 _state_name: PhantomData<N>,
95 _state_cmd: PhantomData<C>,
96}
97
98impl Default for CustomProcessBuilder<WithOutName, WithOutCmd> {
99 fn default() -> Self {
100 Self {
101 config: CustomProcess::default(),
102 errors: vec![],
103 _state_name: PhantomData,
104 _state_cmd: PhantomData,
105 }
106 }
107}
108
109impl<A, B> CustomProcessBuilder<A, B> {
110 fn transition<C, D>(
111 config: CustomProcess,
112 errors: Vec<anyhow::Error>,
113 ) -> CustomProcessBuilder<C, D> {
114 CustomProcessBuilder {
115 config,
116 errors,
117 _state_name: PhantomData,
118 _state_cmd: PhantomData,
119 }
120 }
121}
122
123impl CustomProcessBuilder<WithOutName, WithOutCmd> {
124 pub fn new() -> CustomProcessBuilder<WithOutName, WithOutCmd> {
125 CustomProcessBuilder::default()
126 }
127}
128
129impl<C: Cmd> CustomProcessBuilder<WithOutName, C> {
130 pub fn with_name<T: Into<String> + Copy>(self, name: T) -> CustomProcessBuilder<WithName, C> {
132 let name: String = name.into();
133
134 match ensure_value_is_not_empty(&name) {
135 Ok(_) => Self::transition(
136 CustomProcess {
137 name,
138 ..self.config
139 },
140 self.errors,
141 ),
142 Err(e) => Self::transition(
143 CustomProcess {
144 name,
146 ..self.config
147 },
148 merge_errors(self.errors, FieldError::Name(e).into()),
149 ),
150 }
151 }
152}
153
154impl CustomProcessBuilder<WithName, WithOutCmd> {
155 pub fn with_command<T>(self, command: T) -> CustomProcessBuilder<WithName, WithCmd>
157 where
158 T: TryInto<Command>,
159 T::Error: Error + Send + Sync + 'static,
160 {
161 match command.try_into() {
162 Ok(command) => Self::transition(
163 CustomProcess {
164 command,
165 ..self.config
166 },
167 self.errors,
168 ),
169 Err(error) => Self::transition(
170 self.config,
171 merge_errors(self.errors, FieldError::Command(error.into()).into()),
172 ),
173 }
174 }
175}
176
177impl CustomProcessBuilder<WithName, WithCmd> {
178 pub fn with_image<T>(self, image: T) -> Self
180 where
181 T: TryInto<Image>,
182 T::Error: Error + Send + Sync + 'static,
183 {
184 match image.try_into() {
185 Ok(image) => Self::transition(
186 CustomProcess {
187 image: Some(image),
188 ..self.config
189 },
190 self.errors,
191 ),
192 Err(error) => Self::transition(
193 self.config,
194 merge_errors(self.errors, FieldError::Image(error.into()).into()),
195 ),
196 }
197 }
198
199 pub fn with_args(self, args: Vec<Arg>) -> Self {
201 Self::transition(
202 CustomProcess {
203 args,
204 ..self.config
205 },
206 self.errors,
207 )
208 }
209
210 pub fn with_env(self, env: Vec<impl Into<EnvVar>>) -> Self {
212 let env = env.into_iter().map(|var| var.into()).collect::<Vec<_>>();
213
214 Self::transition(CustomProcess { env, ..self.config }, self.errors)
215 }
216
217 pub fn build(self) -> Result<CustomProcess, (String, Vec<anyhow::Error>)> {
219 if !self.errors.is_empty() {
220 return Err((self.config.name.clone(), self.errors));
221 }
222
223 Ok(self.config)
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn custom_process_config_builder_should_succeeds_and_returns_a_custom_process_config() {
233 let cpb = CustomProcessBuilder::new()
234 .with_name("demo")
235 .with_command("some")
236 .with_args(vec![("--port", "100").into(), "--custom-flag".into()])
237 .build()
238 .unwrap();
239
240 assert_eq!(cpb.command().as_str(), "some");
241 let args: Vec<Arg> = vec![("--port", "100").into(), "--custom-flag".into()];
242 assert_eq!(cpb.args(), args.iter().collect::<Vec<&Arg>>());
243 }
244}