libcnb_data/
launch.rs

1use crate::newtypes::libcnb_newtype;
2use serde::{Deserialize, Serialize, Serializer};
3use std::path::PathBuf;
4
5/// Data Structure for the launch.toml file.
6#[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/// A non-consuming builder for [`Launch`] values.
18///
19/// # Examples
20/// ```
21/// use libcnb_data::launch::{LaunchBuilder, ProcessBuilder};
22/// use libcnb_data::process_type;
23///
24/// let launch_toml = LaunchBuilder::new()
25///     .process(
26///         ProcessBuilder::new(process_type!("web"), ["bundle"])
27///             .args(["exec", "ruby", "app.rb"])
28///             .build(),
29///     )
30///     .build();
31///
32/// assert!(toml::to_string(&launch_toml).is_ok());
33/// ```
34#[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    /// Adds a process to the launch configuration.
46    pub fn process<P: Into<Process>>(&mut self, process: P) -> &mut Self {
47        self.launch.processes.push(process.into());
48        self
49    }
50
51    /// Adds multiple processes to the launch configuration.
52    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    /// Adds a label to the launch configuration.
64    pub fn label<L: Into<Label>>(&mut self, label: L) -> &mut Self {
65        self.launch.labels.push(label.into());
66        self
67    }
68
69    /// Adds multiple labels to the launch configuration.
70    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    /// Adds a slice to the launch configuration.
79    pub fn slice<S: Into<Slice>>(&mut self, slice: S) -> &mut Self {
80        self.launch.slices.push(slice.into());
81        self
82    }
83
84    /// Adds multiple slices to the launch configuration.
85    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    /// Builds the `Launch` based on the configuration of this builder.
94    #[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    // There is no explicitly defined value in the CNB spec that denotes the app directory. Since
128    // we cannot enforce skipping serialization (which indicates the app directory) from this type
129    // directly, we serialize this a ".". The CNB spec says that any relative path is treated
130    // relative to the app directory, so "." will be the app directory itself. However, types that
131    // contain this type (i.e. Process), should always add
132    // `#[serde(skip_serializing_if = "WorkingDirectory::is_app")]` to a field of this type.
133    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
144// Custom Serialize implementation since we want to always serialize as a string. Serde's untagged
145// enum representation does not work here since App would serialize as null, but we want a default
146// string value. #[serde(rename = ".")] doesn't work here. There are more generic solutions that can
147// be found on the web, but they're much more heavyweight than this simple Serialize implementation.
148impl 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
170/// A non-consuming builder for [`Process`] values.
171///
172/// # Examples
173/// ```
174/// # use libcnb_data::process_type;
175/// # use libcnb_data::launch::ProcessBuilder;
176/// ProcessBuilder::new(process_type!("web"), ["java"])
177///     .arg("-jar")
178///     .arg("target/application-1.0.0.jar")
179///     .default(true)
180///     .build();
181/// ```
182impl ProcessBuilder {
183    /// Constructs a new `ProcessBuilder` with the following defaults:
184    ///
185    /// * No additional, user-overridable, arguments to the command
186    /// * `default` is `false`
187    /// * `working_directory` will be `WorkingDirectory::App`.
188    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    /// Adds a user-overridable argument to the process.
201    ///
202    /// Only one argument can be passed per use. So instead of:
203    /// ```
204    /// # use libcnb_data::process_type;
205    /// # libcnb_data::launch::ProcessBuilder::new(process_type!("web"), ["command"])
206    /// .arg("-C /path/to/repo")
207    /// # ;
208    /// ```
209    ///
210    /// usage would be:
211    ///
212    /// ```
213    /// # use libcnb_data::process_type;
214    /// # libcnb_data::launch::ProcessBuilder::new(process_type!("web"), ["command"])
215    /// .arg("-C")
216    /// .arg("/path/to/repo")
217    /// # ;
218    /// ```
219    ///
220    /// To pass multiple arguments see [`args`](Self::args).
221    pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
222        self.process.args.push(arg.into());
223        self
224    }
225
226    /// Adds multiple, user-overridable, arguments to the process.
227    ///
228    /// To pass a single argument see [`arg`](Self::arg).
229    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    /// Sets the `default` flag on the process.
238    ///
239    /// Indicates that the process type should be selected as the buildpack-provided
240    /// default during the export phase.
241    pub fn default(&mut self, value: bool) -> &mut Self {
242        self.process.default = value;
243        self
244    }
245
246    /// Set the working directory for the process.
247    pub fn working_directory(&mut self, value: WorkingDirectory) -> &mut Self {
248        self.process.working_directory = value;
249        self
250    }
251
252    /// Builds the `Process` based on the configuration of this builder.
253    #[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    /// Path globs for this slice.
263    ///
264    /// These globs need to follow the pattern syntax defined in the [Go standard library](https://golang.org/pkg/path/filepath/#Match)
265    /// and only match files/directories inside the application directory.
266    #[serde(rename = "paths")]
267    pub path_globs: Vec<String>,
268}
269
270libcnb_newtype!(
271    launch,
272    /// Construct a [`ProcessType`] value at compile time.
273    ///
274    /// Passing a string that is not a valid `ProcessType` value will yield a compilation error.
275    ///
276    /// # Examples:
277    /// ```
278    /// use libcnb_data::launch::ProcessType;
279    /// use libcnb_data::process_type;
280    ///
281    /// let process_type: ProcessType = process_type!("web");
282    /// ```
283    process_type,
284    /// The type of a process.
285    ///
286    /// It MUST only contain numbers, letters, and the characters `.`, `_`, and `-`.
287    ///
288    /// Use the [`process_type`](crate::process_type) macro to construct a `ProcessType` from a
289    /// literal string. To parse a dynamic string into a `ProcessType`, use
290    /// [`str::parse`](str::parse).
291    ///
292    /// # Examples
293    /// ```
294    /// use libcnb_data::launch::ProcessType;
295    /// use libcnb_data::process_type;
296    ///
297    /// let from_literal = process_type!("web");
298    ///
299    /// let input = "web";
300    /// let from_dynamic: ProcessType = input.parse().unwrap();
301    /// assert_eq!(from_dynamic, from_literal);
302    ///
303    /// let input = "!nv4lid";
304    /// let invalid: Result<ProcessType, _> = input.parse();
305    /// assert!(invalid.is_err());
306    /// ```
307    ProcessType,
308    ProcessTypeError,
309    r"^[[:alnum:]._-]+$"
310);
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use serde_test::{assert_ser_tokens, Token};
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}