mirror of
https://github.com/OMGeeky/confique.git
synced 2025-12-29 07:46:15 +01:00
249 lines
6.8 KiB
Rust
249 lines
6.8 KiB
Rust
//! TOML specific features. This module only exists if the Cargo feature `toml`
|
|
//! is enabled.
|
|
|
|
use std::fmt::{self, Write};
|
|
|
|
use crate::{
|
|
meta::{Expr, MapKey},
|
|
template::{self, Formatter},
|
|
Config,
|
|
};
|
|
|
|
|
|
|
|
/// Options for generating a TOML template.
|
|
#[non_exhaustive]
|
|
pub struct FormatOptions {
|
|
/// Indentation for nested tables. Default: 0.
|
|
pub indent: u8,
|
|
|
|
/// Non TOML-specific options.
|
|
pub general: template::FormatOptions,
|
|
}
|
|
|
|
impl Default for FormatOptions {
|
|
fn default() -> Self {
|
|
Self {
|
|
indent: 0,
|
|
general: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Formats the configuration description as a TOML file.
|
|
///
|
|
/// This can be used to generate a template file that you can give to the users
|
|
/// of your application. It usually is a convenient to start with a correctly
|
|
/// formatted file with all possible options inside.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # use pretty_assertions::assert_eq;
|
|
/// use std::path::PathBuf;
|
|
/// use confique::{Config, toml::FormatOptions};
|
|
///
|
|
/// /// App configuration.
|
|
/// #[derive(Config)]
|
|
/// struct Conf {
|
|
/// /// The color of the app.
|
|
/// color: String,
|
|
///
|
|
/// #[config(nested)]
|
|
/// log: LogConfig,
|
|
/// }
|
|
///
|
|
/// #[derive(Config)]
|
|
/// struct LogConfig {
|
|
/// /// If set to `true`, the app will log to stdout.
|
|
/// #[config(default = true)]
|
|
/// stdout: bool,
|
|
///
|
|
/// /// If this is set, the app will write logs to the given file. Of course,
|
|
/// /// the app has to have write access to that file.
|
|
/// #[config(env = "LOG_FILE")]
|
|
/// file: Option<PathBuf>,
|
|
/// }
|
|
///
|
|
/// const EXPECTED: &str = "\
|
|
/// ## App configuration.
|
|
///
|
|
/// ## The color of the app.
|
|
/// ##
|
|
/// ## Required! This value must be specified.
|
|
/// ##color =
|
|
///
|
|
/// [log]
|
|
/// ## If set to `true`, the app will log to stdout.
|
|
/// ##
|
|
/// ## Default value: true
|
|
/// ##stdout = true
|
|
///
|
|
/// ## If this is set, the app will write logs to the given file. Of course,
|
|
/// ## the app has to have write access to that file.
|
|
/// ##
|
|
/// ## Can also be specified via environment variable `LOG_FILE`.
|
|
/// ##file =
|
|
/// ";
|
|
///
|
|
/// fn main() {
|
|
/// let toml = confique::toml::template::<Conf>(FormatOptions::default());
|
|
/// assert_eq!(toml, EXPECTED);
|
|
/// }
|
|
/// ```
|
|
pub fn template<C: Config>(options: FormatOptions) -> String {
|
|
let mut out = TomlFormatter::new(&options);
|
|
template::format::<C>(&mut out, options.general);
|
|
out.finish()
|
|
}
|
|
|
|
struct TomlFormatter {
|
|
indent: u8,
|
|
buffer: String,
|
|
stack: Vec<&'static str>,
|
|
}
|
|
|
|
impl TomlFormatter {
|
|
fn new(options: &FormatOptions) -> Self {
|
|
Self {
|
|
indent: options.indent,
|
|
buffer: String::new(),
|
|
stack: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn emit_indentation(&mut self) {
|
|
let num_spaces = self.stack.len() * self.indent as usize;
|
|
write!(self.buffer, "{: <1$}", "", num_spaces).unwrap();
|
|
}
|
|
}
|
|
|
|
impl Formatter for TomlFormatter {
|
|
type ExprPrinter = PrintExpr<'static>;
|
|
|
|
fn buffer(&mut self) -> &mut String {
|
|
&mut self.buffer
|
|
}
|
|
|
|
fn comment(&mut self, comment: impl fmt::Display) {
|
|
self.emit_indentation();
|
|
writeln!(self.buffer, "#{comment}").unwrap();
|
|
}
|
|
|
|
fn disabled_field(&mut self, name: &str, value: Option<&'static Expr>) {
|
|
match value.map(PrintExpr) {
|
|
None => self.comment(format_args!("{name} =")),
|
|
Some(v) => self.comment(format_args!("{name} = {v}")),
|
|
};
|
|
}
|
|
|
|
fn start_nested(&mut self, name: &'static str, doc: &[&'static str]) {
|
|
self.stack.push(name);
|
|
doc.iter().for_each(|doc| self.comment(doc));
|
|
self.emit_indentation();
|
|
writeln!(self.buffer, "[{}]", self.stack.join(".")).unwrap();
|
|
}
|
|
|
|
fn end_nested(&mut self) {
|
|
self.stack.pop().expect("formatter bug: stack empty");
|
|
}
|
|
|
|
fn start_main(&mut self) {
|
|
self.make_gap(1);
|
|
}
|
|
|
|
fn finish(self) -> String {
|
|
assert!(self.stack.is_empty(), "formatter bug: stack not empty");
|
|
self.buffer
|
|
}
|
|
}
|
|
|
|
/// Helper to emit `meta::Expr` into TOML.
|
|
struct PrintExpr<'a>(&'a Expr);
|
|
|
|
impl From<&'static Expr> for PrintExpr<'static> {
|
|
fn from(expr: &'static Expr) -> Self {
|
|
Self(expr)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for PrintExpr<'_> {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self.0 {
|
|
Expr::Map(entries) => {
|
|
// TODO: pretty printing of long arrays onto multiple lines?
|
|
f.write_str("{ ")?;
|
|
for (i, entry) in entries.iter().enumerate() {
|
|
if i != 0 {
|
|
f.write_str(", ")?;
|
|
}
|
|
|
|
match entry.key {
|
|
MapKey::Str(s) if is_valid_bare_key(s) => f.write_str(s)?,
|
|
_ => PrintExpr(&entry.key.into()).fmt(f)?,
|
|
}
|
|
f.write_str(" = ")?;
|
|
PrintExpr(&entry.value).fmt(f)?;
|
|
}
|
|
f.write_str(" }")?;
|
|
Ok(())
|
|
},
|
|
|
|
// All these other types can simply be serialized as is.
|
|
Expr::Str(_) | Expr::Float(_) | Expr::Integer(_) | Expr::Bool(_) | Expr::Array(_) => {
|
|
toml::to_string(&self.0)
|
|
.expect("string serialization to TOML failed")
|
|
.fmt(f)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_valid_bare_key(s: &str) -> bool {
|
|
s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use pretty_assertions::assert_str_eq;
|
|
|
|
use crate::test_utils::{self, include_format_output};
|
|
use super::{template, FormatOptions};
|
|
|
|
#[test]
|
|
fn default() {
|
|
let out = template::<test_utils::example1::Conf>(FormatOptions::default());
|
|
assert_str_eq!(&out, include_format_output!("1-default.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn no_comments() {
|
|
let mut options = FormatOptions::default();
|
|
options.general.comments = false;
|
|
let out = template::<test_utils::example1::Conf>(options);
|
|
assert_str_eq!(&out, include_format_output!("1-no-comments.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn indent_2() {
|
|
let mut options = FormatOptions::default();
|
|
options.indent = 2;
|
|
let out = template::<test_utils::example1::Conf>(options);
|
|
assert_str_eq!(&out, include_format_output!("1-indent-2.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn nested_gap_2() {
|
|
let mut options = FormatOptions::default();
|
|
options.general.nested_field_gap = 2;
|
|
let out = template::<test_utils::example1::Conf>(options);
|
|
assert_str_eq!(&out, include_format_output!("1-nested-gap-2.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn immediately_nested() {
|
|
let out = template::<test_utils::example2::Conf>(Default::default());
|
|
assert_str_eq!(&out, include_format_output!("2-default.toml"));
|
|
}
|
|
}
|