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/// Stable value kind contract for schema-registry fields.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
163#[serde(rename_all = "snake_case")]
164pub enum ConfigSchemaValueKindV1 {
165    /// UTF-8 text value.
166    String,
167    /// Signed integer value.
168    Integer,
169    /// Boolean value.
170    Boolean,
171    /// Filesystem path value.
172    Path,
173    /// JSON text value.
174    Json,
175}
176
177/// Stable field source classification for config schema entries.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
179#[serde(rename_all = "snake_case")]
180pub enum ConfigSchemaSourceV1 {
181    /// Field is owned directly by this crate.
182    BuiltIn,
183    /// Field is generated from shared app policy.
184    BuiltInShared,
185}
186
187/// Stable deprecation status for config schema entries.
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
189#[serde(rename_all = "snake_case")]
190pub enum ConfigDeprecationStatusV1 {
191    /// Field is active and fully supported.
192    Active,
193    /// Field remains available but is scheduled for removal.
194    Deprecated,
195}
196
197/// Stable schema-field contract for one logical config key.
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
199pub struct ConfigSchemaFieldV1 {
200    /// Scope identifier (`cli`, `dag`, mounted app namespace, ...).
201    pub scope: String,
202    /// Operator-facing logical dotted key.
203    pub logical_key: String,
204    /// Storage key persisted in env-style files.
205    pub storage_key: String,
206    /// Environment variable aliases checked in precedence order.
207    pub env_vars: Vec<String>,
208    /// Field value kind.
209    pub value_kind: ConfigSchemaValueKindV1,
210    /// Whether the value is secret-bearing and should be redacted by default.
211    pub sensitive: bool,
212    /// Optional default value used by core runtime policy.
213    pub default_value: Option<String>,
214    /// Deprecation status marker.
215    pub deprecation_status: ConfigDeprecationStatusV1,
216    /// Human description for docs and explain surfaces.
217    pub description: String,
218}
219
220/// Stable registry scope contract grouping config keys by scope.
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
222pub struct ConfigSchemaScopeV1 {
223    /// Scope identifier.
224    pub scope: String,
225    /// Scope source class.
226    pub source: ConfigSchemaSourceV1,
227    /// Fields within this scope.
228    pub fields: Vec<ConfigSchemaFieldV1>,
229}
230
231/// Stable versioned config-schema registry contract.
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
233pub struct ConfigSchemaRegistryV1 {
234    /// Registry schema id.
235    pub schema_version: String,
236    /// Scope inventory.
237    pub scopes: Vec<ConfigSchemaScopeV1>,
238}
239
240/// Export format for config output.
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
242#[serde(rename_all = "lowercase")]
243pub enum ConfigExportFormat {
244    /// Dotenv output.
245    Env,
246    /// JSON output.
247    Json,
248    /// YAML output.
249    Yaml,
250}
251
252/// Generic config command result envelope.
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
254pub struct ConfigCommandResult {
255    /// Command status marker.
256    pub status: String,
257    /// Command target path.
258    pub command: String,
259}
260
261/// Config error category.
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
263#[serde(rename_all = "kebab-case")]
264pub enum ConfigErrorKind {
265    /// Key/value validation failed.
266    Validation,
267    /// Config text parsing failed.
268    Parse,
269    /// File persistence failed.
270    Persistence,
271    /// Concurrent or semantic conflict detected.
272    Conflict,
273    /// Requested key/value not found.
274    NotFound,
275}
276
277/// Key/value validation failure details.
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
279pub struct ConfigValidationError {
280    /// Failing key when available.
281    pub key: Option<ConfigKey>,
282    /// Validation reason.
283    pub message: String,
284}
285
286/// Config parse failure details.
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
288pub struct ConfigParseError {
289    /// 1-based line number.
290    pub line: usize,
291    /// Raw line content.
292    pub content: String,
293    /// Parse reason.
294    pub message: String,
295}
296
297/// Config persistence failure details.
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
299pub struct ConfigPersistenceError {
300    /// Target path.
301    pub path: String,
302    /// Operation name.
303    pub operation: String,
304    /// Failure reason.
305    pub message: String,
306}
307
308/// Config conflict details.
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
310pub struct ConfigConflictError {
311    /// Conflicting key.
312    pub key: Option<ConfigKey>,
313    /// Conflict reason.
314    pub message: String,
315}
316
317/// Result payload for reload operations.
318#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
319pub struct ConfigReloadResult {
320    /// Reload status.
321    pub status: String,
322    /// Path reloaded.
323    pub reloaded_path: String,
324}
325
326/// Result payload for clear operations.
327#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
328pub struct ConfigClearResult {
329    /// Clear status.
330    pub status: String,
331    /// Number of removed keys.
332    pub removed_keys: usize,
333}