use crate::contracts::{
AgentConfig, Config, LoopConfig, ParallelConfig, PluginsConfig, ProjectType, QueueConfig,
};
use crate::fsutil;
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ConfigLayer {
pub version: Option<u32>,
pub project_type: Option<ProjectType>,
pub queue: QueueConfig,
pub agent: AgentConfig,
pub parallel: ParallelConfig,
#[serde(rename = "loop")]
pub loop_field: LoopConfig,
pub plugins: PluginsConfig,
pub profiles: Option<BTreeMap<String, AgentConfig>>,
}
pub fn load_layer(path: &Path) -> Result<ConfigLayer> {
let raw = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
let layer =
crate::jsonc::parse_jsonc::<ConfigLayer>(&raw, &format!("config {}", path.display()))?;
Ok(layer)
}
pub fn save_layer(path: &Path, layer: &ConfigLayer) -> Result<()> {
let mut to_save = layer.clone();
if to_save.version.is_none() {
to_save.version = Some(2);
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create config directory {}", parent.display()))?;
}
let rendered = serde_json::to_string_pretty(&to_save).context("serialize config JSON")?;
fsutil::write_atomic(path, rendered.as_bytes())
.with_context(|| format!("write config JSON {}", path.display()))?;
Ok(())
}
pub fn apply_layer(mut base: Config, layer: ConfigLayer) -> Result<Config> {
if let Some(version) = layer.version {
if version != 2 {
bail!(
"Unsupported config version: {}. Ralph requires version 2. Upgrade your config file to the 0.3 contract and set `version` to 2.",
version
);
}
base.version = version;
}
if let Some(project_type) = layer.project_type {
base.project_type = Some(project_type);
}
base.queue.merge_from(layer.queue);
base.agent.merge_from(layer.agent);
base.parallel.merge_from(layer.parallel);
base.loop_field.merge_from(layer.loop_field);
base.plugins.merge_from(layer.plugins);
if let Some(profiles) = layer.profiles {
let base_profiles = base.profiles.get_or_insert_with(BTreeMap::new);
for (name, patch) in profiles {
base_profiles
.entry(name)
.and_modify(|existing| existing.merge_from(patch.clone()))
.or_insert(patch);
}
}
Ok(base)
}