use serde::de;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Config {
#[serde(default)]
pub env_files: Vec<EnvFile>,
#[serde(default)]
pub required_tools: Vec<String>,
#[serde(default)]
pub docker: DockerConfig,
#[serde(default)]
pub dev: DevConfig,
#[serde(default)]
pub commands: Vec<CustomCommand>,
}
#[derive(Debug, Clone)]
pub struct EnvFile {
pub path: String,
pub template: Option<String>,
}
impl<'de> Deserialize<'de> for EnvFile {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Simple(String),
Full { path: String, template: Option<String> },
}
match Raw::deserialize(deserializer)? {
Raw::Simple(path) => Ok(EnvFile { path, template: None }),
Raw::Full { path, template } => Ok(EnvFile { path, template }),
}
}
}
#[derive(Debug, Deserialize)]
pub struct DockerConfig {
#[serde(default = "default_compose_file")]
pub compose_file: String,
#[serde(default)]
pub health_checks: Vec<HealthCheck>,
}
impl Default for DockerConfig {
fn default() -> Self {
Self {
compose_file: default_compose_file(),
health_checks: Vec::new(),
}
}
}
fn default_compose_file() -> String {
"docker-compose.yml".to_string()
}
#[derive(Debug)]
pub struct HealthCheck {
pub name: String,
pub cmd: Option<Vec<String>>,
pub url: Option<String>,
pub tcp: Option<String>,
pub timeout: u64,
}
impl<'de> Deserialize<'de> for HealthCheck {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Raw {
name: String,
#[serde(default)]
cmd: Option<Vec<String>>,
#[serde(default)]
url: Option<String>,
#[serde(default)]
tcp: Option<String>,
#[serde(default = "default_timeout")]
timeout: u64,
}
let raw = Raw::deserialize(deserializer)?;
let count = raw.cmd.is_some() as u8 + raw.url.is_some() as u8 + raw.tcp.is_some() as u8;
if count == 0 {
return Err(de::Error::custom(format!(
"health check '{}': must specify one of cmd, url, or tcp",
raw.name
)));
}
if count > 1 {
return Err(de::Error::custom(format!(
"health check '{}': only one of cmd, url, or tcp may be set",
raw.name
)));
}
if let Some(url) = &raw.url {
let lower = url.to_lowercase();
if !lower.starts_with("http://") && !lower.starts_with("https://") {
return Err(de::Error::custom(format!(
"health check '{}': url must use http:// or https:// scheme, got: {}",
raw.name, url
)));
}
}
Ok(HealthCheck {
name: raw.name,
cmd: raw.cmd,
url: raw.url,
tcp: raw.tcp,
timeout: raw.timeout,
})
}
}
fn default_timeout() -> u64 {
30
}
#[derive(Debug, Deserialize)]
pub struct DevConfig {
#[serde(default = "default_mprocs_config")]
pub mprocs_config: String,
#[serde(default)]
pub hooks: Vec<Hook>,
#[serde(default)]
pub runner: Option<RunnerConfig>,
}
impl Default for DevConfig {
fn default() -> Self {
Self {
mprocs_config: default_mprocs_config(),
hooks: Vec::new(),
runner: None,
}
}
}
fn default_mprocs_config() -> String {
"mprocs.yaml".to_string()
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
pub enum RunnerConfig {
#[serde(rename = "mprocs")]
Mprocs,
#[serde(rename = "shell")]
Shell { cmd: String },
#[serde(rename = "none")]
None,
}
#[derive(Debug, Deserialize)]
pub struct Hook {
pub cmd: String,
#[serde(default)]
pub cwd: Option<String>,
#[serde(default)]
pub condition: Option<HookCondition>,
}
#[derive(Debug, Deserialize)]
pub struct HookCondition {
#[serde(default)]
pub missing: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct CustomCommand {
pub name: String,
pub cmd: Vec<String>,
#[serde(default)]
pub description: String,
#[serde(default)]
pub docker: bool,
}
impl Config {
pub fn load(toml_str: &str) -> Result<Self, toml::de::Error> {
toml::from_str(toml_str)
}
}