Skip to main content

osp_cli/config/
mod.rs

1//! Configuration exists so the app can answer three questions consistently:
2//! what keys are legal, which source wins, and what file edits are allowed.
3//!
4//! The mental model is:
5//!
6//! - [`crate::config::LoaderPipeline`] materializes source layers from files,
7//!   environment, and in-memory overrides.
8//! - [`crate::config::ConfigResolver`] applies source precedence, scope
9//!   precedence, interpolation, and schema adaptation.
10//! - [`crate::config::RuntimeConfig`] lowers the resolved config into the
11//!   smaller runtime view used by the app and REPL.
12//! - [`crate::config::set_scoped_value_in_toml`] edits TOML-backed config files
13//!   while preserving the same schema and scope rules used at runtime.
14//!
15//! Quick starts:
16//!
17//! In-memory or test-only config resolution:
18//!
19//! ```
20//! use osp_cli::config::{ConfigLayer, LoaderPipeline, ResolveOptions, StaticLayerLoader};
21//!
22//! let mut defaults = ConfigLayer::default();
23//! defaults.set("profile.default", "default");
24//! defaults.set("theme.name", "dracula");
25//!
26//! let resolved = LoaderPipeline::new(StaticLayerLoader::new(defaults))
27//!     .resolve(ResolveOptions::new().with_terminal("cli"))
28//!     .unwrap();
29//!
30//! assert_eq!(resolved.terminal(), Some("cli"));
31//! assert_eq!(resolved.get_string("theme.name"), Some("dracula"));
32//! ```
33//!
34//! Host-style bootstrap with runtime defaults plus standard path discovery:
35//!
36//! ```no_run
37//! use osp_cli::config::{
38//!     ResolveOptions, RuntimeConfig, RuntimeConfigPaths, RuntimeDefaults,
39//!     RuntimeLoadOptions, build_runtime_pipeline,
40//! };
41//!
42//! let defaults = RuntimeDefaults::from_process_env("dracula", "osp> ").to_layer();
43//! let paths = RuntimeConfigPaths::discover();
44//! let presentation = None;
45//! let cli = None;
46//! let session = None;
47//!
48//! let resolved = build_runtime_pipeline(
49//!     defaults,
50//!     presentation,
51//!     &paths,
52//!     RuntimeLoadOptions::default(),
53//!     cli,
54//!     session,
55//! )
56//! .resolve(ResolveOptions::new().with_terminal("cli"))?;
57//!
58//! let runtime = RuntimeConfig::from_resolved(&resolved);
59//! assert_eq!(runtime.active_profile, resolved.active_profile());
60//! # Ok::<(), osp_cli::config::ConfigError>(())
61//! ```
62//!
63//! On-disk config mutation:
64//!
65//! - use [`crate::config::set_scoped_value_in_toml`] and
66//!   [`crate::config::unset_scoped_value_in_toml`] instead of editing TOML by
67//!   hand
68//! - use [`crate::config::ConfigResolver::explain_key`] when the main question
69//!   is "why did this value win?"
70//!
71//! Broad-strokes flow:
72//!
73//! ```text
74//! files / env / session overrides
75//!        │
76//!        ▼ [ LoaderPipeline ]
77//!    ConfigLayer values + scope metadata
78//!        │
79//!        ▼ [ ConfigResolver ]
80//!    precedence + interpolation + type adaptation
81//!        │
82//!        ├── ResolvedConfig  (full provenance-aware map)
83//!        ├── RuntimeConfig   (smaller runtime view used by the host)
84//!        └── config explain  (why this winner won)
85//! ```
86//!
87//! Read this module when you need to answer "where did this config value come
88//! from?", "why did this value win?", or "what writes are legal for this key?".
89//!
90//! Most callers should not be assembling bespoke config logic. The normal path
91//! is:
92//!
93//! - load layers through [`crate::config::LoaderPipeline`]
94//! - resolve once through [`crate::config::ConfigResolver`]
95//! - consume the result as [`crate::config::ResolvedConfig`] or
96//!   [`crate::config::RuntimeConfig`]
97//! - for host-style startup, prefer [`crate::config::RuntimeDefaults`],
98//!   [`crate::config::RuntimeConfigPaths`], and
99//!   [`crate::config::build_runtime_pipeline`] over hand-rolled file/env
100//!   discovery
101//!
102//! The same discipline should hold for writes. File edits belong through the
103//! store helpers here so `config set`, runtime resolution, and `config explain`
104//! stay aligned. A hand-rolled file edit path almost always creates drift.
105//!
106//! Contract:
107//!
108//! - source precedence and scope precedence are defined here, not in callers
109//! - schema validation and config-store editing should stay aligned
110//! - other modules should not hand-roll config merging or scope filtering
111
112mod bootstrap;
113mod core;
114mod defaults;
115mod error;
116mod explain;
117mod interpolate;
118mod loader;
119mod resolver;
120mod runtime;
121mod selector;
122mod store;
123
124pub use core::*;
125pub use defaults::*;
126pub use error::*;
127pub use loader::*;
128pub use resolver::*;
129pub use runtime::*;
130pub use store::*;