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, Default)]
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    #[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
145// Custom Serialize implementation since we want to always serialize as a string. Serde's untagged
146// enum representation does not work here since App would serialize as null, but we want a default
147// string value. #[serde(rename = ".")] doesn't work here. There are more generic solutions that can
148// be found on the web, but they're much more heavyweight than this simple Serialize implementation.
149impl 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
165/// A non-consuming builder for [`Process`] values.
166///
167/// # Examples
168/// ```
169/// # use libcnb_data::process_type;
170/// # use libcnb_data::launch::ProcessBuilder;
171/// ProcessBuilder::new(process_type!("web"), ["java"])
172///     .arg("-jar")
173///     .arg("target/application-1.0.0.jar")
174///     .default(true)
175///     .build();
176/// ```
177impl ProcessBuilder {
178    /// Constructs a new `ProcessBuilder` with the following defaults:
179    ///
180    /// * No additional, user-overridable, arguments to the command
181    /// * `default` is `false`
182    /// * `working_directory` will be `WorkingDirectory::App`.
183    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    /// Adds a user-overridable argument to the process.
196    ///
197    /// Only one argument can be passed per use. So instead of:
198    /// ```
199    /// # use libcnb_data::process_type;
200    /// # libcnb_data::launch::ProcessBuilder::new(process_type!("web"), ["command"])
201    /// .arg("-C /path/to/repo")
202    /// # ;
203    /// ```
204    ///
205    /// usage would be:
206    ///
207    /// ```
208    /// # use libcnb_data::process_type;
209    /// # libcnb_data::launch::ProcessBuilder::new(process_type!("web"), ["command"])
210    /// .arg("-C")
211    /// .arg("/path/to/repo")
212    /// # ;
213    /// ```
214    ///
215    /// To pass multiple arguments see [`args`](Self::args).
216    pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
217        self.process.args.push(arg.into());
218        self
219    }
220
221    /// Adds multiple, user-overridable, arguments to the process.
222    ///
223    /// To pass a single argument see [`arg`](Self::arg).
224    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    /// Sets the `default` flag on the process.
233    ///
234    /// Indicates that the process type should be selected as the buildpack-provided
235    /// default during the export phase.
236    pub fn default(&mut self, value: bool) -> &mut Self {
237        self.process.default = value;
238        self
239    }
240
241    /// Set the working directory for the process.
242    pub fn working_directory(&mut self, value: WorkingDirectory) -> &mut Self {
243        self.process.working_directory = value;
244        self
245    }
246
247    /// Builds the `Process` based on the configuration of this builder.
248    #[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    /// Path globs for this slice.
258    ///
259    /// These globs need to follow the pattern syntax defined in the [Go standard library](https://golang.org/pkg/path/filepath/#Match)
260    /// and only match files/directories inside the application directory.
261    #[serde(rename = "paths")]
262    pub path_globs: Vec<String>,
263}
264
265libcnb_newtype!(
266    launch,
267    /// Construct a [`ProcessType`] value at compile time.
268    ///
269    /// Passing a string that is not a valid `ProcessType` value will yield a compilation error.
270    ///
271    /// # Examples:
272    /// ```
273    /// use libcnb_data::launch::ProcessType;
274    /// use libcnb_data::process_type;
275    ///
276    /// let process_type: ProcessType = process_type!("web");
277    /// ```
278    process_type,
279    /// The type of a process.
280    ///
281    /// It MUST only contain numbers, letters, and the characters `.`, `_`, and `-`.
282    ///
283    /// Use the [`process_type`](crate::process_type) macro to construct a `ProcessType` from a
284    /// literal string. To parse a dynamic string into a `ProcessType`, use
285    /// [`str::parse`](str::parse).
286    ///
287    /// # Examples
288    /// ```
289    /// use libcnb_data::launch::ProcessType;
290    /// use libcnb_data::process_type;
291    ///
292    /// let from_literal = process_type!("web");
293    ///
294    /// let input = "web";
295    /// let from_dynamic: ProcessType = input.parse().unwrap();
296    /// assert_eq!(from_dynamic, from_literal);
297    ///
298    /// let input = "!nv4lid";
299    /// let invalid: Result<ProcessType, _> = input.parse();
300    /// assert!(invalid.is_err());
301    /// ```
302    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}