From 0bc105b22dfdb1092abb6b02ce6451c580e61ff2 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Tue, 27 Jul 2021 15:05:07 +0200 Subject: [PATCH] Add `yaml::format` --- src/format.rs | 31 +++++++++ src/lib.rs | 4 ++ src/toml.rs | 48 ++++--------- src/yaml.rs | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 232 insertions(+), 35 deletions(-) create mode 100644 src/format.rs create mode 100644 src/yaml.rs diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 0000000..a3c4b8d --- /dev/null +++ b/src/format.rs @@ -0,0 +1,31 @@ +use std::fmt; + + +/// Adds zero, one or two line breaks to make sure that there are at least two +/// line breaks at the end of the string. Except if the buffer is completely +/// empty, in which case it is not modified. +pub(crate) fn add_empty_line(out: &mut String) { + match () { + () if out.is_empty() => {}, + () if out.ends_with("\n\n") => {}, + () if out.ends_with('\n') => out.push('\n'), + _ => out.push_str("\n\n"), + } +} + +pub(crate) fn assert_single_trailing_newline(out: &mut String) { + while out.ends_with("\n\n") { + out.pop(); + } +} + +pub(crate) struct DefaultValueComment(pub(crate) Option); + +impl fmt::Display for DefaultValueComment { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + None => "Required! This value must be specified.".fmt(f), + Some(v) => write!(f, "Default value: {}", v), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 24b31de..8fc0902 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,11 +9,15 @@ mod builder; mod env; mod error; mod file; +mod format; pub mod meta; #[cfg(feature = "toml")] pub mod toml; +#[cfg(feature = "yaml")] +pub mod yaml; + pub use serde; pub use confique_macro::Config; diff --git a/src/toml.rs b/src/toml.rs index a3a7668..763b366 100644 --- a/src/toml.rs +++ b/src/toml.rs @@ -3,7 +3,11 @@ use std::fmt::{self, Write}; -use crate::{Config, meta::{Expr, FieldKind, LeafKind, Meta}}; +use crate::{ + Config, + format::{DefaultValueComment, add_empty_line, assert_single_trailing_newline}, + meta::{Expr, FieldKind, LeafKind, Meta}, +}; @@ -11,7 +15,7 @@ use crate::{Config, meta::{Expr, FieldKind, LeafKind, Meta}}; pub struct FormatOptions { // TODO: think about forward/backwards compatibility. - /// Indentation if nested tables. Default: 0. + /// Indentation for nested tables. Default: 0. pub indent: u8, /// Whether to include doc comments (with your own text and information @@ -93,9 +97,7 @@ pub fn format(options: FormatOptions) -> String { // Print root docs. if options.comments { - for doc in meta.doc { - writeln!(out, "#{}", doc).unwrap(); - } + meta.doc.iter().for_each(|doc| writeln!(out, "#{}", doc).unwrap()); if !meta.doc.is_empty() { add_empty_line(&mut out); } @@ -103,11 +105,7 @@ pub fn format(options: FormatOptions) -> String { // Recursively format all nested objects and fields format_impl(&mut out, meta, vec![], &options); - - // Make sure there is only a single trailing newline. - while out.ends_with("\n\n") { - out.pop(); - } + assert_single_trailing_newline(&mut out); out } @@ -135,26 +133,17 @@ fn format_impl( for field in meta.fields { if options.comments { - for doc in field.doc { - emit!("#{}", doc); - } + field.doc.iter().for_each(|doc| emit!("#{}", doc)); } match &field.kind { FieldKind::Leaf { kind: LeafKind::Required { default }, .. } => { // Emit comment about default value or the value being required if options.comments { - if let Some(v) = default { - if !field.doc.is_empty() { - emit!("#"); - } - emit!("# Default value: {}", PrintExpr(v)); - } else { - if !field.doc.is_empty() { - emit!("#"); - } - emit!("# Required! This value must be specified."); + if !field.doc.is_empty() { + emit!("#"); } + emit!("# {}", DefaultValueComment(default.as_ref().map(PrintExpr))); } // Emit the actual line with the name and optional value @@ -178,24 +167,13 @@ fn format_impl( } } -/// Adds zero, one or two line breaks to make sure that there are at least two -/// line breaks at the end of the string. Except if the buffer is completely -/// empty, in which case it is not modified. -fn add_empty_line(out: &mut String) { - match () { - () if out.is_empty() => {}, - () if out.ends_with("\n\n") => {}, - () if out.ends_with('\n') => out.push('\n'), - _ => out.push_str("\n\n"), - } -} - /// Helper to emit `meta::Expr` into TOML. struct PrintExpr(&'static Expr); impl fmt::Display for PrintExpr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { + // TODO: escape stuff! Expr::Str(v) => write!(f, "\"{}\"", v), Expr::Float(v) => v.fmt(f), Expr::Integer(v) => v.fmt(f), diff --git a/src/yaml.rs b/src/yaml.rs new file mode 100644 index 0000000..9c7885d --- /dev/null +++ b/src/yaml.rs @@ -0,0 +1,184 @@ +use std::fmt::{self, Write}; + +use crate::{ + Config, + format::{DefaultValueComment, add_empty_line, assert_single_trailing_newline}, + meta::{Expr, FieldKind, LeafKind, Meta}, +}; + + + +/// Options for generating a YAML template. +pub struct FormatOptions { + // TODO: think about forward/backwards compatibility. + + /// Amount of indentation in spaces. Default: 2. + pub indent: u8, + + /// Whether to include doc comments (with your own text and information + /// about whether a value is required and/or has a default). Default: + /// true. + pub comments: bool, + + // Potential future options: + // - Comment out default values (`#foo = 3` vs `foo = 3`) + // - Which docs to include from nested objects +} + +impl Default for FormatOptions { + fn default() -> Self { + Self { + indent: 2, + comments: true, + } + } +} + + +/// Formats the configuration description as a YAML 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 std::path::PathBuf; +/// use confique::{Config, yaml::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. +/// file: Option, +/// } +/// +/// +/// let yaml = confique::yaml::format::(FormatOptions::default()); +/// assert_eq!(yaml, concat!( +/// "# App configuration.\n", +/// "\n", +/// "# The color of the app.\n", +/// "#\n", +/// "# Required! This value must be specified.\n", +/// "#color:\n", +/// "\n", +/// "log:\n", +/// " # If set to `true`, the app will log to stdout.\n", +/// " #\n", +/// " # Default value: true\n", +/// " #stdout: true\n", +/// "\n", +/// " # If this is set, the app will write logs to the given file. Of course,\n", +/// " # the app has to have write access to that file.\n", +/// " #file:\n", +/// )); +/// ``` +pub fn format(options: FormatOptions) -> String { + let mut out = String::new(); + let meta = &C::META; + + // Print root docs. + if options.comments { + meta.doc.iter().for_each(|doc| writeln!(out, "#{}", doc).unwrap()); + if !meta.doc.is_empty() { + add_empty_line(&mut out); + } + } + + // Recursively format all nested objects and fields + format_impl(&mut out, meta, 0, &options); + assert_single_trailing_newline(&mut out); + + out +} + +fn format_impl( + s: &mut String, + meta: &Meta, + depth: usize, + options: &FormatOptions, +) { + /// Like `println!` but into `s` and with indentation. + macro_rules! emit { + ($fmt:literal $(, $args:expr)* $(,)?) => {{ + // Writing to a string never fails, we can unwrap. + let indent = depth * options.indent as usize; + write!(s, "{: <1$}", "", indent).unwrap(); + writeln!(s, $fmt $(, $args)*).unwrap(); + }}; + } + + for field in meta.fields { + if options.comments { + field.doc.iter().for_each(|doc| emit!("#{}", doc)); + } + + match &field.kind { + FieldKind::Leaf { kind: LeafKind::Required { default }, .. } => { + // Emit comment about default value or the value being required + if options.comments { + if !field.doc.is_empty() { + emit!("#"); + } + emit!("# {}", DefaultValueComment(default.as_ref().map(PrintExpr))); + } + + // Emit the actual line with the name and optional value + match default { + Some(v) => emit!("#{}: {}", field.name, PrintExpr(v)), + None => emit!("#{}:", field.name), + } + } + + FieldKind::Leaf { kind: LeafKind::Optional, .. } => emit!("#{}:", field.name), + + FieldKind::Nested { meta } => { + emit!("{}:", field.name); + format_impl(s, meta, depth + 1, options); + } + } + + if options.comments { + add_empty_line(s); + } + } +} + +/// Helper to emit `meta::Expr` into YAML. +struct PrintExpr(&'static Expr); + +impl fmt::Display for PrintExpr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self.0 { + Expr::Str(v) => { + // This is a bit ugly. Sadly, no YAML crate in our dependency + // tree has an API to serialize a string only, without emitting + // the `---` at the start of the document. But instead of + // implementing the quoting logic ourselves (which is really + // complicated as it turns out!), we use this hack. + let value = serde_yaml::Value::String(v.to_owned()); + let serialized = serde_yaml::to_string(&value).unwrap(); + serialized[4..].fmt(f) + }, + Expr::Float(v) => v.fmt(f), + Expr::Integer(v) => v.fmt(f), + Expr::Bool(v) => v.fmt(f), + } + } +}