Add yaml::format

This commit is contained in:
Lukas Kalbertodt
2021-07-27 15:05:07 +02:00
parent 74ce2daf05
commit 0bc105b22d
4 changed files with 232 additions and 35 deletions

31
src/format.rs Normal file
View File

@@ -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<T>(pub(crate) Option<T>);
impl<T: fmt::Display> fmt::Display for DefaultValueComment<T> {
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),
}
}
}

View File

@@ -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;

View File

@@ -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<C: Config>(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<C: Config>(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),

184
src/yaml.rs Normal file
View File

@@ -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<PathBuf>,
/// }
///
///
/// let yaml = confique::yaml::format::<Conf>(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<C: Config>(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),
}
}
}