Skip to main content

bijux_cli/contracts/
config.rs

1use std::collections::BTreeMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// Normalized config key.
7#[derive(
8    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
9)]
10pub struct ConfigKey(pub String);
11
12impl ConfigKey {
13    /// Build a validated key.
14    pub fn new(raw: &str) -> Result<Self, String> {
15        let trimmed = raw.trim();
16        if trimmed.is_empty() {
17            return Err("config key cannot be empty".to_string());
18        }
19        if !trimmed.is_ascii() {
20            return Err("config key must be ASCII".to_string());
21        }
22        if trimmed.contains('.') {
23            return Err("config key cannot contain section separator '.'".to_string());
24        }
25
26        let normalized = trimmed
27            .strip_prefix("BIJUXCLI_")
28            .or_else(|| trimmed.strip_prefix("BIJUX_"))
29            .unwrap_or(trimmed)
30            .to_ascii_lowercase();
31        if !normalized.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
32            return Err("config key must contain only alphanumerics and '_'".to_string());
33        }
34        Ok(Self(normalized))
35    }
36
37    /// Borrow normalized key.
38    #[must_use]
39    pub fn as_str(&self) -> &str {
40        &self.0
41    }
42}
43
44/// Validated config value.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
46pub struct ConfigValue(pub String);
47
48impl ConfigValue {
49    /// Build a validated value.
50    pub fn new(raw: &str) -> Result<Self, String> {
51        if !raw.is_ascii() {
52            return Err("config value must be ASCII".to_string());
53        }
54        if raw.chars().any(|ch| matches!(ch, '\r' | '\n' | '\t' | '\u{000B}' | '\u{000C}')) {
55            return Err("config value cannot contain control characters".to_string());
56        }
57        Ok(Self(raw.to_string()))
58    }
59
60    /// Borrow value.
61    #[must_use]
62    pub fn as_str(&self) -> &str {
63        &self.0
64    }
65}
66
67/// One key/value config entry.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
69pub struct ConfigEntry {
70    /// Entry key.
71    pub key: ConfigKey,
72    /// Entry value.
73    pub value: ConfigValue,
74}
75
76/// Snapshot of stored config state.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
78pub struct ConfigSnapshot {
79    /// All active entries by normalized key.
80    pub entries: BTreeMap<ConfigKey, ConfigValue>,
81}
82
83/// Config mutation operation.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
85#[serde(tag = "kind", rename_all = "kebab-case")]
86pub enum ConfigMutation {
87    /// Insert or update a key.
88    Set {
89        /// Key to set.
90        key: ConfigKey,
91        /// Value to persist.
92        value: ConfigValue,
93    },
94    /// Remove a key if present.
95    Unset {
96        /// Key to remove.
97        key: ConfigKey,
98    },
99    /// Remove all keys.
100    Clear,
101}
102
103/// Source of resolved config read value.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
105#[serde(rename_all = "lowercase")]
106pub enum ConfigSource {
107    /// Command-line argument source.
108    Flags,
109    /// Environment variable source.
110    Env,
111    /// File-backed config source.
112    File,
113    /// Built-in defaults source.
114    Defaults,
115}
116
117/// Resolved value including source metadata.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
119pub struct ResolvedConfigValue {
120    /// Resolved key.
121    pub key: ConfigKey,
122    /// Resolved value.
123    pub value: ConfigValue,
124    /// Winning source.
125    pub source: ConfigSource,
126    /// Source file when applicable.
127    pub source_path: Option<String>,
128}
129
130/// Canonical file paths used by config operations.
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
132pub struct ConfigPathSet {
133    /// Active config file path.
134    pub config_file: String,
135    /// Active history file path.
136    pub history_file: String,
137    /// Active plugin directory path.
138    pub plugins_dir: String,
139}
140
141/// Result of loading config state.
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
143pub struct ConfigLoadResult {
144    /// Loaded snapshot.
145    pub snapshot: ConfigSnapshot,
146    /// Paths used for this load.
147    pub paths: ConfigPathSet,
148}
149
150/// Result of persisting config state.
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
152pub struct ConfigWriteResult {
153    /// Whether storage was updated.
154    pub updated: bool,
155    /// Number of entries after write.
156    pub entry_count: usize,
157    /// Target path written.
158    pub target_path: String,
159}
160
161/// Export format for config output.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
163#[serde(rename_all = "lowercase")]
164pub enum ConfigExportFormat {
165    /// Dotenv output.
166    Env,
167    /// JSON output.
168    Json,
169    /// YAML output.
170    Yaml,
171}
172
173/// Generic config command result envelope.
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
175pub struct ConfigCommandResult {
176    /// Command status marker.
177    pub status: String,
178    /// Command target path.
179    pub command: String,
180}
181
182/// Config error category.
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
184#[serde(rename_all = "kebab-case")]
185pub enum ConfigErrorKind {
186    /// Key/value validation failed.
187    Validation,
188    /// Config text parsing failed.
189    Parse,
190    /// File persistence failed.
191    Persistence,
192    /// Concurrent or semantic conflict detected.
193    Conflict,
194    /// Requested key/value not found.
195    NotFound,
196}
197
198/// Key/value validation failure details.
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
200pub struct ConfigValidationError {
201    /// Failing key when available.
202    pub key: Option<ConfigKey>,
203    /// Validation reason.
204    pub message: String,
205}
206
207/// Config parse failure details.
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
209pub struct ConfigParseError {
210    /// 1-based line number.
211    pub line: usize,
212    /// Raw line content.
213    pub content: String,
214    /// Parse reason.
215    pub message: String,
216}
217
218/// Config persistence failure details.
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
220pub struct ConfigPersistenceError {
221    /// Target path.
222    pub path: String,
223    /// Operation name.
224    pub operation: String,
225    /// Failure reason.
226    pub message: String,
227}
228
229/// Config conflict details.
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
231pub struct ConfigConflictError {
232    /// Conflicting key.
233    pub key: Option<ConfigKey>,
234    /// Conflict reason.
235    pub message: String,
236}
237
238/// Result payload for reload operations.
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
240pub struct ConfigReloadResult {
241    /// Reload status.
242    pub status: String,
243    /// Path reloaded.
244    pub reloaded_path: String,
245}
246
247/// Result payload for clear operations.
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
249pub struct ConfigClearResult {
250    /// Clear status.
251    pub status: String,
252    /// Number of removed keys.
253    pub removed_keys: usize,
254}