Skip to main content

devset_core/
settings.rs

1//! Settings a profile defaults and a target overrides.
2
3use derive_more::Display;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use crate::errors::{ProfileError, Result};
8use crate::merge::Driver;
9use crate::name::ProfileName;
10use crate::resolve::Layer;
11
12/// A `[merge]` table: defaults in `profile.toml`, decisions in `.devset/config.toml`.
13#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
14#[serde(deny_unknown_fields, rename_all = "kebab-case")]
15pub struct MergeSpec {
16    /// What a run does when a file conflicts.
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub on_conflict: Option<OnConflict>,
19    /// A merge program, with `%O %A %B %P` substituted.
20    ///
21    /// From a profile it is only a suggestion: devset never runs a program a profile names.
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    #[schemars(with = "Option<String>")]
24    pub driver: Option<Driver>,
25}
26
27/// What a run does when a file conflicts.
28#[derive(
29    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Display,
30)]
31#[serde(rename_all = "kebab-case")]
32#[display(rename_all = "kebab-case")]
33pub enum OnConflict {
34    /// Write every other file and advance the lock; conflicts wait in `.devset/conflicts/`.
35    #[default]
36    ApplyOthers,
37    /// Write nothing but the conflicts until every one is resolved.
38    ApplyNone,
39}
40
41/// The settings in force: devset's defaults, then each layer's, then the target's.
42#[derive(Clone, Debug, Default)]
43pub struct Settings {
44    /// What a run does when a file conflicts.
45    pub on_conflict: OnConflict,
46    /// How files are merged.
47    pub driver: Driver,
48}
49
50/// A merge driver a layer suggests and the target has not configured.
51#[derive(Clone, Debug)]
52pub struct Suggestion {
53    /// The layer's profile name.
54    pub layer: ProfileName,
55    /// The driver it names.
56    pub driver: Driver,
57}
58
59impl Settings {
60    /// The settings `target` decides, falling back to what `layers` agree on.
61    pub(crate) fn compose(layers: &[Layer], target: &MergeSpec) -> Result<Self> {
62        let on_conflict = match target.on_conflict {
63            Some(value) => value,
64            None => {
65                agreed(layers, "merge.on-conflict", |merge| merge.on_conflict)?.unwrap_or_default()
66            },
67        };
68        let driver = target.driver.clone().unwrap_or_default();
69        Ok(Self { on_conflict, driver })
70    }
71
72    /// The drivers `layers` suggest, while `target` configures none.
73    pub(crate) fn suggestions(layers: &[Layer], target: &MergeSpec) -> Vec<Suggestion> {
74        if target.driver.is_some() {
75            return Vec::new();
76        }
77        layers
78            .iter()
79            .filter_map(|layer| {
80                let driver = layer.merge().driver.clone()?;
81                Some(Suggestion { layer: layer.name().clone(), driver })
82            })
83            .collect()
84    }
85}
86
87/// The value every layer that sets `key` agrees on.
88fn agreed<T: Copy + PartialEq>(
89    layers: &[Layer], key: &'static str, get: fn(&MergeSpec) -> Option<T>,
90) -> Result<Option<T>> {
91    let mut agreed: Option<(T, &Layer)> = None;
92    for layer in layers {
93        let Some(value) = get(layer.merge()) else {
94            continue;
95        };
96        match agreed {
97            Some((first, by)) if first != value => {
98                let (first, second) = (by.name().clone(), layer.name().clone());
99                return Err(ProfileError::Setting { key: key.to_owned(), first, second }.into());
100            },
101            Some(_) => {},
102            None => agreed = Some((value, layer)),
103        }
104    }
105    Ok(agreed.map(|(value, _)| value))
106}