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)]
125#[serde(untagged)]
126pub enum WorkingDirectory {
127 App,
134 Directory(PathBuf),
135}
136
137impl WorkingDirectory {
138 #[must_use]
139 pub fn is_app(&self) -> bool {
140 matches!(self, Self::App)
141 }
142}
143
144impl Serialize for WorkingDirectory {
149 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
150 where
151 S: Serializer,
152 {
153 match self {
154 Self::App => serializer.serialize_str("."),
155 Self::Directory(path) => path.serialize(serializer),
156 }
157 }
158}
159
160impl Default for WorkingDirectory {
161 fn default() -> Self {
162 Self::App
163 }
164}
165
166pub struct ProcessBuilder {
167 process: Process,
168}
169
170impl ProcessBuilder {
183 pub fn new(r#type: ProcessType, command: impl IntoIterator<Item = impl Into<String>>) -> Self {
189 Self {
190 process: Process {
191 r#type,
192 command: command.into_iter().map(Into::into).collect(),
193 args: Vec::new(),
194 default: false,
195 working_directory: WorkingDirectory::App,
196 },
197 }
198 }
199
200 pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
222 self.process.args.push(arg.into());
223 self
224 }
225
226 pub fn args(&mut self, args: impl IntoIterator<Item = impl Into<String>>) -> &mut Self {
230 for arg in args {
231 self.arg(arg);
232 }
233
234 self
235 }
236
237 pub fn default(&mut self, value: bool) -> &mut Self {
242 self.process.default = value;
243 self
244 }
245
246 pub fn working_directory(&mut self, value: WorkingDirectory) -> &mut Self {
248 self.process.working_directory = value;
249 self
250 }
251
252 #[must_use]
254 pub fn build(&self) -> Process {
255 self.process.clone()
256 }
257}
258
259#[derive(Deserialize, Serialize, Clone, Debug)]
260#[serde(deny_unknown_fields)]
261pub struct Slice {
262 #[serde(rename = "paths")]
267 pub path_globs: Vec<String>,
268}
269
270libcnb_newtype!(
271 launch,
272 process_type,
284 ProcessType,
308 ProcessTypeError,
309 r"^[[:alnum:]._-]+$"
310);
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use serde_test::{Token, assert_ser_tokens};
316
317 #[test]
318 fn launch_builder_add_processes() {
319 let launch = LaunchBuilder::new()
320 .process(ProcessBuilder::new(process_type!("web"), ["web_command"]).build())
321 .processes([
322 ProcessBuilder::new(process_type!("another"), ["another_command"]).build(),
323 ProcessBuilder::new(process_type!("worker"), ["worker_command"]).build(),
324 ])
325 .build();
326
327 assert_eq!(
328 launch.processes,
329 [
330 ProcessBuilder::new(process_type!("web"), ["web_command"]).build(),
331 ProcessBuilder::new(process_type!("another"), ["another_command"]).build(),
332 ProcessBuilder::new(process_type!("worker"), ["worker_command"]).build(),
333 ]
334 );
335 }
336
337 #[test]
338 fn process_type_validation_valid() {
339 assert!("web".parse::<ProcessType>().is_ok());
340 assert!("Abc123._-".parse::<ProcessType>().is_ok());
341 }
342
343 #[test]
344 fn process_type_validation_invalid() {
345 assert_eq!(
346 "worker/foo".parse::<ProcessType>(),
347 Err(ProcessTypeError::InvalidValue(String::from("worker/foo")))
348 );
349 assert_eq!(
350 "worker:foo".parse::<ProcessType>(),
351 Err(ProcessTypeError::InvalidValue(String::from("worker:foo")))
352 );
353 assert_eq!(
354 "worker foo".parse::<ProcessType>(),
355 Err(ProcessTypeError::InvalidValue(String::from("worker foo")))
356 );
357 assert_eq!(
358 "".parse::<ProcessType>(),
359 Err(ProcessTypeError::InvalidValue(String::new()))
360 );
361 }
362
363 #[test]
364 fn process_with_default_values_deserialization() {
365 let toml_str = r#"
366type = "web"
367command = ["foo"]
368"#;
369
370 assert_eq!(
371 toml::from_str::<Process>(toml_str),
372 Ok(Process {
373 r#type: process_type!("web"),
374 command: vec![String::from("foo")],
375 args: Vec::new(),
376 default: false,
377 working_directory: WorkingDirectory::App
378 })
379 );
380 }
381
382 #[test]
383 fn process_with_default_values_serialization() {
384 let process = ProcessBuilder::new(process_type!("web"), ["foo"]).build();
385
386 let string = toml::to_string(&process).unwrap();
387 assert_eq!(
388 string,
389 r#"type = "web"
390command = ["foo"]
391"#
392 );
393 }
394
395 #[test]
396 fn process_with_some_default_values_serialization() {
397 let process = ProcessBuilder::new(process_type!("web"), ["foo"])
398 .default(true)
399 .working_directory(WorkingDirectory::Directory(PathBuf::from("dist")))
400 .build();
401
402 let string = toml::to_string(&process).unwrap();
403 assert_eq!(
404 string,
405 r#"type = "web"
406command = ["foo"]
407default = true
408working-dir = "dist"
409"#
410 );
411 }
412
413 #[test]
414 fn process_builder() {
415 let mut process_builder = ProcessBuilder::new(process_type!("web"), ["java"]);
416
417 assert_eq!(
418 process_builder.build(),
419 Process {
420 r#type: process_type!("web"),
421 command: vec![String::from("java")],
422 args: Vec::new(),
423 default: false,
424 working_directory: WorkingDirectory::App
425 }
426 );
427
428 process_builder.default(true);
429
430 assert_eq!(
431 process_builder.build(),
432 Process {
433 r#type: process_type!("web"),
434 command: vec![String::from("java")],
435 args: Vec::new(),
436 default: true,
437 working_directory: WorkingDirectory::App
438 }
439 );
440
441 process_builder.working_directory(WorkingDirectory::Directory(PathBuf::from("dist")));
442
443 assert_eq!(
444 process_builder.build(),
445 Process {
446 r#type: process_type!("web"),
447 command: vec![String::from("java")],
448 args: Vec::new(),
449 default: true,
450 working_directory: WorkingDirectory::Directory(PathBuf::from("dist"))
451 }
452 );
453 }
454
455 #[test]
456 fn process_builder_args() {
457 assert_eq!(
458 ProcessBuilder::new(process_type!("web"), ["java"])
459 .arg("foo")
460 .args(["baz", "eggs"])
461 .arg("bar")
462 .build(),
463 Process {
464 r#type: process_type!("web"),
465 command: vec![String::from("java")],
466 args: vec![
467 String::from("foo"),
468 String::from("baz"),
469 String::from("eggs"),
470 String::from("bar"),
471 ],
472 default: false,
473 working_directory: WorkingDirectory::App
474 }
475 );
476 }
477
478 #[test]
479 fn process_working_directory_serialization() {
480 assert_ser_tokens(&WorkingDirectory::App, &[Token::BorrowedStr(".")]);
481
482 assert_ser_tokens(
483 &WorkingDirectory::Directory(PathBuf::from("/")),
484 &[Token::BorrowedStr("/")],
485 );
486 assert_ser_tokens(
487 &WorkingDirectory::Directory(PathBuf::from("/foo/bar")),
488 &[Token::BorrowedStr("/foo/bar")],
489 );
490 assert_ser_tokens(
491 &WorkingDirectory::Directory(PathBuf::from("relative/foo/bar")),
492 &[Token::BorrowedStr("relative/foo/bar")],
493 );
494 }
495}