1use crate::contracts::{
20 AgentConfig, Config, LoopConfig, ParallelConfig, PluginsConfig, ProjectType, QueueConfig,
21};
22use crate::fsutil;
23use anyhow::{Context, Result, bail};
24use serde::{Deserialize, Serialize};
25use std::collections::BTreeMap;
26use std::fs;
27use std::path::Path;
28
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30#[serde(default, deny_unknown_fields)]
31pub struct ConfigLayer {
32 pub version: Option<u32>,
33 pub project_type: Option<ProjectType>,
34 pub queue: QueueConfig,
35 pub agent: AgentConfig,
36 pub parallel: ParallelConfig,
37 #[serde(rename = "loop")]
38 pub loop_field: LoopConfig,
39 pub plugins: PluginsConfig,
40 pub profiles: Option<BTreeMap<String, AgentConfig>>,
42}
43
44pub fn load_layer(path: &Path) -> Result<ConfigLayer> {
46 let raw = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
47 let layer =
48 crate::jsonc::parse_jsonc::<ConfigLayer>(&raw, &format!("config {}", path.display()))?;
49 Ok(layer)
50}
51
52pub fn save_layer(path: &Path, layer: &ConfigLayer) -> Result<()> {
55 let mut to_save = layer.clone();
56 if to_save.version.is_none() {
57 to_save.version = Some(1);
58 }
59 if let Some(parent) = path.parent() {
60 fs::create_dir_all(parent)
61 .with_context(|| format!("create config directory {}", parent.display()))?;
62 }
63 let rendered = serde_json::to_string_pretty(&to_save).context("serialize config JSON")?;
64 fsutil::write_atomic(path, rendered.as_bytes())
65 .with_context(|| format!("write config JSON {}", path.display()))?;
66 Ok(())
67}
68
69pub fn apply_layer(mut base: Config, layer: ConfigLayer) -> Result<Config> {
72 if let Some(version) = layer.version {
73 if version != 1 {
74 bail!(
75 "Unsupported config version: {}. Ralph requires version 1. Update the 'version' field in your config file.",
76 version
77 );
78 }
79 base.version = version;
80 }
81
82 if let Some(project_type) = layer.project_type {
83 base.project_type = Some(project_type);
84 }
85
86 base.queue.merge_from(layer.queue);
87 base.agent.merge_from(layer.agent);
88 base.parallel.merge_from(layer.parallel);
89 base.loop_field.merge_from(layer.loop_field);
90 base.plugins.merge_from(layer.plugins);
91
92 if let Some(profiles) = layer.profiles {
94 let base_profiles = base.profiles.get_or_insert_with(BTreeMap::new);
95 for (name, patch) in profiles {
96 base_profiles
97 .entry(name)
98 .and_modify(|existing| existing.merge_from(patch.clone()))
99 .or_insert(patch);
100 }
101 }
102
103 Ok(base)
104}