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");