use crate::newtypes::libcnb_newtype;
use serde::{Deserialize, Serialize, Serializer};
use std::path::PathBuf;
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
#[serde(deny_unknown_fields)]
pub struct Launch {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub labels: Vec<Label>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub processes: Vec<Process>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub slices: Vec<Slice>,
}
#[derive(Default)]
pub struct LaunchBuilder {
launch: Launch,
}
impl LaunchBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn process<P: Into<Process>>(&mut self, process: P) -> &mut Self {
self.launch.processes.push(process.into());
self
}
pub fn processes<I: IntoIterator<Item = P>, P: Into<Process>>(
&mut self,
processes: I,
) -> &mut Self {
for process in processes {
self.process(process);
}
self
}
pub fn label<L: Into<Label>>(&mut self, label: L) -> &mut Self {
self.launch.labels.push(label.into());
self
}
pub fn labels<I: IntoIterator<Item = L>, L: Into<Label>>(&mut self, labels: I) -> &mut Self {
for label in labels {
self.label(label);
}
self
}
pub fn slice<S: Into<Slice>>(&mut self, slice: S) -> &mut Self {
self.launch.slices.push(slice.into());
self
}
pub fn slices<I: IntoIterator<Item = S>, S: Into<Slice>>(&mut self, slices: I) -> &mut Self {
for slice in slices {
self.slice(slice);
}
self
}
#[must_use]
pub fn build(&self) -> Launch {
self.launch.clone()
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct Label {
pub key: String,
pub value: String,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Process {
pub r#type: ProcessType,
pub command: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub default: bool,
#[serde(
rename = "working-dir",
default,
skip_serializing_if = "WorkingDirectory::is_app"
)]
pub working_directory: WorkingDirectory,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(untagged)]
pub enum WorkingDirectory {
#[default]
App,
Directory(PathBuf),
}
impl WorkingDirectory {
#[must_use]
pub fn is_app(&self) -> bool {
matches!(self, Self::App)
}
}
impl Serialize for WorkingDirectory {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::App => serializer.serialize_str("."),
Self::Directory(path) => path.serialize(serializer),
}
}
}
pub struct ProcessBuilder {
process: Process,
}
impl ProcessBuilder {
pub fn new(r#type: ProcessType, command: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
process: Process {
r#type,
command: command.into_iter().map(Into::into).collect(),
args: Vec::new(),
default: false,
working_directory: WorkingDirectory::App,
},
}
}
pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
self.process.args.push(arg.into());
self
}
pub fn args(&mut self, args: impl IntoIterator<Item = impl Into<String>>) -> &mut Self {
for arg in args {
self.arg(arg);
}
self
}
pub fn default(&mut self, value: bool) -> &mut Self {
self.process.default = value;
self
}
pub fn working_directory(&mut self, value: WorkingDirectory) -> &mut Self {
self.process.working_directory = value;
self
}
#[must_use]
pub fn build(&self) -> Process {
self.process.clone()
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct Slice {
#[serde(rename = "paths")]
pub path_globs: Vec<String>,
}
libcnb_newtype!(
launch,
process_type,
ProcessType,
ProcessTypeError,
r"^[[:alnum:]._-]+$"
);
#[cfg(test)]
mod tests {
use super::*;
use serde_test::{Token, assert_ser_tokens};
#[test]
fn launch_builder_add_processes() {
let launch = LaunchBuilder::new()
.process(ProcessBuilder::new(process_type!("web"), ["web_command"]).build())
.processes([
ProcessBuilder::new(process_type!("another"), ["another_command"]).build(),
ProcessBuilder::new(process_type!("worker"), ["worker_command"]).build(),
])
.build();
assert_eq!(
launch.processes,
[
ProcessBuilder::new(process_type!("web"), ["web_command"]).build(),
ProcessBuilder::new(process_type!("another"), ["another_command"]).build(),
ProcessBuilder::new(process_type!("worker"), ["worker_command"]).build(),
]
);
}
#[test]
fn process_type_validation_valid() {
assert!("web".parse::<ProcessType>().is_ok());
assert!("Abc123._-".parse::<ProcessType>().is_ok());
}
#[test]
fn process_type_validation_invalid() {
assert_eq!(
"worker/foo".parse::<ProcessType>(),
Err(ProcessTypeError::InvalidValue(String::from("worker/foo")))
);
assert_eq!(
"worker:foo".parse::<ProcessType>(),
Err(ProcessTypeError::InvalidValue(String::from("worker:foo")))
);
assert_eq!(
"worker foo".parse::<ProcessType>(),
Err(ProcessTypeError::InvalidValue(String::from("worker foo")))
);
assert_eq!(
"".parse::<ProcessType>(),
Err(ProcessTypeError::InvalidValue(String::new()))
);
}
#[test]
fn process_with_default_values_deserialization() {
let toml_str = r#"
type = "web"
command = ["foo"]
"#;
assert_eq!(
toml::from_str::<Process>(toml_str),
Ok(Process {
r#type: process_type!("web"),
command: vec![String::from("foo")],
args: Vec::new(),
default: false,
working_directory: WorkingDirectory::App
})
);
}
#[test]
fn process_with_default_values_serialization() {
let process = ProcessBuilder::new(process_type!("web"), ["foo"]).build();
let string = toml::to_string(&process).unwrap();
assert_eq!(
string,
r#"type = "web"
command = ["foo"]
"#
);
}
#[test]
fn process_with_some_default_values_serialization() {
let process = ProcessBuilder::new(process_type!("web"), ["foo"])
.default(true)
.working_directory(WorkingDirectory::Directory(PathBuf::from("dist")))
.build();
let string = toml::to_string(&process).unwrap();
assert_eq!(
string,
r#"type = "web"
command = ["foo"]
default = true
working-dir = "dist"
"#
);
}
#[test]
fn process_builder() {
let mut process_builder = ProcessBuilder::new(process_type!("web"), ["java"]);
assert_eq!(
process_builder.build(),
Process {
r#type: process_type!("web"),
command: vec![String::from("java")],
args: Vec::new(),
default: false,
working_directory: WorkingDirectory::App
}
);
process_builder.default(true);
assert_eq!(
process_builder.build(),
Process {
r#type: process_type!("web"),
command: vec![String::from("java")],
args: Vec::new(),
default: true,
working_directory: WorkingDirectory::App
}
);
process_builder.working_directory(WorkingDirectory::Directory(PathBuf::from("dist")));
assert_eq!(
process_builder.build(),
Process {
r#type: process_type!("web"),
command: vec![String::from("java")],
args: Vec::new(),
default: true,
working_directory: WorkingDirectory::Directory(PathBuf::from("dist"))
}
);
}
#[test]
fn process_builder_args() {
assert_eq!(
ProcessBuilder::new(process_type!("web"), ["java"])
.arg("foo")
.args(["baz", "eggs"])
.arg("bar")
.build(),
Process {
r#type: process_type!("web"),
command: vec![String::from("java")],
args: vec![
String::from("foo"),
String::from("baz"),
String::from("eggs"),
String::from("bar"),
],
default: false,
working_directory: WorkingDirectory::App
}
);
}
#[test]
fn process_working_directory_serialization() {
assert_ser_tokens(&WorkingDirectory::App, &[Token::BorrowedStr(".")]);
assert_ser_tokens(
&WorkingDirectory::Directory(PathBuf::from("/")),
&[Token::BorrowedStr("/")],
);
assert_ser_tokens(
&WorkingDirectory::Directory(PathBuf::from("/foo/bar")),
&[Token::BorrowedStr("/foo/bar")],
);
assert_ser_tokens(
&WorkingDirectory::Directory(PathBuf::from("relative/foo/bar")),
&[Token::BorrowedStr("relative/foo/bar")],
);
}
}