1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt::{Display, Formatter};
3use std::sync::OnceLock;
4
5use crate::config::ConfigError;
6
7#[derive(Debug, Clone, PartialEq)]
9pub struct TomlEditResult {
10 pub previous: Option<ConfigValue>,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
16pub enum ConfigSource {
17 BuiltinDefaults,
19 PresentationDefaults,
21 ConfigFile,
23 Secrets,
26 Environment,
28 Cli,
30 Session,
32 Derived,
34}
35
36impl Display for ConfigSource {
37 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
38 let value = match self {
39 ConfigSource::BuiltinDefaults => "defaults",
40 ConfigSource::PresentationDefaults => "presentation",
41 ConfigSource::ConfigFile => "file",
42 ConfigSource::Secrets => "secrets",
43 ConfigSource::Environment => "env",
44 ConfigSource::Cli => "cli",
45 ConfigSource::Session => "session",
46 ConfigSource::Derived => "derived",
47 };
48 write!(f, "{value}")
49 }
50}
51
52#[derive(Debug, Clone, PartialEq)]
54pub enum ConfigValue {
55 String(String),
57 Bool(bool),
59 Integer(i64),
61 Float(f64),
63 List(Vec<ConfigValue>),
65 Secret(SecretValue),
67}
68
69impl ConfigValue {
70 pub fn is_secret(&self) -> bool {
81 matches!(self, ConfigValue::Secret(_))
82 }
83
84 pub fn reveal(&self) -> &ConfigValue {
95 match self {
96 ConfigValue::Secret(secret) => secret.expose(),
97 other => other,
98 }
99 }
100
101 pub fn into_secret(self) -> ConfigValue {
112 match self {
113 ConfigValue::Secret(_) => self,
114 other => ConfigValue::Secret(SecretValue::new(other)),
115 }
116 }
117
118 pub(crate) fn from_toml(path: &str, value: &toml::Value) -> Result<Self, ConfigError> {
119 match value {
120 toml::Value::String(v) => Ok(Self::String(v.clone())),
121 toml::Value::Integer(v) => Ok(Self::Integer(*v)),
122 toml::Value::Float(v) => Ok(Self::Float(*v)),
123 toml::Value::Boolean(v) => Ok(Self::Bool(*v)),
124 toml::Value::Datetime(v) => Ok(Self::String(v.to_string())),
125 toml::Value::Array(values) => {
126 let mut out = Vec::with_capacity(values.len());
127 for item in values {
128 out.push(Self::from_toml(path, item)?);
129 }
130 Ok(Self::List(out))
131 }
132 toml::Value::Table(_) => Err(ConfigError::UnsupportedTomlValue {
133 path: path.to_string(),
134 kind: "table".to_string(),
135 }),
136 }
137 }
138
139 pub(crate) fn as_interpolation_string(
140 &self,
141 key: &str,
142 placeholder: &str,
143 ) -> Result<String, ConfigError> {
144 match self.reveal() {
145 ConfigValue::String(value) => Ok(value.clone()),
146 ConfigValue::Bool(value) => Ok(value.to_string()),
147 ConfigValue::Integer(value) => Ok(value.to_string()),
148 ConfigValue::Float(value) => Ok(value.to_string()),
149 ConfigValue::List(_) => Err(ConfigError::NonScalarPlaceholder {
150 key: key.to_string(),
151 placeholder: placeholder.to_string(),
152 }),
153 ConfigValue::Secret(_) => Err(ConfigError::NonScalarPlaceholder {
154 key: key.to_string(),
155 placeholder: placeholder.to_string(),
156 }),
157 }
158 }
159}
160
161#[derive(Clone, PartialEq)]
163pub struct SecretValue(Box<ConfigValue>);
164
165impl SecretValue {
166 pub fn new(value: ConfigValue) -> Self {
177 Self(Box::new(value))
178 }
179
180 pub fn expose(&self) -> &ConfigValue {
182 &self.0
183 }
184
185 pub fn into_inner(self) -> ConfigValue {
187 *self.0
188 }
189}
190
191impl std::fmt::Debug for SecretValue {
192 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
193 write!(f, "[REDACTED]")
194 }
195}
196
197impl Display for SecretValue {
198 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
199 write!(f, "[REDACTED]")
200 }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum SchemaValueType {
206 String,
208 Bool,
210 Integer,
212 Float,
214 StringList,
216}
217
218impl Display for SchemaValueType {
219 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
220 let value = match self {
221 SchemaValueType::String => "string",
222 SchemaValueType::Bool => "bool",
223 SchemaValueType::Integer => "integer",
224 SchemaValueType::Float => "float",
225 SchemaValueType::StringList => "list",
226 };
227 write!(f, "{value}")
228 }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub enum BootstrapPhase {
234 Path,
236 Profile,
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub enum BootstrapScopeRule {
243 GlobalOnly,
245 GlobalOrTerminal,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub enum BootstrapValueRule {
252 NonEmptyString,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct BootstrapKeySpec {
259 pub key: &'static str,
261 pub phase: BootstrapPhase,
263 pub runtime_visible: bool,
265 pub scope_rule: BootstrapScopeRule,
267}
268
269impl BootstrapKeySpec {
270 fn allows_scope(&self, scope: &Scope) -> bool {
271 match self.scope_rule {
272 BootstrapScopeRule::GlobalOnly => scope.profile.is_none() && scope.terminal.is_none(),
273 BootstrapScopeRule::GlobalOrTerminal => scope.profile.is_none(),
274 }
275 }
276}
277
278#[derive(Debug, Clone)]
280#[must_use]
281pub struct SchemaEntry {
282 canonical_key: Option<&'static str>,
283 value_type: SchemaValueType,
284 required: bool,
285 writable: bool,
286 allowed_values: Option<Vec<String>>,
287 runtime_visible: bool,
288 bootstrap_phase: Option<BootstrapPhase>,
289 bootstrap_scope_rule: Option<BootstrapScopeRule>,
290 bootstrap_value_rule: Option<BootstrapValueRule>,
291}
292
293impl SchemaEntry {
294 pub fn string() -> Self {
306 Self {
307 canonical_key: None,
308 value_type: SchemaValueType::String,
309 required: false,
310 writable: true,
311 allowed_values: None,
312 runtime_visible: true,
313 bootstrap_phase: None,
314 bootstrap_scope_rule: None,
315 bootstrap_value_rule: None,
316 }
317 }
318
319 pub fn boolean() -> Self {
321 Self {
322 canonical_key: None,
323 value_type: SchemaValueType::Bool,
324 required: false,
325 writable: true,
326 allowed_values: None,
327 runtime_visible: true,
328 bootstrap_phase: None,
329 bootstrap_scope_rule: None,
330 bootstrap_value_rule: None,
331 }
332 }
333
334 pub fn integer() -> Self {
336 Self {
337 canonical_key: None,
338 value_type: SchemaValueType::Integer,
339 required: false,
340 writable: true,
341 allowed_values: None,
342 runtime_visible: true,
343 bootstrap_phase: None,
344 bootstrap_scope_rule: None,
345 bootstrap_value_rule: None,
346 }
347 }
348
349 pub fn float() -> Self {
351 Self {
352 canonical_key: None,
353 value_type: SchemaValueType::Float,
354 required: false,
355 writable: true,
356 allowed_values: None,
357 runtime_visible: true,
358 bootstrap_phase: None,
359 bootstrap_scope_rule: None,
360 bootstrap_value_rule: None,
361 }
362 }
363
364 pub fn string_list() -> Self {
366 Self {
367 canonical_key: None,
368 value_type: SchemaValueType::StringList,
369 required: false,
370 writable: true,
371 allowed_values: None,
372 runtime_visible: true,
373 bootstrap_phase: None,
374 bootstrap_scope_rule: None,
375 bootstrap_value_rule: None,
376 }
377 }
378
379 pub fn required(mut self) -> Self {
381 self.required = true;
382 self
383 }
384
385 pub fn read_only(mut self) -> Self {
387 self.writable = false;
388 self
389 }
390
391 pub fn bootstrap_only(mut self, phase: BootstrapPhase, scope_rule: BootstrapScopeRule) -> Self {
393 self.runtime_visible = false;
394 self.bootstrap_phase = Some(phase);
395 self.bootstrap_scope_rule = Some(scope_rule);
396 self
397 }
398
399 pub fn with_bootstrap_value_rule(mut self, rule: BootstrapValueRule) -> Self {
401 self.bootstrap_value_rule = Some(rule);
402 self
403 }
404
405 pub fn with_allowed_values<I, S>(mut self, values: I) -> Self
407 where
408 I: IntoIterator<Item = S>,
409 S: AsRef<str>,
410 {
411 self.allowed_values = Some(
412 values
413 .into_iter()
414 .map(|value| value.as_ref().to_ascii_lowercase())
415 .collect(),
416 );
417 self
418 }
419
420 pub fn value_type(&self) -> SchemaValueType {
422 self.value_type
423 }
424
425 pub fn allowed_values(&self) -> Option<&[String]> {
427 self.allowed_values.as_deref()
428 }
429
430 pub fn runtime_visible(&self) -> bool {
432 self.runtime_visible
433 }
434
435 pub fn writable(&self) -> bool {
437 self.writable
438 }
439
440 fn with_canonical_key(mut self, key: &'static str) -> Self {
441 self.canonical_key = Some(key);
442 self
443 }
444
445 fn bootstrap_spec(&self) -> Option<BootstrapKeySpec> {
446 Some(BootstrapKeySpec {
447 key: self.canonical_key?,
448 phase: self.bootstrap_phase?,
449 runtime_visible: self.runtime_visible,
450 scope_rule: self.bootstrap_scope_rule?,
451 })
452 }
453
454 fn validate_bootstrap_value(&self, key: &str, value: &ConfigValue) -> Result<(), ConfigError> {
455 match self.bootstrap_value_rule {
456 Some(BootstrapValueRule::NonEmptyString) => match value.reveal() {
457 ConfigValue::String(current) if !current.trim().is_empty() => Ok(()),
458 ConfigValue::String(current) => Err(ConfigError::InvalidBootstrapValue {
459 key: key.to_string(),
460 reason: format!("expected a non-empty string, got {current:?}"),
461 }),
462 other => Err(ConfigError::InvalidBootstrapValue {
463 key: key.to_string(),
464 reason: format!("expected string, got {other:?}"),
465 }),
466 },
467 None => Ok(()),
468 }
469 }
470}
471
472#[derive(Debug, Clone)]
474pub struct ConfigSchema {
475 entries: BTreeMap<String, SchemaEntry>,
476 allow_extensions_namespace: bool,
477}
478
479#[derive(Debug, Clone, Copy, PartialEq, Eq)]
480enum DynamicSchemaKeyKind {
481 PluginCommandState,
482 PluginCommandProvider,
483}
484
485impl Default for ConfigSchema {
486 fn default() -> Self {
487 builtin_config_schema().clone()
488 }
489}
490
491impl ConfigSchema {
492 fn builtin() -> Self {
493 let mut schema = Self {
494 entries: BTreeMap::new(),
495 allow_extensions_namespace: true,
496 };
497
498 schema.insert(
499 "profile.default",
500 SchemaEntry::string()
501 .bootstrap_only(
502 BootstrapPhase::Profile,
503 BootstrapScopeRule::GlobalOrTerminal,
504 )
505 .with_bootstrap_value_rule(BootstrapValueRule::NonEmptyString),
506 );
507 schema.insert(
508 "profile.active",
509 SchemaEntry::string().required().read_only(),
510 );
511 schema.insert("theme.name", SchemaEntry::string());
512 schema.insert("theme.path", SchemaEntry::string_list());
513 schema.insert("user.name", SchemaEntry::string());
514 schema.insert("user.display_name", SchemaEntry::string());
515 schema.insert("user.full_name", SchemaEntry::string());
516 schema.insert("domain", SchemaEntry::string());
517
518 schema.insert(
519 "ui.format",
520 SchemaEntry::string()
521 .with_allowed_values(["auto", "guide", "json", "table", "md", "mreg", "value"]),
522 );
523 schema.insert(
524 "ui.mode",
525 SchemaEntry::string().with_allowed_values(["auto", "plain", "rich"]),
526 );
527 schema.insert(
528 "ui.presentation",
529 SchemaEntry::string().with_allowed_values([
530 "expressive",
531 "compact",
532 "austere",
533 "gammel-og-bitter",
534 ]),
535 );
536 schema.insert(
537 "ui.color.mode",
538 SchemaEntry::string().with_allowed_values(["auto", "always", "never"]),
539 );
540 schema.insert(
541 "ui.unicode.mode",
542 SchemaEntry::string().with_allowed_values(["auto", "always", "never"]),
543 );
544 schema.insert("ui.width", SchemaEntry::integer());
545 schema.insert("ui.margin", SchemaEntry::integer());
546 schema.insert("ui.indent", SchemaEntry::integer());
547 schema.insert(
548 "ui.help.level",
549 SchemaEntry::string()
550 .with_allowed_values(["inherit", "none", "tiny", "normal", "verbose"]),
551 );
552 schema.insert(
553 "ui.guide.default_format",
554 SchemaEntry::string().with_allowed_values(["guide", "inherit", "none"]),
555 );
556 schema.insert(
557 "ui.messages.layout",
558 SchemaEntry::string().with_allowed_values(["grouped", "minimal"]),
559 );
560 schema.insert(
561 "ui.chrome.frame",
562 SchemaEntry::string().with_allowed_values([
563 "none",
564 "top",
565 "bottom",
566 "top-bottom",
567 "square",
568 "round",
569 ]),
570 );
571 schema.insert(
572 "ui.chrome.rule_policy",
573 SchemaEntry::string().with_allowed_values([
574 "per-section",
575 "independent",
576 "separate",
577 "shared",
578 "stacked",
579 "list",
580 ]),
581 );
582 schema.insert(
583 "ui.table.overflow",
584 SchemaEntry::string().with_allowed_values([
585 "clip", "hidden", "crop", "ellipsis", "truncate", "wrap", "none", "visible",
586 ]),
587 );
588 schema.insert(
589 "ui.table.border",
590 SchemaEntry::string().with_allowed_values(["none", "square", "round"]),
591 );
592 schema.insert(
593 "ui.help.table_chrome",
594 SchemaEntry::string().with_allowed_values(["inherit", "none", "square", "round"]),
595 );
596 schema.insert("ui.help.entry_indent", SchemaEntry::string());
597 schema.insert("ui.help.entry_gap", SchemaEntry::string());
598 schema.insert("ui.help.section_spacing", SchemaEntry::string());
599 schema.insert("ui.short_list_max", SchemaEntry::integer());
600 schema.insert("ui.medium_list_max", SchemaEntry::integer());
601 schema.insert("ui.grid_padding", SchemaEntry::integer());
602 schema.insert("ui.grid_columns", SchemaEntry::integer());
603 schema.insert("ui.column_weight", SchemaEntry::integer());
604 schema.insert("ui.mreg.stack_min_col_width", SchemaEntry::integer());
605 schema.insert("ui.mreg.stack_overflow_ratio", SchemaEntry::integer());
606 schema.insert(
607 "ui.message.verbosity",
608 SchemaEntry::string()
609 .with_allowed_values(["error", "warning", "success", "info", "trace"]),
610 );
611 schema.insert("ui.prompt", SchemaEntry::string());
612 schema.insert("ui.prompt.secrets", SchemaEntry::boolean());
613 schema.insert("extensions.plugins.timeout_ms", SchemaEntry::integer());
614 schema.insert("extensions.plugins.discovery.path", SchemaEntry::boolean());
615 schema.insert("repl.prompt", SchemaEntry::string());
616 schema.insert(
617 "repl.input_mode",
618 SchemaEntry::string().with_allowed_values(["auto", "interactive", "basic"]),
619 );
620 schema.insert("repl.simple_prompt", SchemaEntry::boolean());
621 schema.insert("repl.shell_indicator", SchemaEntry::string());
622 schema.insert(
623 "repl.intro",
624 SchemaEntry::string().with_allowed_values(["none", "minimal", "compact", "full"]),
625 );
626 schema.insert("repl.intro_template.minimal", SchemaEntry::string());
627 schema.insert("repl.intro_template.compact", SchemaEntry::string());
628 schema.insert("repl.intro_template.full", SchemaEntry::string());
629 schema.insert("repl.history.path", SchemaEntry::string());
630 schema.insert("repl.history.max_entries", SchemaEntry::integer());
631 schema.insert("repl.history.enabled", SchemaEntry::boolean());
632 schema.insert("repl.history.dedupe", SchemaEntry::boolean());
633 schema.insert("repl.history.profile_scoped", SchemaEntry::boolean());
634 schema.insert("repl.history.menu_rows", SchemaEntry::integer());
635 schema.insert("repl.history.exclude", SchemaEntry::string_list());
636 schema.insert("session.cache.max_results", SchemaEntry::integer());
637 schema.insert("color.prompt.text", SchemaEntry::string());
638 schema.insert("color.prompt.command", SchemaEntry::string());
639 schema.insert("color.prompt.completion.text", SchemaEntry::string());
640 schema.insert("color.prompt.completion.background", SchemaEntry::string());
641 schema.insert("color.prompt.completion.highlight", SchemaEntry::string());
642 schema.insert("color.text", SchemaEntry::string());
643 schema.insert("color.text.muted", SchemaEntry::string());
644 schema.insert("color.key", SchemaEntry::string());
645 schema.insert("color.border", SchemaEntry::string());
646 schema.insert("color.table.header", SchemaEntry::string());
647 schema.insert("color.mreg.key", SchemaEntry::string());
648 schema.insert("color.value", SchemaEntry::string());
649 schema.insert("color.value.number", SchemaEntry::string());
650 schema.insert("color.value.bool_true", SchemaEntry::string());
651 schema.insert("color.value.bool_false", SchemaEntry::string());
652 schema.insert("color.value.null", SchemaEntry::string());
653 schema.insert("color.value.ipv4", SchemaEntry::string());
654 schema.insert("color.value.ipv6", SchemaEntry::string());
655 schema.insert("color.panel.border", SchemaEntry::string());
656 schema.insert("color.panel.title", SchemaEntry::string());
657 schema.insert("color.code", SchemaEntry::string());
658 schema.insert("color.json.key", SchemaEntry::string());
659 schema.insert("color.message.error", SchemaEntry::string());
660 schema.insert("color.message.warning", SchemaEntry::string());
661 schema.insert("color.message.success", SchemaEntry::string());
662 schema.insert("color.message.info", SchemaEntry::string());
663 schema.insert("color.message.trace", SchemaEntry::string());
664 schema.insert("auth.visible.builtins", SchemaEntry::string());
665 schema.insert("auth.visible.plugins", SchemaEntry::string());
666 schema.insert("debug.level", SchemaEntry::integer());
667 schema.insert("log.file.enabled", SchemaEntry::boolean());
668 schema.insert("log.file.path", SchemaEntry::string());
669 schema.insert(
670 "log.file.level",
671 SchemaEntry::string().with_allowed_values(["error", "warn", "info", "debug", "trace"]),
672 );
673
674 schema.insert("base.dir", SchemaEntry::string());
675
676 schema
677 }
678}
679
680impl ConfigSchema {
681 pub fn insert(&mut self, key: &'static str, entry: SchemaEntry) {
683 self.entries
684 .insert(key.to_string(), entry.with_canonical_key(key));
685 }
686
687 pub fn set_allow_extensions_namespace(&mut self, value: bool) {
689 self.allow_extensions_namespace = value;
690 }
691
692 pub fn is_known_key(&self, key: &str) -> bool {
694 self.entries.contains_key(key)
695 || self.is_extension_key(key)
696 || self.is_alias_key(key)
697 || dynamic_schema_key_kind(key).is_some()
698 }
699
700 pub fn is_runtime_visible_key(&self, key: &str) -> bool {
702 self.entries
703 .get(key)
704 .is_some_and(SchemaEntry::runtime_visible)
705 || self.is_extension_key(key)
706 || dynamic_schema_key_kind(key).is_some()
707 }
708
709 pub fn validate_writable_key(&self, key: &str) -> Result<(), ConfigError> {
711 let normalized = key.trim().to_ascii_lowercase();
712 if let Some(entry) = self.entries.get(&normalized)
713 && !entry.writable()
714 {
715 return Err(ConfigError::ReadOnlyConfigKey {
716 key: normalized,
717 reason: "derived at runtime".to_string(),
718 });
719 }
720 Ok(())
721 }
722
723 pub fn bootstrap_key_spec(&self, key: &str) -> Option<BootstrapKeySpec> {
725 let normalized = key.trim().to_ascii_lowercase();
726 self.entries
727 .get(&normalized)
728 .and_then(SchemaEntry::bootstrap_spec)
729 }
730
731 pub fn entries(&self) -> impl Iterator<Item = (&str, &SchemaEntry)> {
733 self.entries
734 .iter()
735 .map(|(key, entry)| (key.as_str(), entry))
736 }
737
738 pub fn expected_type(&self, key: &str) -> Option<SchemaValueType> {
740 self.entries
741 .get(key)
742 .map(|entry| entry.value_type)
743 .or_else(|| dynamic_schema_key_kind(key).map(|_| SchemaValueType::String))
744 }
745
746 pub fn parse_input_value(&self, key: &str, raw: &str) -> Result<ConfigValue, ConfigError> {
764 if !self.is_known_key(key) {
765 return Err(ConfigError::UnknownConfigKeys {
766 keys: vec![key.to_string()],
767 });
768 }
769 self.validate_writable_key(key)?;
770
771 let value = match self.expected_type(key) {
772 Some(SchemaValueType::String) | None => ConfigValue::String(raw.to_string()),
773 Some(SchemaValueType::Bool) => {
774 ConfigValue::Bool(
775 parse_bool(raw).ok_or_else(|| ConfigError::InvalidValueType {
776 key: key.to_string(),
777 expected: SchemaValueType::Bool,
778 actual: "string".to_string(),
779 })?,
780 )
781 }
782 Some(SchemaValueType::Integer) => {
783 let parsed =
784 raw.trim()
785 .parse::<i64>()
786 .map_err(|_| ConfigError::InvalidValueType {
787 key: key.to_string(),
788 expected: SchemaValueType::Integer,
789 actual: "string".to_string(),
790 })?;
791 ConfigValue::Integer(parsed)
792 }
793 Some(SchemaValueType::Float) => {
794 let parsed =
795 raw.trim()
796 .parse::<f64>()
797 .map_err(|_| ConfigError::InvalidValueType {
798 key: key.to_string(),
799 expected: SchemaValueType::Float,
800 actual: "string".to_string(),
801 })?;
802 ConfigValue::Float(parsed)
803 }
804 Some(SchemaValueType::StringList) => {
805 let items = parse_string_list(raw);
806 ConfigValue::List(items.into_iter().map(ConfigValue::String).collect())
807 }
808 };
809
810 if let Some(entry) = self.entries.get(key) {
811 validate_allowed_values(
812 key,
813 &value,
814 entry
815 .allowed_values()
816 .map(|values| values.iter().map(String::as_str).collect::<Vec<_>>())
817 .as_deref(),
818 )?;
819 } else if let Some(DynamicSchemaKeyKind::PluginCommandState) = dynamic_schema_key_kind(key)
820 {
821 validate_allowed_values(key, &value, Some(&["enabled", "disabled"]))?;
822 }
823
824 Ok(value)
825 }
826
827 pub(crate) fn validate_and_adapt(
828 &self,
829 values: &mut BTreeMap<String, ResolvedValue>,
830 ) -> Result<(), ConfigError> {
831 let mut unknown = Vec::new();
832 for key in values.keys() {
833 if self.is_runtime_visible_key(key) {
834 continue;
835 }
836 unknown.push(key.clone());
837 }
838 if !unknown.is_empty() {
839 unknown.sort();
840 return Err(ConfigError::UnknownConfigKeys { keys: unknown });
841 }
842
843 for (key, entry) in &self.entries {
844 if entry.runtime_visible && entry.required && !values.contains_key(key) {
845 return Err(ConfigError::MissingRequiredKey { key: key.clone() });
846 }
847 }
848
849 for (key, resolved) in values.iter_mut() {
850 if let Some(kind) = dynamic_schema_key_kind(key) {
851 resolved.value = adapt_dynamic_value_for_schema(key, &resolved.value, kind)?;
852 continue;
853 }
854 let Some(schema_entry) = self.entries.get(key) else {
855 continue;
856 };
857 if !schema_entry.runtime_visible {
858 continue;
859 }
860 resolved.value = adapt_value_for_schema(key, &resolved.value, schema_entry)?;
861 }
862
863 Ok(())
864 }
865
866 fn is_extension_key(&self, key: &str) -> bool {
867 self.allow_extensions_namespace && key.starts_with("extensions.")
868 }
869
870 fn is_alias_key(&self, key: &str) -> bool {
871 key.starts_with("alias.")
872 }
873
874 pub fn validate_key_scope(&self, key: &str, scope: &Scope) -> Result<(), ConfigError> {
876 let normalized_scope = normalize_scope(scope.clone());
877 if let Some(spec) = self.bootstrap_key_spec(key)
878 && !spec.allows_scope(&normalized_scope)
879 {
880 return Err(ConfigError::InvalidBootstrapScope {
881 key: spec.key.to_string(),
882 profile: normalized_scope.profile,
883 terminal: normalized_scope.terminal,
884 });
885 }
886
887 Ok(())
888 }
889
890 pub fn validate_bootstrap_value(
892 &self,
893 key: &str,
894 value: &ConfigValue,
895 ) -> Result<(), ConfigError> {
896 let normalized = key.trim().to_ascii_lowercase();
897 let Some(entry) = self.entries.get(&normalized) else {
898 return Ok(());
899 };
900 entry.validate_bootstrap_value(&normalized, value)
901 }
902}
903
904fn builtin_config_schema() -> &'static ConfigSchema {
905 static BUILTIN_SCHEMA: OnceLock<ConfigSchema> = OnceLock::new();
906 BUILTIN_SCHEMA.get_or_init(ConfigSchema::builtin)
907}
908
909impl Display for ConfigValue {
910 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
911 match self {
912 ConfigValue::String(v) => write!(f, "{v}"),
913 ConfigValue::Bool(v) => write!(f, "{v}"),
914 ConfigValue::Integer(v) => write!(f, "{v}"),
915 ConfigValue::Float(v) => write!(f, "{v}"),
916 ConfigValue::List(v) => {
917 let joined = v
918 .iter()
919 .map(ToString::to_string)
920 .collect::<Vec<String>>()
921 .join(",");
922 write!(f, "[{joined}]")
923 }
924 ConfigValue::Secret(secret) => write!(f, "{secret}"),
925 }
926 }
927}
928
929impl From<&str> for ConfigValue {
930 fn from(value: &str) -> Self {
931 ConfigValue::String(value.to_string())
932 }
933}
934
935impl From<String> for ConfigValue {
936 fn from(value: String) -> Self {
937 ConfigValue::String(value)
938 }
939}
940
941impl From<bool> for ConfigValue {
942 fn from(value: bool) -> Self {
943 ConfigValue::Bool(value)
944 }
945}
946
947impl From<i64> for ConfigValue {
948 fn from(value: i64) -> Self {
949 ConfigValue::Integer(value)
950 }
951}
952
953impl From<f64> for ConfigValue {
954 fn from(value: f64) -> Self {
955 ConfigValue::Float(value)
956 }
957}
958
959impl From<Vec<String>> for ConfigValue {
960 fn from(values: Vec<String>) -> Self {
961 ConfigValue::List(values.into_iter().map(ConfigValue::String).collect())
962 }
963}
964
965#[derive(Debug, Clone, Default, PartialEq, Eq)]
967pub struct Scope {
968 pub profile: Option<String>,
970 pub terminal: Option<String>,
972}
973
974impl Scope {
975 pub fn global() -> Self {
985 Self::default()
986 }
987
988 pub fn profile(profile: &str) -> Self {
1000 Self {
1001 profile: Some(normalize_identifier(profile)),
1002 terminal: None,
1003 }
1004 }
1005
1006 pub fn terminal(terminal: &str) -> Self {
1008 Self {
1009 profile: None,
1010 terminal: Some(normalize_identifier(terminal)),
1011 }
1012 }
1013
1014 pub fn profile_terminal(profile: &str, terminal: &str) -> Self {
1016 Self {
1017 profile: Some(normalize_identifier(profile)),
1018 terminal: Some(normalize_identifier(terminal)),
1019 }
1020 }
1021}
1022
1023#[derive(Debug, Clone, PartialEq)]
1025pub struct LayerEntry {
1026 pub key: String,
1028 pub value: ConfigValue,
1030 pub scope: Scope,
1032 pub origin: Option<String>,
1034}
1035
1036#[derive(Debug, Clone, Default)]
1038pub struct ConfigLayer {
1039 pub(crate) entries: Vec<LayerEntry>,
1040}
1041
1042impl ConfigLayer {
1043 pub fn entries(&self) -> &[LayerEntry] {
1045 &self.entries
1046 }
1047
1048 pub fn extend_from_layer(&mut self, other: &ConfigLayer) {
1072 self.entries.extend(other.entries().iter().cloned());
1073 }
1074
1075 pub fn set<K, V>(&mut self, key: K, value: V)
1090 where
1091 K: Into<String>,
1092 V: Into<ConfigValue>,
1093 {
1094 self.insert(key, value, Scope::global());
1095 }
1096
1097 pub fn set_for_profile<K, V>(&mut self, profile: &str, key: K, value: V)
1099 where
1100 K: Into<String>,
1101 V: Into<ConfigValue>,
1102 {
1103 self.insert(key, value, Scope::profile(profile));
1104 }
1105
1106 pub fn set_for_terminal<K, V>(&mut self, terminal: &str, key: K, value: V)
1108 where
1109 K: Into<String>,
1110 V: Into<ConfigValue>,
1111 {
1112 self.insert(key, value, Scope::terminal(terminal));
1113 }
1114
1115 pub fn set_for_profile_terminal<K, V>(
1117 &mut self,
1118 profile: &str,
1119 terminal: &str,
1120 key: K,
1121 value: V,
1122 ) where
1123 K: Into<String>,
1124 V: Into<ConfigValue>,
1125 {
1126 self.insert(key, value, Scope::profile_terminal(profile, terminal));
1127 }
1128
1129 pub fn insert<K, V>(&mut self, key: K, value: V, scope: Scope)
1131 where
1132 K: Into<String>,
1133 V: Into<ConfigValue>,
1134 {
1135 self.entries.push(LayerEntry {
1136 key: key.into(),
1137 value: value.into(),
1138 scope: normalize_scope(scope),
1139 origin: None,
1140 });
1141 }
1142
1143 pub fn insert_with_origin<K, V, O>(&mut self, key: K, value: V, scope: Scope, origin: Option<O>)
1145 where
1146 K: Into<String>,
1147 V: Into<ConfigValue>,
1148 O: Into<String>,
1149 {
1150 self.entries.push(LayerEntry {
1151 key: key.into(),
1152 value: value.into(),
1153 scope: normalize_scope(scope),
1154 origin: origin.map(Into::into),
1155 });
1156 }
1157
1158 pub fn mark_all_secret(&mut self) {
1160 for entry in &mut self.entries {
1161 if !entry.value.is_secret() {
1162 entry.value = entry.value.clone().into_secret();
1163 }
1164 }
1165 }
1166
1167 pub fn remove_scoped(&mut self, key: &str, scope: &Scope) -> Option<ConfigValue> {
1182 let normalized_scope = normalize_scope(scope.clone());
1183 let index = self
1184 .entries
1185 .iter()
1186 .rposition(|entry| entry.key == key && entry.scope == normalized_scope)?;
1187 Some(self.entries.remove(index).value)
1188 }
1189
1190 pub fn from_toml_str(raw: &str) -> Result<Self, ConfigError> {
1208 let parsed = raw
1209 .parse::<toml::Value>()
1210 .map_err(|err| ConfigError::TomlParse(err.to_string()))?;
1211
1212 let root = parsed.as_table().ok_or(ConfigError::TomlRootMustBeTable)?;
1213 let mut layer = ConfigLayer::default();
1214
1215 for (section, value) in root {
1216 match section.as_str() {
1217 "default" => {
1218 let table = value
1219 .as_table()
1220 .ok_or_else(|| ConfigError::InvalidSection {
1221 section: "default".to_string(),
1222 expected: "table".to_string(),
1223 })?;
1224 flatten_table(&mut layer, table, "", &Scope::global())?;
1225 }
1226 "profile" => {
1227 let profiles = value
1228 .as_table()
1229 .ok_or_else(|| ConfigError::InvalidSection {
1230 section: "profile".to_string(),
1231 expected: "table".to_string(),
1232 })?;
1233 for (profile, profile_table_value) in profiles {
1234 let profile_table = profile_table_value.as_table().ok_or_else(|| {
1235 ConfigError::InvalidSection {
1236 section: format!("profile.{profile}"),
1237 expected: "table".to_string(),
1238 }
1239 })?;
1240 flatten_table(&mut layer, profile_table, "", &Scope::profile(profile))?;
1241 }
1242 }
1243 "terminal" => {
1244 let terminals =
1245 value
1246 .as_table()
1247 .ok_or_else(|| ConfigError::InvalidSection {
1248 section: "terminal".to_string(),
1249 expected: "table".to_string(),
1250 })?;
1251
1252 for (terminal, terminal_table_value) in terminals {
1253 let terminal_table = terminal_table_value.as_table().ok_or_else(|| {
1254 ConfigError::InvalidSection {
1255 section: format!("terminal.{terminal}"),
1256 expected: "table".to_string(),
1257 }
1258 })?;
1259
1260 for (key, terminal_value) in terminal_table {
1261 if key == "profile" {
1262 continue;
1263 }
1264
1265 flatten_key_value(
1266 &mut layer,
1267 key,
1268 terminal_value,
1269 &Scope::terminal(terminal),
1270 )?;
1271 }
1272
1273 if let Some(profile_section) = terminal_table.get("profile") {
1274 let profile_tables = profile_section.as_table().ok_or_else(|| {
1275 ConfigError::InvalidSection {
1276 section: format!("terminal.{terminal}.profile"),
1277 expected: "table".to_string(),
1278 }
1279 })?;
1280
1281 for (profile_key, profile_value) in profile_tables {
1282 if let Some(profile_table) = profile_value.as_table() {
1283 flatten_table(
1284 &mut layer,
1285 profile_table,
1286 "",
1287 &Scope::profile_terminal(profile_key, terminal),
1288 )?;
1289 } else {
1290 flatten_key_value(
1291 &mut layer,
1292 &format!("profile.{profile_key}"),
1293 profile_value,
1294 &Scope::terminal(terminal),
1295 )?;
1296 }
1297 }
1298 }
1299 }
1300 }
1301 unknown => {
1302 return Err(ConfigError::UnknownTopLevelSection(unknown.to_string()));
1303 }
1304 }
1305 }
1306
1307 Ok(layer)
1308 }
1309
1310 pub fn from_env_iter<I, K, V>(vars: I) -> Result<Self, ConfigError>
1312 where
1313 I: IntoIterator<Item = (K, V)>,
1314 K: AsRef<str>,
1315 V: AsRef<str>,
1316 {
1317 let mut layer = ConfigLayer::default();
1318
1319 for (name, value) in vars {
1320 let key = name.as_ref();
1321 if !key.starts_with("OSP__") {
1322 continue;
1323 }
1324
1325 let spec = parse_env_key(key)?;
1326 builtin_config_schema().validate_writable_key(&spec.key)?;
1327 validate_key_scope(&spec.key, &spec.scope)?;
1328 let converted = ConfigValue::String(value.as_ref().to_string());
1329 validate_bootstrap_value(&spec.key, &converted)?;
1330 layer.insert_with_origin(spec.key, converted, spec.scope, Some(key.to_string()));
1331 }
1332
1333 Ok(layer)
1334 }
1335
1336 pub(crate) fn validate_entries(&self) -> Result<(), ConfigError> {
1337 for entry in &self.entries {
1338 builtin_config_schema().validate_writable_key(&entry.key)?;
1339 validate_key_scope(&entry.key, &entry.scope)?;
1340 validate_bootstrap_value(&entry.key, &entry.value)?;
1341 }
1342
1343 Ok(())
1344 }
1345}
1346
1347pub(crate) struct EnvKeySpec {
1348 pub(crate) key: String,
1349 pub(crate) scope: Scope,
1350}
1351
1352#[derive(Debug, Clone, Default)]
1354#[must_use]
1355pub struct ResolveOptions {
1356 pub profile_override: Option<String>,
1358 pub terminal: Option<String>,
1360}
1361
1362impl ResolveOptions {
1363 pub fn new() -> Self {
1375 Self::default()
1376 }
1377
1378 pub fn with_profile_override(mut self, profile_override: Option<String>) -> Self {
1380 self.profile_override = profile_override
1381 .map(|value| normalize_identifier(&value))
1382 .filter(|value| !value.is_empty());
1383 self
1384 }
1385
1386 pub fn with_profile(mut self, profile: &str) -> Self {
1397 self.profile_override = Some(normalize_identifier(profile));
1398 self
1399 }
1400
1401 pub fn with_terminal(mut self, terminal: &str) -> Self {
1403 self.terminal = Some(normalize_identifier(terminal));
1404 self
1405 }
1406
1407 pub fn with_terminal_override(mut self, terminal: Option<String>) -> Self {
1409 self.terminal = terminal
1410 .map(|value| normalize_identifier(&value))
1411 .filter(|value| !value.is_empty());
1412 self
1413 }
1414}
1415
1416#[derive(Debug, Clone, PartialEq)]
1418pub struct ResolvedValue {
1419 pub raw_value: ConfigValue,
1421 pub value: ConfigValue,
1423 pub source: ConfigSource,
1425 pub scope: Scope,
1427 pub origin: Option<String>,
1429}
1430
1431#[derive(Debug, Clone, PartialEq)]
1433pub struct ExplainCandidate {
1434 pub entry_index: usize,
1436 pub value: ConfigValue,
1438 pub scope: Scope,
1440 pub origin: Option<String>,
1442 pub rank: Option<u8>,
1444 pub selected_in_layer: bool,
1446}
1447
1448#[derive(Debug, Clone, PartialEq)]
1450pub struct ExplainLayer {
1451 pub source: ConfigSource,
1453 pub selected_entry_index: Option<usize>,
1455 pub candidates: Vec<ExplainCandidate>,
1457}
1458
1459#[derive(Debug, Clone, PartialEq)]
1461pub struct ExplainInterpolationStep {
1462 pub placeholder: String,
1464 pub raw_value: ConfigValue,
1466 pub value: ConfigValue,
1468 pub source: ConfigSource,
1470 pub scope: Scope,
1472 pub origin: Option<String>,
1474}
1475
1476#[derive(Debug, Clone, PartialEq)]
1478pub struct ExplainInterpolation {
1479 pub template: String,
1481 pub steps: Vec<ExplainInterpolationStep>,
1483}
1484
1485#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1487pub enum ActiveProfileSource {
1488 Override,
1490 DefaultProfile,
1492}
1493
1494impl ActiveProfileSource {
1495 pub fn as_str(self) -> &'static str {
1497 match self {
1498 Self::Override => "override",
1499 Self::DefaultProfile => "profile.default",
1500 }
1501 }
1502}
1503
1504#[derive(Debug, Clone, PartialEq)]
1506pub struct ConfigExplain {
1507 pub key: String,
1509 pub active_profile: String,
1511 pub active_profile_source: ActiveProfileSource,
1513 pub terminal: Option<String>,
1515 pub known_profiles: BTreeSet<String>,
1517 pub layers: Vec<ExplainLayer>,
1519 pub final_entry: Option<ResolvedValue>,
1521 pub interpolation: Option<ExplainInterpolation>,
1523}
1524
1525#[derive(Debug, Clone, PartialEq)]
1527pub struct BootstrapConfigExplain {
1528 pub key: String,
1530 pub active_profile: String,
1532 pub active_profile_source: ActiveProfileSource,
1534 pub terminal: Option<String>,
1536 pub known_profiles: BTreeSet<String>,
1538 pub layers: Vec<ExplainLayer>,
1540 pub final_entry: Option<ResolvedValue>,
1542}
1543
1544#[derive(Debug, Clone, PartialEq)]
1546pub struct ResolvedConfig {
1547 pub(crate) active_profile: String,
1548 pub(crate) terminal: Option<String>,
1549 pub(crate) known_profiles: BTreeSet<String>,
1550 pub(crate) values: BTreeMap<String, ResolvedValue>,
1551 pub(crate) aliases: BTreeMap<String, ResolvedValue>,
1552}
1553
1554impl ResolvedConfig {
1555 pub fn active_profile(&self) -> &str {
1557 &self.active_profile
1558 }
1559
1560 pub fn terminal(&self) -> Option<&str> {
1562 self.terminal.as_deref()
1563 }
1564
1565 pub fn known_profiles(&self) -> &BTreeSet<String> {
1567 &self.known_profiles
1568 }
1569
1570 pub fn values(&self) -> &BTreeMap<String, ResolvedValue> {
1572 &self.values
1573 }
1574
1575 pub fn aliases(&self) -> &BTreeMap<String, ResolvedValue> {
1577 &self.aliases
1578 }
1579
1580 pub fn get(&self, key: &str) -> Option<&ConfigValue> {
1582 self.values.get(key).map(|entry| &entry.value)
1583 }
1584
1585 pub fn get_string(&self, key: &str) -> Option<&str> {
1603 match self.get(key).map(ConfigValue::reveal) {
1604 Some(ConfigValue::String(value)) => Some(value),
1605 _ => None,
1606 }
1607 }
1608
1609 pub fn get_bool(&self, key: &str) -> Option<bool> {
1627 match self.get(key).map(ConfigValue::reveal) {
1628 Some(ConfigValue::Bool(value)) => Some(*value),
1629 _ => None,
1630 }
1631 }
1632
1633 pub fn get_string_list(&self, key: &str) -> Option<Vec<String>> {
1670 match self.get(key).map(ConfigValue::reveal) {
1671 Some(ConfigValue::List(values)) => Some(
1672 values
1673 .iter()
1674 .filter_map(|value| match value {
1675 ConfigValue::String(text) => Some(text.clone()),
1676 ConfigValue::Secret(secret) => match secret.expose() {
1677 ConfigValue::String(text) => Some(text.clone()),
1678 _ => None,
1679 },
1680 _ => None,
1681 })
1682 .collect(),
1683 ),
1684 Some(ConfigValue::String(value)) => Some(vec![value.clone()]),
1685 Some(ConfigValue::Secret(secret)) => match secret.expose() {
1686 ConfigValue::String(value) => Some(vec![value.clone()]),
1687 _ => None,
1688 },
1689 _ => None,
1690 }
1691 }
1692
1693 pub fn get_value_entry(&self, key: &str) -> Option<&ResolvedValue> {
1695 self.values.get(key)
1696 }
1697
1698 pub fn get_alias_entry(&self, key: &str) -> Option<&ResolvedValue> {
1700 let normalized = if key.trim().to_ascii_lowercase().starts_with("alias.") {
1701 key.trim().to_ascii_lowercase()
1702 } else {
1703 format!("alias.{}", key.trim().to_ascii_lowercase())
1704 };
1705 self.aliases.get(&normalized)
1706 }
1707}
1708
1709fn flatten_table(
1710 layer: &mut ConfigLayer,
1711 table: &toml::value::Table,
1712 prefix: &str,
1713 scope: &Scope,
1714) -> Result<(), ConfigError> {
1715 for (key, value) in table {
1716 let full_key = if prefix.is_empty() {
1717 key.to_string()
1718 } else {
1719 format!("{prefix}.{key}")
1720 };
1721
1722 flatten_key_value(layer, &full_key, value, scope)?;
1723 }
1724
1725 Ok(())
1726}
1727
1728fn flatten_key_value(
1729 layer: &mut ConfigLayer,
1730 key: &str,
1731 value: &toml::Value,
1732 scope: &Scope,
1733) -> Result<(), ConfigError> {
1734 match value {
1735 toml::Value::Table(table) => flatten_table(layer, table, key, scope),
1736 _ => {
1737 let converted = ConfigValue::from_toml(key, value)?;
1738 builtin_config_schema().validate_writable_key(key)?;
1739 validate_key_scope(key, scope)?;
1740 validate_bootstrap_value(key, &converted)?;
1741 layer.insert(key.to_string(), converted, scope.clone());
1742 Ok(())
1743 }
1744 }
1745}
1746
1747pub fn bootstrap_key_spec(key: &str) -> Option<BootstrapKeySpec> {
1749 builtin_config_schema().bootstrap_key_spec(key)
1750}
1751
1752pub fn is_bootstrap_only_key(key: &str) -> bool {
1755 bootstrap_key_spec(key).is_some_and(|spec| !spec.runtime_visible)
1756}
1757
1758pub fn is_alias_key(key: &str) -> bool {
1770 key.trim().to_ascii_lowercase().starts_with("alias.")
1771}
1772
1773pub fn validate_key_scope(key: &str, scope: &Scope) -> Result<(), ConfigError> {
1775 builtin_config_schema().validate_key_scope(key, scope)
1776}
1777
1778pub fn validate_bootstrap_value(key: &str, value: &ConfigValue) -> Result<(), ConfigError> {
1780 builtin_config_schema().validate_bootstrap_value(key, value)
1781}
1782
1783fn adapt_value_for_schema(
1784 key: &str,
1785 value: &ConfigValue,
1786 schema: &SchemaEntry,
1787) -> Result<ConfigValue, ConfigError> {
1788 let (is_secret, value) = match value {
1789 ConfigValue::Secret(secret) => (true, secret.expose()),
1790 other => (false, other),
1791 };
1792
1793 let adapted = match schema.value_type {
1794 SchemaValueType::String => match value {
1795 ConfigValue::String(value) => ConfigValue::String(value.clone()),
1796 other => {
1797 return Err(ConfigError::InvalidValueType {
1798 key: key.to_string(),
1799 expected: SchemaValueType::String,
1800 actual: value_type_name(other).to_string(),
1801 });
1802 }
1803 },
1804 SchemaValueType::Bool => match value {
1805 ConfigValue::Bool(value) => ConfigValue::Bool(*value),
1806 ConfigValue::String(value) => {
1807 ConfigValue::Bool(parse_bool(value).ok_or_else(|| {
1808 ConfigError::InvalidValueType {
1809 key: key.to_string(),
1810 expected: SchemaValueType::Bool,
1811 actual: "string".to_string(),
1812 }
1813 })?)
1814 }
1815 other => {
1816 return Err(ConfigError::InvalidValueType {
1817 key: key.to_string(),
1818 expected: SchemaValueType::Bool,
1819 actual: value_type_name(other).to_string(),
1820 });
1821 }
1822 },
1823 SchemaValueType::Integer => match value {
1824 ConfigValue::Integer(value) => ConfigValue::Integer(*value),
1825 ConfigValue::String(value) => {
1826 let parsed =
1827 value
1828 .trim()
1829 .parse::<i64>()
1830 .map_err(|_| ConfigError::InvalidValueType {
1831 key: key.to_string(),
1832 expected: SchemaValueType::Integer,
1833 actual: "string".to_string(),
1834 })?;
1835 ConfigValue::Integer(parsed)
1836 }
1837 other => {
1838 return Err(ConfigError::InvalidValueType {
1839 key: key.to_string(),
1840 expected: SchemaValueType::Integer,
1841 actual: value_type_name(other).to_string(),
1842 });
1843 }
1844 },
1845 SchemaValueType::Float => match value {
1846 ConfigValue::Float(value) => ConfigValue::Float(*value),
1847 ConfigValue::Integer(value) => ConfigValue::Float(*value as f64),
1848 ConfigValue::String(value) => {
1849 let parsed =
1850 value
1851 .trim()
1852 .parse::<f64>()
1853 .map_err(|_| ConfigError::InvalidValueType {
1854 key: key.to_string(),
1855 expected: SchemaValueType::Float,
1856 actual: "string".to_string(),
1857 })?;
1858 ConfigValue::Float(parsed)
1859 }
1860 other => {
1861 return Err(ConfigError::InvalidValueType {
1862 key: key.to_string(),
1863 expected: SchemaValueType::Float,
1864 actual: value_type_name(other).to_string(),
1865 });
1866 }
1867 },
1868 SchemaValueType::StringList => match value {
1869 ConfigValue::List(values) => {
1870 let mut out = Vec::with_capacity(values.len());
1871 for value in values {
1872 match value {
1873 ConfigValue::String(value) => out.push(ConfigValue::String(value.clone())),
1874 ConfigValue::Secret(secret) => match secret.expose() {
1875 ConfigValue::String(value) => {
1876 out.push(ConfigValue::String(value.clone()))
1877 }
1878 other => {
1879 return Err(ConfigError::InvalidValueType {
1880 key: key.to_string(),
1881 expected: SchemaValueType::StringList,
1882 actual: value_type_name(other).to_string(),
1883 });
1884 }
1885 },
1886 other => {
1887 return Err(ConfigError::InvalidValueType {
1888 key: key.to_string(),
1889 expected: SchemaValueType::StringList,
1890 actual: value_type_name(other).to_string(),
1891 });
1892 }
1893 }
1894 }
1895 ConfigValue::List(out)
1896 }
1897 ConfigValue::String(value) => {
1898 let items = parse_string_list(value);
1899 ConfigValue::List(items.into_iter().map(ConfigValue::String).collect())
1900 }
1901 ConfigValue::Secret(secret) => match secret.expose() {
1902 ConfigValue::String(value) => {
1903 let items = parse_string_list(value);
1904 ConfigValue::List(items.into_iter().map(ConfigValue::String).collect())
1905 }
1906 other => {
1907 return Err(ConfigError::InvalidValueType {
1908 key: key.to_string(),
1909 expected: SchemaValueType::StringList,
1910 actual: value_type_name(other).to_string(),
1911 });
1912 }
1913 },
1914 other => {
1915 return Err(ConfigError::InvalidValueType {
1916 key: key.to_string(),
1917 expected: SchemaValueType::StringList,
1918 actual: value_type_name(other).to_string(),
1919 });
1920 }
1921 },
1922 };
1923
1924 let adapted = if is_secret {
1925 adapted.into_secret()
1926 } else {
1927 adapted
1928 };
1929
1930 if let Some(allowed_values) = &schema.allowed_values
1931 && let ConfigValue::String(value) = adapted.reveal()
1932 {
1933 let normalized = value.to_ascii_lowercase();
1934 if !allowed_values.contains(&normalized) {
1935 return Err(ConfigError::InvalidEnumValue {
1936 key: key.to_string(),
1937 value: value.clone(),
1938 allowed: allowed_values.clone(),
1939 });
1940 }
1941 }
1942
1943 Ok(adapted)
1944}
1945
1946fn adapt_dynamic_value_for_schema(
1947 key: &str,
1948 value: &ConfigValue,
1949 kind: DynamicSchemaKeyKind,
1950) -> Result<ConfigValue, ConfigError> {
1951 let adapted = match kind {
1952 DynamicSchemaKeyKind::PluginCommandState | DynamicSchemaKeyKind::PluginCommandProvider => {
1953 adapt_value_for_schema(key, value, &SchemaEntry::string())?
1954 }
1955 };
1956
1957 if matches!(kind, DynamicSchemaKeyKind::PluginCommandState) {
1958 validate_allowed_values(key, &adapted, Some(&["enabled", "disabled"]))?;
1959 }
1960
1961 Ok(adapted)
1962}
1963
1964fn validate_allowed_values(
1965 key: &str,
1966 value: &ConfigValue,
1967 allowed: Option<&[&str]>,
1968) -> Result<(), ConfigError> {
1969 let Some(allowed) = allowed else {
1970 return Ok(());
1971 };
1972 if let ConfigValue::String(current) = value {
1973 let normalized = current.to_ascii_lowercase();
1974 if !allowed.iter().any(|candidate| *candidate == normalized) {
1975 return Err(ConfigError::InvalidEnumValue {
1976 key: key.to_string(),
1977 value: current.clone(),
1978 allowed: allowed.iter().map(|value| (*value).to_string()).collect(),
1979 });
1980 }
1981 }
1982 Ok(())
1983}
1984
1985fn dynamic_schema_key_kind(key: &str) -> Option<DynamicSchemaKeyKind> {
1986 let normalized = key.trim().to_ascii_lowercase();
1987 let remainder = normalized.strip_prefix("plugins.")?;
1988 let (command, field) = remainder.rsplit_once('.')?;
1989 if command.trim().is_empty() {
1990 return None;
1991 }
1992 match field {
1993 "state" => Some(DynamicSchemaKeyKind::PluginCommandState),
1994 "provider" => Some(DynamicSchemaKeyKind::PluginCommandProvider),
1995 _ => None,
1996 }
1997}
1998
1999fn parse_bool(value: &str) -> Option<bool> {
2000 match value.trim().to_ascii_lowercase().as_str() {
2001 "true" => Some(true),
2002 "false" => Some(false),
2003 _ => None,
2004 }
2005}
2006
2007fn parse_string_list(value: &str) -> Vec<String> {
2008 let trimmed = value.trim();
2009 if trimmed.is_empty() {
2010 return Vec::new();
2011 }
2012
2013 let inner = trimmed
2014 .strip_prefix('[')
2015 .and_then(|value| value.strip_suffix(']'))
2016 .unwrap_or(trimmed);
2017
2018 inner
2019 .split(',')
2020 .map(|value| value.trim())
2021 .filter(|value| !value.is_empty())
2022 .map(|value| {
2023 value
2024 .strip_prefix('"')
2025 .and_then(|value| value.strip_suffix('"'))
2026 .or_else(|| {
2027 value
2028 .strip_prefix('\'')
2029 .and_then(|value| value.strip_suffix('\''))
2030 })
2031 .unwrap_or(value)
2032 .to_string()
2033 })
2034 .collect()
2035}
2036
2037fn value_type_name(value: &ConfigValue) -> &'static str {
2038 match value.reveal() {
2039 ConfigValue::String(_) => "string",
2040 ConfigValue::Bool(_) => "bool",
2041 ConfigValue::Integer(_) => "integer",
2042 ConfigValue::Float(_) => "float",
2043 ConfigValue::List(_) => "list",
2044 ConfigValue::Secret(_) => "string",
2045 }
2046}
2047
2048pub(crate) fn parse_env_key(key: &str) -> Result<EnvKeySpec, ConfigError> {
2049 let Some(raw) = key.strip_prefix("OSP__") else {
2050 return Err(ConfigError::InvalidEnvOverride {
2051 key: key.to_string(),
2052 reason: "missing OSP__ prefix".to_string(),
2053 });
2054 };
2055
2056 let parts = raw
2057 .split("__")
2058 .filter(|part| !part.is_empty())
2059 .collect::<Vec<&str>>();
2060
2061 if parts.is_empty() {
2062 return Err(ConfigError::InvalidEnvOverride {
2063 key: key.to_string(),
2064 reason: "missing key path".to_string(),
2065 });
2066 }
2067
2068 let mut cursor = 0usize;
2069 let mut terminal: Option<String> = None;
2070 let mut profile: Option<String> = None;
2071
2072 while cursor < parts.len() {
2073 let part = parts[cursor];
2074 if part.eq_ignore_ascii_case("TERM") {
2075 if terminal.is_some() {
2076 return Err(ConfigError::InvalidEnvOverride {
2077 key: key.to_string(),
2078 reason: "TERM scope specified more than once".to_string(),
2079 });
2080 }
2081 let term = parts
2082 .get(cursor + 1)
2083 .ok_or_else(|| ConfigError::InvalidEnvOverride {
2084 key: key.to_string(),
2085 reason: "TERM requires a terminal name".to_string(),
2086 })?;
2087 terminal = Some(normalize_identifier(term));
2088 cursor += 2;
2089 continue;
2090 }
2091
2092 if part.eq_ignore_ascii_case("PROFILE") {
2093 if remaining_parts_are_bootstrap_profile_default(&parts[cursor..]) {
2096 break;
2097 }
2098 if profile.is_some() {
2099 return Err(ConfigError::InvalidEnvOverride {
2100 key: key.to_string(),
2101 reason: "PROFILE scope specified more than once".to_string(),
2102 });
2103 }
2104 let profile_name =
2105 parts
2106 .get(cursor + 1)
2107 .ok_or_else(|| ConfigError::InvalidEnvOverride {
2108 key: key.to_string(),
2109 reason: "PROFILE requires a profile name".to_string(),
2110 })?;
2111 profile = Some(normalize_identifier(profile_name));
2112 cursor += 2;
2113 continue;
2114 }
2115
2116 break;
2117 }
2118
2119 let key_parts = &parts[cursor..];
2120 if key_parts.is_empty() {
2121 return Err(ConfigError::InvalidEnvOverride {
2122 key: key.to_string(),
2123 reason: "missing final config key".to_string(),
2124 });
2125 }
2126
2127 let dotted_key = key_parts
2128 .iter()
2129 .map(|part| part.to_ascii_lowercase())
2130 .collect::<Vec<String>>()
2131 .join(".");
2132
2133 Ok(EnvKeySpec {
2134 key: dotted_key,
2135 scope: Scope { profile, terminal },
2136 })
2137}
2138
2139fn remaining_parts_are_bootstrap_profile_default(parts: &[&str]) -> bool {
2140 matches!(parts, [profile, default]
2141 if profile.eq_ignore_ascii_case("PROFILE")
2142 && default.eq_ignore_ascii_case("DEFAULT"))
2143}
2144
2145pub(crate) fn normalize_scope(scope: Scope) -> Scope {
2146 Scope {
2147 profile: scope
2148 .profile
2149 .as_deref()
2150 .map(normalize_identifier)
2151 .filter(|value| !value.is_empty()),
2152 terminal: scope
2153 .terminal
2154 .as_deref()
2155 .map(normalize_identifier)
2156 .filter(|value| !value.is_empty()),
2157 }
2158}
2159
2160pub(crate) fn normalize_identifier(value: &str) -> String {
2161 value.trim().to_ascii_lowercase()
2162}
2163
2164#[cfg(test)]
2165mod tests;