1use std::collections::BTreeMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6#[derive(
8 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
9)]
10pub struct ConfigKey(pub String);
11
12impl ConfigKey {
13 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 #[must_use]
39 pub fn as_str(&self) -> &str {
40 &self.0
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
46pub struct ConfigValue(pub String);
47
48impl ConfigValue {
49 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 #[must_use]
62 pub fn as_str(&self) -> &str {
63 &self.0
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
69pub struct ConfigEntry {
70 pub key: ConfigKey,
72 pub value: ConfigValue,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
78pub struct ConfigSnapshot {
79 pub entries: BTreeMap<ConfigKey, ConfigValue>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
85#[serde(tag = "kind", rename_all = "kebab-case")]
86pub enum ConfigMutation {
87 Set {
89 key: ConfigKey,
91 value: ConfigValue,
93 },
94 Unset {
96 key: ConfigKey,
98 },
99 Clear,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
105#[serde(rename_all = "lowercase")]
106pub enum ConfigSource {
107 Flags,
109 Env,
111 File,
113 Defaults,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
119pub struct ResolvedConfigValue {
120 pub key: ConfigKey,
122 pub value: ConfigValue,
124 pub source: ConfigSource,
126 pub source_path: Option<String>,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
132pub struct ConfigPathSet {
133 pub config_file: String,
135 pub history_file: String,
137 pub plugins_dir: String,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
143pub struct ConfigLoadResult {
144 pub snapshot: ConfigSnapshot,
146 pub paths: ConfigPathSet,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
152pub struct ConfigWriteResult {
153 pub updated: bool,
155 pub entry_count: usize,
157 pub target_path: String,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
163#[serde(rename_all = "lowercase")]
164pub enum ConfigExportFormat {
165 Env,
167 Json,
169 Yaml,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
175pub struct ConfigCommandResult {
176 pub status: String,
178 pub command: String,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
184#[serde(rename_all = "kebab-case")]
185pub enum ConfigErrorKind {
186 Validation,
188 Parse,
190 Persistence,
192 Conflict,
194 NotFound,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
200pub struct ConfigValidationError {
201 pub key: Option<ConfigKey>,
203 pub message: String,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
209pub struct ConfigParseError {
210 pub line: usize,
212 pub content: String,
214 pub message: String,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
220pub struct ConfigPersistenceError {
221 pub path: String,
223 pub operation: String,
225 pub message: String,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
231pub struct ConfigConflictError {
232 pub key: Option<ConfigKey>,
234 pub message: String,
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
240pub struct ConfigReloadResult {
241 pub status: String,
243 pub reloaded_path: String,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
249pub struct ConfigClearResult {
250 pub status: String,
252 pub removed_keys: usize,
254}