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 error;
115mod explain;
116mod interpolate;
117mod loader;
118mod resolver;
119mod runtime;
120mod selector;
121mod store;
122
123pub use core::*;
124pub use error::*;
125pub use loader::*;
126pub use resolver::*;
127pub use runtime::*;
128pub use store::*;