mirror of
https://github.com/OMGeeky/confique.git
synced 2025-12-29 15:56:00 +01:00
Add yaml::format
This commit is contained in:
31
src/format.rs
Normal file
31
src/format.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
48
src/toml.rs
48
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<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
184
src/yaml.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user