Skip to main content

vtcode_config/loader/
layers.rs

1use crate::loader::merge::{merge_toml_values, merge_toml_values_with_origins};
2use hashbrown::HashMap;
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use toml::Value as TomlValue;
6
7use super::fingerprint::fingerprint_toml_value;
8
9/// Source of a configuration layer.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub enum ConfigLayerSource {
12    /// System-wide configuration (e.g., /etc/vtcode/vtcode.toml)
13    System { file: PathBuf },
14    /// User-specific configuration (e.g., ~/.vtcode/vtcode.toml)
15    User { file: PathBuf },
16    /// Project-specific configuration (e.g., .vtcode/projects/foo/config/vtcode.toml)
17    Project { file: PathBuf },
18    /// Workspace-specific configuration (e.g., vtcode.toml in workspace root)
19    Workspace { file: PathBuf },
20    /// Runtime overrides (e.g., CLI flags)
21    Runtime,
22}
23
24impl ConfigLayerSource {
25    /// Lower numbers are lower precedence.
26    pub const fn precedence(&self) -> i16 {
27        match self {
28            Self::System { .. } => 10,
29            Self::User { .. } => 20,
30            Self::Project { .. } => 25,
31            Self::Workspace { .. } => 30,
32            Self::Runtime => 40,
33        }
34    }
35
36    pub fn label(&self) -> String {
37        match self {
38            Self::System { file } => format!("system:{}", file.display()),
39            Self::User { file } => format!("user:{}", file.display()),
40            Self::Project { file } => format!("project:{}", file.display()),
41            Self::Workspace { file } => format!("workspace:{}", file.display()),
42            Self::Runtime => "runtime".to_string(),
43        }
44    }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct ConfigLayerMetadata {
49    pub name: String,
50    pub version: String,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum LayerDisabledReason {
56    ParseError,
57    LoadError,
58    UntrustedWorkspace,
59    PolicyDisabled,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct ConfigLayerLoadError {
64    pub message: String,
65}
66
67/// A single layer of configuration.
68#[derive(Debug, Clone, PartialEq)]
69pub struct ConfigLayerEntry {
70    /// Source of this layer
71    pub source: ConfigLayerSource,
72    /// Stable metadata for this layer
73    pub metadata: ConfigLayerMetadata,
74    /// Parsed TOML content
75    pub config: TomlValue,
76    /// Optional reason the layer was disabled
77    pub disabled_reason: Option<LayerDisabledReason>,
78    /// Optional error attached to this layer
79    pub error: Option<ConfigLayerLoadError>,
80}
81
82impl ConfigLayerEntry {
83    /// Create a new configuration layer entry.
84    pub fn new(source: ConfigLayerSource, config: TomlValue) -> Self {
85        let metadata = ConfigLayerMetadata {
86            name: source.label(),
87            version: fingerprint_toml_value(&config),
88        };
89        Self {
90            source,
91            metadata,
92            config,
93            disabled_reason: None,
94            error: None,
95        }
96    }
97
98    /// Create a disabled layer entry while retaining layer metadata.
99    pub fn disabled(
100        source: ConfigLayerSource,
101        reason: LayerDisabledReason,
102        message: impl Into<String>,
103    ) -> Self {
104        let message = message.into();
105        let config = TomlValue::Table(toml::Table::new());
106        let metadata = ConfigLayerMetadata {
107            name: source.label(),
108            version: fingerprint_toml_value(&TomlValue::String(format!(
109                "{}:{}",
110                source.label(),
111                message
112            ))),
113        };
114        Self {
115            source,
116            metadata,
117            config,
118            disabled_reason: Some(reason),
119            error: Some(ConfigLayerLoadError { message }),
120        }
121    }
122
123    pub fn is_enabled(&self) -> bool {
124        self.disabled_reason.is_none() && self.error.is_none()
125    }
126}
127
128/// A stack of configuration layers, ordered from lowest to highest precedence.
129#[derive(Debug, Clone, Default)]
130pub struct ConfigLayerStack {
131    layers: Vec<ConfigLayerEntry>,
132}
133
134impl ConfigLayerStack {
135    /// Create a new configuration layer stack.
136    pub fn new(layers: Vec<ConfigLayerEntry>) -> Self {
137        Self { layers }
138    }
139
140    /// Add a layer to the stack.
141    pub fn push(&mut self, layer: ConfigLayerEntry) {
142        self.layers.push(layer);
143    }
144
145    /// Merge all layers into a single effective configuration.
146    pub fn effective_config(&self) -> TomlValue {
147        self.effective_config_with_origins().0
148    }
149
150    /// Merge all layers and return an origin map (`path -> winning layer metadata`).
151    pub fn effective_config_with_origins(
152        &self,
153    ) -> (TomlValue, HashMap<String, ConfigLayerMetadata>) {
154        let mut merged = TomlValue::Table(toml::Table::new());
155        let mut origins = HashMap::new();
156        for layer in self.ordered_enabled_layers() {
157            merge_toml_values_with_origins(
158                &mut merged,
159                &layer.config,
160                &mut origins,
161                &layer.metadata,
162            );
163        }
164        (merged, origins)
165    }
166
167    /// Return the first layer error in precedence order.
168    pub fn first_layer_error(&self) -> Option<(&ConfigLayerEntry, &ConfigLayerLoadError)> {
169        for layer in self.ordered_layers() {
170            if let Some(error) = layer.error.as_ref() {
171                return Some((layer, error));
172            }
173        }
174        None
175    }
176
177    /// Merge all enabled layers without origin tracking.
178    pub fn effective_config_without_origins(&self) -> TomlValue {
179        let mut merged = TomlValue::Table(toml::Table::new());
180        for layer in self.ordered_enabled_layers() {
181            merge_toml_values(&mut merged, &layer.config);
182        }
183        merged
184    }
185
186    fn ordered_layers(&self) -> Vec<&ConfigLayerEntry> {
187        let mut with_index: Vec<(usize, &ConfigLayerEntry)> =
188            self.layers.iter().enumerate().collect();
189        with_index.sort_by(|(left_idx, left), (right_idx, right)| {
190            left.source
191                .precedence()
192                .cmp(&right.source.precedence())
193                .then(left_idx.cmp(right_idx))
194        });
195        with_index.into_iter().map(|(_, layer)| layer).collect()
196    }
197
198    fn ordered_enabled_layers(&self) -> Vec<&ConfigLayerEntry> {
199        self.ordered_layers()
200            .into_iter()
201            .filter(|layer| layer.is_enabled())
202            .collect()
203    }
204
205    /// Get all layers in the stack.
206    pub fn layers(&self) -> &[ConfigLayerEntry] {
207        &self.layers
208    }
209}