1use crate::newtypes::libcnb_newtype;
2use serde::{Deserialize, Serialize, Serializer};
3use std::path::PathBuf;
4
5#[derive(Deserialize, Serialize, Clone, Debug, Default)]
7#[serde(deny_unknown_fields)]
8pub struct Launch {
9 #[serde(default, skip_serializing_if = "Vec::is_empty")]
10 pub labels: Vec<Label>,
11 #[serde(default, skip_serializing_if = "Vec::is_empty")]
12 pub processes: Vec<Process>,
13 #[serde(default, skip_serializing_if = "Vec::is_empty")]
14 pub slices: Vec<Slice>,
15}
16
17#[derive(Default)]
35pub struct LaunchBuilder {
36 launch: Launch,
37}
38
39impl LaunchBuilder {
40 #[must_use]
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn process<P: Into<Process>>(&mut self, process: P) -> &mut Self {
47 self.launch.processes.push(process.into());
48 self
49 }
50
51 pub fn processes<I: IntoIterator<Item = P>, P: Into<Process>>(
53 &mut self,
54 processes: I,
55 ) -> &mut Self {
56 for process in processes {
57 self.process(process);
58 }
59
60 self
61 }
62
63 pub fn label<L: Into<Label>>(&mut self, label: L) -> &mut Self {
65 self.launch.labels.push(label.into());
66 self
67 }
68
69 pub fn labels<I: IntoIterator<Item = L>, L: Into<Label>>(&mut self, labels: I) -> &mut Self {
71 for label in labels {
72 self.label(label);
73 }
74
75 self
76 }
77
78 pub fn slice<S: Into<Slice>>(&mut self, slice: S) -> &mut Self {
80 self.launch.slices.push(slice.into());
81 self
82 }
83
84 pub fn slices<I: IntoIterator<Item = S>, S: Into<Slice>>(&mut self, slices: I) -> &mut Self {
86 for slice in slices {
87 self.slice(slice);
88 }
89
90 self
91 }
92
93 #[must_use]
95 pub fn build(&self) -> Launch {
96 self.launch.clone()
97 }
98}
99
100#[derive(Deserialize, Serialize, Clone, Debug)]
101#[serde(deny_unknown_fields)]
102pub struct Label {
103 pub key: String,
104 pub value: String,
105}
106
107#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
108#[serde(deny_unknown_fields)]
109pub struct Process {
110 pub r#type: ProcessType,
111 pub command: Vec<String>,
112 #[serde(default, skip_serializing_if = "Vec::is_empty")]
113 pub args: Vec<String>,
114 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
115 pub default: bool,
116 #[serde(
117 rename = "working-dir",
118 default,
119 skip_serializing_if = "WorkingDirectory::is_app"
120 )]
121 pub working_directory: WorkingDirectory,
122}
123
124#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Default)]
125#[serde(untagged)]
126pub enum WorkingDirectory {
127 #[default]
134 App,
135 Directory(PathBuf),
136}
137
138impl WorkingDirectory {
139 #[must_use]
140 pub fn is_app(&self) -> bool {
141 matches!(self, Self::App)
142 }
143}
144
145impl Serialize for WorkingDirectory {
150 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
151 where
152 S: Serializer,
153 {
154 match self {
155 Self::App => serializer.serialize_str("."),
156 Self::Directory(path) => path.serialize(serializer),
157 }
158 }
159}
160
161pub struct ProcessBuilder {
162 process: Process,
163}
164
165impl ProcessBuilder {
178 pub fn new(r#type: ProcessType, command: impl IntoIterator<Item = impl Into<String>>) -> Self {
184 Self {
185 process: Process {
186 r#type,
187 command: command.into_iter().map(Into::into).collect(),
188 args: Vec::new(),
189 default: false,
190 working_directory: WorkingDirectory::App,
191 },
192 }
193 }
194
195 pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
217 self.process.args.push(arg.into());
218 self
219 }
220
221 pub fn args(&mut self, args: impl IntoIterator<Item = impl Into<String>>) -> &mut Self {
225 for arg in args {
226 self.arg(arg);
227 }
228
229 self
230 }
231
232 pub fn default(&mut self, value: bool) -> &mut Self {
237 self.process.default = value;
238 self
239 }
240
241 pub fn working_directory(&mut self, value: WorkingDirectory) -> &mut Self {
243 self.process.working_directory = value;
244 self
245 }
246
247 #[must_use]
249 pub fn build(&self) -> Process {
250 self.process.clone()
251 }
252}
253
254#[derive(Deserialize, Serialize, Clone, Debug)]
255#[serde(deny_unknown_fields)]
256pub struct Slice {
257 #[serde(rename = "paths")]
262 pub path_globs: Vec<String>,
263}
264
265libcnb_newtype!(
266 launch,
267 process_type,
279 ProcessType,
303 ProcessTypeError,
304 r"^[[:alnum:]._-]+$"
305);
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use serde_test::{Token, assert_ser_tokens};
311
312 #[test]
313 fn launch_builder_add_processes() {
314 let launch = LaunchBuilder::new()
315 .process(ProcessBuilder::new(process_type!("web"), ["web_command"]).build())
316 .processes([
317 ProcessBuilder::new(process_type!("another"), ["another_command"]).build(),
318 ProcessBuilder::new(process_type!("worker"), ["worker_command"]).build(),
319 ])
320 .build();
321
322 assert_eq!(
323 launch.processes,
324 [
325 ProcessBuilder::new(process_type!("web"), ["web_command"]).build(),
326 ProcessBuilder::new(process_type!("another"), ["another_command"]).build(),
327 ProcessBuilder::new(process_type!("worker"), ["worker_command"]).build(),
328 ]
329 );
330 }
331
332 #[test]
333 fn process_type_validation_valid() {
334 assert!("web".parse::<ProcessType>().is_ok());
335 assert!("Abc123._-".parse::<ProcessType>().is_ok());
336 }
337
338 #[test]
339 fn process_type_validation_invalid() {
340 assert_eq!(
341 "worker/foo".parse::<ProcessType>(),
342 Err(ProcessTypeError::InvalidValue(String::from("worker/foo")))
343 );
344 assert_eq!(
345 "worker:foo".parse::<ProcessType>(),
346 Err(ProcessTypeError::InvalidValue(String::from("worker:foo")))
347 );
348 assert_eq!(
349 "worker foo".parse::<ProcessType>(),
350 Err(ProcessTypeError::InvalidValue(String::from("worker foo")))
351 );
352 assert_eq!(
353 "".parse::<ProcessType>(),
354 Err(ProcessTypeError::InvalidValue(String::new()))
355 );
356 }
357
358 #[test]
359 fn process_with_default_values_deserialization() {
360 let toml_str = r#"
361type = "web"
362command = ["foo"]
363"#;
364
365 assert_eq!(
366 toml::from_str::<Process>(toml_str),
367 Ok(Process {
368 r#type: process_type!("web"),
369 command: vec![String::from("foo")],
370 args: Vec::new(),
371 default: false,
372 working_directory: WorkingDirectory::App
373 })
374 );
375 }
376
377 #[test]
378 fn process_with_default_values_serialization() {
379 let process = ProcessBuilder::new(process_type!("web"), ["foo"]).build();
380
381 let string = toml::to_string(&process).unwrap();
382 assert_eq!(
383 string,
384 r#"type = "web"
385command = ["foo"]
386"#
387 );
388 }
389
390 #[test]
391 fn process_with_some_default_values_serialization() {
392 let process = ProcessBuilder::new(process_type!("web"), ["foo"])
393 .default(true)
394 .working_directory(WorkingDirectory::Directory(PathBuf::from("dist")))
395 .build();
396
397 let string = toml::to_string(&process).unwrap();
398 assert_eq!(
399 string,
400 r#"type = "web"
401command = ["foo"]
402default = true
403working-dir = "dist"
404"#
405 );
406 }
407
408 #[test]
409 fn process_builder() {
410 let mut process_builder = ProcessBuilder::new(process_type!("web"), ["java"]);
411
412 assert_eq!(
413 process_builder.build(),
414 Process {
415 r#type: process_type!("web"),
416 command: vec![String::from("java")],
417 args: Vec::new(),
418 default: false,
419 working_directory: WorkingDirectory::App
420 }
421 );
422
423 process_builder.default(true);
424
425 assert_eq!(
426 process_builder.build(),
427 Process {
428 r#type: process_type!("web"),
429 command: vec![String::from("java")],
430 args: Vec::new(),
431 default: true,
432 working_directory: WorkingDirectory::App
433 }
434 );
435
436 process_builder.working_directory(WorkingDirectory::Directory(PathBuf::from("dist")));
437
438 assert_eq!(
439 process_builder.build(),
440 Process {
441 r#type: process_type!("web"),
442 command: vec![String::from("java")],
443 args: Vec::new(),
444 default: true,
445 working_directory: WorkingDirectory::Directory(PathBuf::from("dist"))
446 }
447 );
448 }
449
450 #[test]
451 fn process_builder_args() {
452 assert_eq!(
453 ProcessBuilder::new(process_type!("web"), ["java"])
454 .arg("foo")
455 .args(["baz", "eggs"])
456 .arg("bar")
457 .build(),
458 Process {
459 r#type: process_type!("web"),
460 command: vec![String::from("java")],
461 args: vec![
462 String::from("foo"),
463 String::from("baz"),
464 String::from("eggs"),
465 String::from("bar"),
466 ],
467 default: false,
468 working_directory: WorkingDirectory::App
469 }
470 );
471 }
472
473 #[test]
474 fn process_working_directory_serialization() {
475 assert_ser_tokens(&WorkingDirectory::App, &[Token::BorrowedStr(".")]);
476
477 assert_ser_tokens(
478 &WorkingDirectory::Directory(PathBuf::from("/")),
479 &[Token::BorrowedStr("/")],
480 );
481 assert_ser_tokens(
482 &WorkingDirectory::Directory(PathBuf::from("/foo/bar")),
483 &[Token::BorrowedStr("/foo/bar")],
484 );
485 assert_ser_tokens(
486 &WorkingDirectory::Directory(PathBuf::from("relative/foo/bar")),
487 &[Token::BorrowedStr("relative/foo/bar")],
488 );
489 }
490}