smart_config_commands/lib.rs
1//! Command-line extensions for `smart-config` library.
2//!
3//! The extensions are as follows:
4//!
5//! - [Printing help](Printer::print_help()) for configuration params with optional filtering.
6//! - [Printing a Markdown reference](Printer::print_markdown_reference()) for generated documentation.
7//! - [Debugging](Printer::print_debug()) param values and deserialization errors.
8//!
9//! All extensions are encapsulated in [`Printer`].
10//!
11//! # Examples
12//!
13//! See the crate readme or the `examples` dir for captured output samples.
14//!
15//! ## Printing help
16//!
17//! ```
18//! use smart_config::ConfigSchema;
19//! use smart_config_commands::Printer;
20//!
21//! let mut schema = ConfigSchema::default();
22//! // Add configurations to the schema...
23//!
24//! Printer::stderr().print_help(&schema, |_| true)?;
25//! # std::io::Result::Ok(())
26//! ```
27//!
28//! ## Generating Markdown reference docs
29//!
30//! ```
31//! use smart_config::ConfigSchema;
32//! use smart_config_commands::{MarkdownOptions, Printer};
33//!
34//! let mut schema = ConfigSchema::default();
35//! // Add configurations to the schema...
36//!
37//! Printer::stderr().print_markdown_reference(&schema, &MarkdownOptions::default(), |_| true)?;
38//! # std::io::Result::Ok(())
39//! ```
40//!
41//! In binaries, prefer a task-oriented command name such as `config docs`; the API name is
42//! format-specific because this method emits Markdown.
43//!
44//! ## Debugging param values
45//!
46//! ```
47//! use smart_config::{ConfigSchema, ConfigRepository};
48//! use smart_config_commands::Printer;
49//!
50//! let mut schema = ConfigSchema::default();
51//! // Add configurations to the schema...
52//! let mut repo = ConfigRepository::new(&schema);
53//! // Add sources to the repository...
54//!
55//! Printer::stderr().print_debug(&repo, |param_ref| {
56//! // Allows filtering output params
57//! param_ref.canonical_path().starts_with("test.")
58//! })?;
59//! # std::io::Result::Ok(())
60//! ```
61
62// Documentation settings
63#![doc(html_root_url = "https://docs.rs/smart-config-commands/0.4.0-pre.4")] // x-release-please-version
64// Linter settings
65#![warn(missing_docs)]
66
67use std::{
68 collections::HashSet,
69 io,
70 io::{StderrLock, StdoutLock},
71};
72
73use anstream::{AutoStream, stream::RawStream};
74use anstyle::{AnsiColor, Color, Style};
75use smart_config::{
76 ConfigRef,
77 metadata::{AliasOptions, ParamMetadata},
78};
79
80mod debug;
81mod help;
82mod markdown;
83mod schema_ref;
84mod utils;
85
86pub use self::markdown::{EnvVarOptions, MarkdownOptions};
87
88const CONFIG_PATH: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow)));
89
90/// Wrapper around an I/O writer. Will style the output with ANSI sequences if appropriate.
91///
92/// Internally, the printer is based on [`anstream`] / [`anstyle`]; see their docs to find out how styling support
93/// is detected by default. (TL;DR: based on `NO_COLOR`, `CLICOLOR_FORCE` and `CLICOLOR` env vars, and whether
94/// the output is a terminal.) If this detection doesn't work for you, you can always [create](Self::custom()) a fully custom `Printer`.
95///
96/// [`anstream`]: https://docs.rs/anstream/
97/// [`anstyle`]: https://docs.rs/anstyle/
98#[derive(Debug)]
99pub struct Printer<W: RawStream> {
100 writer: AutoStream<W>,
101}
102
103impl Printer<StdoutLock<'static>> {
104 /// Creates a printer to stdout. The stdout is locked while the printer is alive!
105 pub fn stdout() -> Self {
106 Self {
107 writer: AutoStream::auto(io::stdout()).lock(),
108 }
109 }
110}
111
112impl Printer<StderrLock<'static>> {
113 /// Creates a printer to stderr. The stderr is locked while the printer is alive!
114 pub fn stderr() -> Self {
115 Self {
116 writer: AutoStream::auto(io::stderr()).lock(),
117 }
118 }
119}
120
121impl<W: RawStream> Printer<W> {
122 /// Creates a custom printer.
123 pub fn custom(writer: AutoStream<W>) -> Self {
124 Self { writer }
125 }
126}
127
128/// Reference to a parameter on a configuration.
129#[derive(Debug, Clone, Copy)]
130#[non_exhaustive]
131pub struct ParamRef<'a> {
132 /// Reference to the configuration containing the param.
133 pub config: ConfigRef<'a>,
134 /// Param metadata.
135 pub param: &'static ParamMetadata,
136}
137
138impl<'a> ParamRef<'a> {
139 pub(crate) fn for_tag(config: ConfigRef<'a>) -> Self {
140 Self {
141 config,
142 param: config.metadata().tag.unwrap().param,
143 }
144 }
145
146 /// Returns canonical path to the param.
147 pub fn canonical_path(&self) -> String {
148 if self.config.prefix().is_empty() {
149 self.param.name.to_owned()
150 } else {
151 format!(
152 "{prefix}.{name}",
153 prefix = self.config.prefix(),
154 name = self.param.name
155 )
156 }
157 }
158
159 /// Iterates over all paths to the param.
160 pub fn all_paths(&self) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
161 // Deduplicate paths so that duplicates aren't displayed in UI; `all_paths_for_param()` doesn't do this internally.
162 let mut known_paths = HashSet::new();
163 self.config
164 .all_paths_for_param(self.param)
165 .filter(move |(name, _)| known_paths.insert(name.clone()))
166 }
167}
168
169#[cfg(doctest)]
170doc_comment::doctest!("../README.md");