mirror of
https://github.com/OMGeeky/confique.git
synced 2025-12-26 16:07:44 +01:00
Do major refactor of format code
This gets rid of a lot of duplicated logic that was previously copy&pasted. This commit alos: - Makes it easier to implement new formats - Gets rid of a few bugs - Makes the system more flexible (more options) - Adds a lot of additional tests
This commit is contained in:
231
src/format.rs
231
src/format.rs
@@ -1,31 +1,222 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::{Config, meta::{Meta, FieldKind, LeafKind, Expr}};
|
||||
|
||||
/// 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"),
|
||||
|
||||
/// Trait abstracting over the format differences when it comes to formatting a
|
||||
/// configuration template.
|
||||
///
|
||||
/// To implement this yourself, take a look at the existing impls for guidance.
|
||||
pub trait Formatter {
|
||||
/// A type that is used to print expressions.
|
||||
type ExprPrinter: fmt::Display + From<&'static Expr>;
|
||||
|
||||
/// Internal buffer, mainly used for `make_gap` and similar methods.
|
||||
fn buffer(&mut self) -> &mut String;
|
||||
|
||||
/// Returns internal buffer by value.
|
||||
fn finish(self) -> String;
|
||||
|
||||
/// Write a comment, e.g. `format!("#{comment}")`. Don't add a space after
|
||||
/// your comment token.
|
||||
fn comment(&mut self, comment: impl fmt::Display);
|
||||
|
||||
/// Write a commented-out field with optional value, e.g. `format!("#{name} = {value}")`.
|
||||
fn disabled_field(&mut self, name: &'static str, value: Option<&'static Expr>);
|
||||
|
||||
/// Start a nested configuration section with the given name.
|
||||
fn start_nested(&mut self, name: &'static str, doc: &[&'static str]);
|
||||
|
||||
/// End a nested configuration section.
|
||||
fn end_nested(&mut self);
|
||||
|
||||
/// Called after the global docs are written and before and fields are
|
||||
/// emitted. Default impl does nothing.
|
||||
fn start_main(&mut self) {}
|
||||
|
||||
/// Called after all fields have been emitted (basically the very end).
|
||||
/// Default impl does nothing.
|
||||
fn end_main(&mut self) {}
|
||||
|
||||
/// Emits a comment describing that this field can be loaded from the given
|
||||
/// env var. Default impl is likely sufficient.
|
||||
fn env_comment(&mut self, env_key: &'static str) {
|
||||
self.comment(format_args!(" Can also be specified via environment variable `{env_key}`."));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn assert_single_trailing_newline(out: &mut String) {
|
||||
while out.ends_with("\n\n") {
|
||||
out.pop();
|
||||
/// Emits a comment either stating that this field is required, or
|
||||
/// specifying the default value. Default impl is likely sufficient.
|
||||
fn default_or_required_comment(&mut self, default_value: Option<&'static Expr>) {
|
||||
match default_value {
|
||||
None => self.comment(format_args!(" Required! This value must be specified.")),
|
||||
Some(v) => self.comment(format_args!(" Default value: {}", Self::ExprPrinter::from(v))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DefaultValueComment<T>(pub(crate) Option<T>);
|
||||
/// Makes sure that there is a gap of at least `size` many empty lines at
|
||||
/// the end of the buffer. Does nothing when the buffer is empty.
|
||||
fn make_gap(&mut self, size: u8) {
|
||||
if !self.buffer().is_empty() {
|
||||
let num_trailing_newlines = self.buffer().chars()
|
||||
.rev()
|
||||
.take_while(|c| *c == '\n')
|
||||
.count();
|
||||
|
||||
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}"),
|
||||
let newlines_needed = (size as usize + 1).saturating_sub(num_trailing_newlines);
|
||||
let buffer = self.buffer();
|
||||
for _ in 0..newlines_needed {
|
||||
buffer.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes sure the buffer ends with a single trailing newline.
|
||||
fn assert_single_trailing_newline(&mut self) {
|
||||
let buffer = self.buffer();
|
||||
if buffer.ends_with('\n') {
|
||||
while buffer.ends_with("\n\n") {
|
||||
buffer.pop();
|
||||
}
|
||||
} else {
|
||||
buffer.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// General (non format-dependent) formatting options.
|
||||
pub struct Options {
|
||||
/// 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,
|
||||
|
||||
/// If `comments` and this field are `true`, leaf fields with `env = "FOO"`
|
||||
/// attribute will have a line like this added:
|
||||
///
|
||||
/// ```text
|
||||
/// # Can also be specified via environment variable `FOO`.
|
||||
/// ```
|
||||
///
|
||||
/// Default: `true`.
|
||||
pub env_keys: bool,
|
||||
|
||||
/// Number of lines between leaf fields. Gap between leaf and nested fields
|
||||
/// is the bigger of this and `nested_field_gap`.
|
||||
///
|
||||
/// Default: `if self.comments { 1 } else { 0 }`.
|
||||
pub leaf_field_gap: Option<u8>,
|
||||
|
||||
/// Number of lines between nested fields. Gap between leaf and nested
|
||||
/// fields is the bigger of this and `leaf_field_gap`.
|
||||
///
|
||||
/// Default: 1.
|
||||
pub nested_field_gap: u8,
|
||||
|
||||
// Potential future options:
|
||||
// - Comment out default values (`#foo = 3` vs `foo = 3`)
|
||||
// - Which docs to include from nested objects
|
||||
}
|
||||
|
||||
impl Options {
|
||||
fn leaf_field_gap(&self) -> u8 {
|
||||
self.leaf_field_gap.unwrap_or(self.comments as u8)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
comments: true,
|
||||
env_keys: true,
|
||||
leaf_field_gap: None,
|
||||
nested_field_gap: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a configuration template with the given formatter.
|
||||
///
|
||||
/// If you don't need to use a custom formatter, rather look at the `format`
|
||||
/// functions in the format-specific modules (e.g. `toml::format`,
|
||||
/// `yaml::format`).
|
||||
pub fn format<C: Config>(out: &mut impl Formatter, options: Options) {
|
||||
let meta = &C::META;
|
||||
|
||||
// Print root docs.
|
||||
if options.comments {
|
||||
meta.doc.iter().for_each(|doc| out.comment(doc));
|
||||
}
|
||||
|
||||
// Recursively format all nested objects and fields
|
||||
out.start_main();
|
||||
format_impl(out, meta, &options);
|
||||
out.end_main();
|
||||
out.assert_single_trailing_newline();
|
||||
}
|
||||
|
||||
|
||||
fn format_impl(out: &mut impl Formatter, meta: &Meta, options: &Options) {
|
||||
// Output all leaf fields first
|
||||
let leaf_fields = meta.fields.iter().filter_map(|f| match &f.kind {
|
||||
FieldKind::Leaf { kind, env } => Some((f, kind, env)),
|
||||
_ => None,
|
||||
});
|
||||
let mut emitted_anything = false;
|
||||
for (i, (field, kind, env)) in leaf_fields.enumerate() {
|
||||
emitted_anything = true;
|
||||
|
||||
if i > 0 {
|
||||
out.make_gap(options.leaf_field_gap());
|
||||
}
|
||||
|
||||
let mut emitted_something = false;
|
||||
macro_rules! empty_sep_doc_line {
|
||||
() => {
|
||||
if emitted_something {
|
||||
out.comment("");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if options.comments {
|
||||
field.doc.iter().for_each(|doc| out.comment(doc));
|
||||
emitted_something = !field.doc.is_empty();
|
||||
|
||||
if let Some(env) = env {
|
||||
empty_sep_doc_line!();
|
||||
out.env_comment(env);
|
||||
}
|
||||
}
|
||||
|
||||
match kind {
|
||||
LeafKind::Optional => out.disabled_field(field.name, None),
|
||||
LeafKind::Required { default } => {
|
||||
// Emit comment about default value or the value being required.
|
||||
if options.comments {
|
||||
empty_sep_doc_line!();
|
||||
out.default_or_required_comment(default.as_ref())
|
||||
}
|
||||
|
||||
// Emit the actual line with the name and optional value
|
||||
out.disabled_field(field.name, default.as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then all nested fields recursively
|
||||
let nested_fields = meta.fields.iter().filter_map(|f| match &f.kind {
|
||||
FieldKind::Nested { meta } => Some((f, meta)),
|
||||
_ => None,
|
||||
});
|
||||
for (field, meta) in nested_fields {
|
||||
if emitted_anything {
|
||||
out.make_gap(options.nested_field_gap);
|
||||
}
|
||||
emitted_anything = true;
|
||||
|
||||
let comments = if options.comments { field.doc } else { &[] };
|
||||
out.start_nested(&field.name, comments);
|
||||
format_impl(out, meta, options);
|
||||
out.end_nested();
|
||||
}
|
||||
}
|
||||
|
||||
44
src/test_utils/example2.rs
Normal file
44
src/test_utils/example2.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::Config;
|
||||
use crate as confique;
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
/// A sample configuration for our app.
|
||||
pub struct Conf {
|
||||
#[config(nested)]
|
||||
pub http: Http,
|
||||
}
|
||||
|
||||
/// Configuring the HTTP server of our app.
|
||||
#[derive(Debug, Config)]
|
||||
pub struct Http {
|
||||
#[config(nested)]
|
||||
pub headers: Headers,
|
||||
|
||||
#[config(nested)]
|
||||
pub log: LogConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct Headers {
|
||||
/// The header in which the reverse proxy specifies the username.
|
||||
#[config(default = "x-username")]
|
||||
pub username: String,
|
||||
|
||||
/// The header in which the reverse proxy specifies the display name.
|
||||
#[config(default = "x-display-name")]
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct LogConfig {
|
||||
/// If set to `true`, the app will log to stdout.
|
||||
#[config(default = true)]
|
||||
pub 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.
|
||||
pub file: Option<PathBuf>,
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub(crate) mod example1;
|
||||
pub(crate) mod example2;
|
||||
|
||||
|
||||
#[allow(unused_macros)]
|
||||
|
||||
205
src/toml.rs
205
src/toml.rs
@@ -5,45 +5,26 @@ use std::fmt::{self, Write};
|
||||
|
||||
use crate::{
|
||||
Config,
|
||||
format::{DefaultValueComment, add_empty_line, assert_single_trailing_newline},
|
||||
meta::{Expr, FieldKind, LeafKind, Meta},
|
||||
format::{self, Formatter},
|
||||
meta::Expr,
|
||||
};
|
||||
|
||||
|
||||
|
||||
/// Options for generating a TOML template.
|
||||
pub struct FormatOptions {
|
||||
// TODO: think about forward/backwards compatibility.
|
||||
|
||||
/// Indentation for nested tables. Default: 0.
|
||||
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,
|
||||
|
||||
/// If `comments` and this field are `true`, leaf fields with `env = "FOO"`
|
||||
/// attribute will have a line like this added:
|
||||
///
|
||||
/// ```text
|
||||
/// # Can also be specified via environment variable `FOO`.
|
||||
/// ```
|
||||
///
|
||||
/// Default: `true`.
|
||||
pub env_keys: bool,
|
||||
|
||||
// Potential future options:
|
||||
// - Comment out default values (`#foo = 3` vs `foo = 3`)
|
||||
// - Which docs to include from nested objects
|
||||
/// Non-TOML specific options.
|
||||
general: format::Options,
|
||||
}
|
||||
|
||||
impl Default for FormatOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
indent: 0,
|
||||
comments: true,
|
||||
env_keys: true,
|
||||
general: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +72,6 @@ impl Default for FormatOptions {
|
||||
/// ## Required! This value must be specified.
|
||||
/// ##color =
|
||||
///
|
||||
///
|
||||
/// [log]
|
||||
/// ## If set to `true`, the app will log to stdout.
|
||||
/// ##
|
||||
@@ -111,105 +91,69 @@ impl Default for FormatOptions {
|
||||
/// }
|
||||
/// ```
|
||||
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, vec![], &options);
|
||||
assert_single_trailing_newline(&mut out);
|
||||
|
||||
out
|
||||
let mut out = TomlFormatter::new(&options);
|
||||
format::format::<C>(&mut out, options.general);
|
||||
out.finish()
|
||||
}
|
||||
|
||||
fn format_impl(
|
||||
s: &mut String,
|
||||
meta: &Meta,
|
||||
path: Vec<&str>,
|
||||
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 = path.len().saturating_sub(1) * options.indent as usize;
|
||||
write!(s, "{: <1$}", "", indent).unwrap();
|
||||
writeln!(s, $fmt $(, $args)*).unwrap();
|
||||
}};
|
||||
}
|
||||
struct TomlFormatter {
|
||||
indent: u8,
|
||||
buffer: String,
|
||||
stack: Vec<&'static str>,
|
||||
}
|
||||
|
||||
// Output all leaf fields first
|
||||
let leaf_fields = meta.fields.iter().filter_map(|f| match &f.kind {
|
||||
FieldKind::Leaf { kind, env } => Some((f, kind, env)),
|
||||
_ => None,
|
||||
});
|
||||
for (field, kind, env) in leaf_fields {
|
||||
let mut emitted_something = false;
|
||||
macro_rules! empty_sep_doc_line {
|
||||
() => {
|
||||
if emitted_something {
|
||||
emit!("#");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if options.comments {
|
||||
field.doc.iter().for_each(|doc| emit!("#{doc}"));
|
||||
emitted_something = !field.doc.is_empty();
|
||||
|
||||
if let Some(env) = env {
|
||||
empty_sep_doc_line!();
|
||||
emit!("# Can also be specified via environment variable `{env}`.")
|
||||
}
|
||||
}
|
||||
|
||||
if let LeafKind::Required { default } = kind {
|
||||
// Emit comment about default value or the value being required
|
||||
if options.comments {
|
||||
empty_sep_doc_line!();
|
||||
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),
|
||||
}
|
||||
} else {
|
||||
emit!("#{} =", field.name);
|
||||
}
|
||||
|
||||
if options.comments {
|
||||
add_empty_line(s);
|
||||
impl TomlFormatter {
|
||||
fn new(options: &FormatOptions) -> Self {
|
||||
Self {
|
||||
indent: options.indent,
|
||||
buffer: String::new(),
|
||||
stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// Then all nested fields recursively
|
||||
let nested_fields = meta.fields.iter().filter_map(|f| match &f.kind {
|
||||
FieldKind::Nested { meta } => Some((f, meta)),
|
||||
_ => None,
|
||||
});
|
||||
for (field, meta) in nested_fields {
|
||||
emit!("");
|
||||
// add_empty_line(s);
|
||||
if options.comments {
|
||||
field.doc.iter().for_each(|doc| emit!("#{doc}"));
|
||||
}
|
||||
fn emit_indentation(&mut self) {
|
||||
let num_spaces = self.stack.len() * self.indent as usize;
|
||||
write!(self.buffer, "{: <1$}", "", num_spaces).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let child_path = path.iter().copied().chain([field.name]).collect::<Vec<_>>();
|
||||
emit!("[{}]", child_path.join("."));
|
||||
format_impl(s, meta, child_path, options);
|
||||
impl Formatter for TomlFormatter {
|
||||
type ExprPrinter = PrintExpr;
|
||||
|
||||
if options.comments {
|
||||
add_empty_line(s);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,10 +188,31 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn no_comments() {
|
||||
let out = format::<test_utils::example1::Conf>(FormatOptions {
|
||||
comments: false,
|
||||
.. FormatOptions::default()
|
||||
});
|
||||
let mut options = FormatOptions::default();
|
||||
options.general.comments = false;
|
||||
let out = format::<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 = format::<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 = format::<test_utils::example1::Conf>(options);
|
||||
assert_str_eq!(&out, include_format_output!("1-nested-gap-2.toml"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn immediately_nested() {
|
||||
let out = format::<test_utils::example2::Conf>(Default::default());
|
||||
assert_str_eq!(&out, include_format_output!("2-default.toml"));
|
||||
}
|
||||
}
|
||||
|
||||
169
src/yaml.rs
169
src/yaml.rs
@@ -5,45 +5,26 @@ use std::fmt::{self, Write};
|
||||
|
||||
use crate::{
|
||||
Config,
|
||||
format::{DefaultValueComment, add_empty_line, assert_single_trailing_newline},
|
||||
meta::{Expr, FieldKind, LeafKind, Meta},
|
||||
format::{self, Formatter},
|
||||
meta::Expr,
|
||||
};
|
||||
|
||||
|
||||
|
||||
/// 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,
|
||||
|
||||
/// If `comments` and this field are `true`, leaf fields with `env = "FOO"`
|
||||
/// attribute will have a line like this added:
|
||||
///
|
||||
/// ```text
|
||||
/// # Can also be specified via environment variable `FOO`.
|
||||
/// ```
|
||||
///
|
||||
/// Default: `true`.
|
||||
pub env_keys: bool,
|
||||
|
||||
// Potential future options:
|
||||
// - Comment out default values (`#foo = 3` vs `foo = 3`)
|
||||
// - Which docs to include from nested objects
|
||||
/// Non-TOML specific options.
|
||||
general: format::Options,
|
||||
}
|
||||
|
||||
impl Default for FormatOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
indent: 2,
|
||||
comments: true,
|
||||
env_keys: true,
|
||||
general: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,92 +93,81 @@ impl Default for FormatOptions {
|
||||
/// }
|
||||
/// ```
|
||||
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
|
||||
let mut out = YamlFormatter::new(&options);
|
||||
format::format::<C>(&mut out, options.general);
|
||||
out.finish()
|
||||
}
|
||||
|
||||
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();
|
||||
}};
|
||||
struct YamlFormatter {
|
||||
indent: u8,
|
||||
buffer: String,
|
||||
depth: u8,
|
||||
}
|
||||
|
||||
impl YamlFormatter {
|
||||
fn new(options: &FormatOptions) -> Self {
|
||||
Self {
|
||||
indent: options.indent,
|
||||
buffer: String::new(),
|
||||
depth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
for field in meta.fields {
|
||||
let mut emitted_something = false;
|
||||
macro_rules! empty_sep_doc_line {
|
||||
() => {
|
||||
if emitted_something {
|
||||
emit!("#");
|
||||
}
|
||||
};
|
||||
}
|
||||
fn emit_indentation(&mut self) {
|
||||
let num_spaces = self.depth as usize * self.indent as usize;
|
||||
write!(self.buffer, "{: <1$}", "", num_spaces).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if options.comments {
|
||||
field.doc.iter().for_each(|doc| emit!("#{doc}"));
|
||||
emitted_something = !field.doc.is_empty();
|
||||
impl format::Formatter for YamlFormatter {
|
||||
type ExprPrinter = PrintExpr;
|
||||
|
||||
if let FieldKind::Leaf { env: Some(env), .. } = field.kind {
|
||||
empty_sep_doc_line!();
|
||||
emit!("# Can also be specified via environment variable `{env}`.")
|
||||
}
|
||||
}
|
||||
fn buffer(&mut self) -> &mut String {
|
||||
&mut self.buffer
|
||||
}
|
||||
|
||||
match &field.kind {
|
||||
FieldKind::Leaf { kind: LeafKind::Required { default }, .. } => {
|
||||
// Emit comment about default value or the value being required
|
||||
if options.comments {
|
||||
empty_sep_doc_line!();
|
||||
emit!("# {}", DefaultValueComment(default.as_ref().map(PrintExpr)));
|
||||
}
|
||||
fn comment(&mut self, comment: impl fmt::Display) {
|
||||
self.emit_indentation();
|
||||
writeln!(self.buffer, "#{comment}").unwrap();
|
||||
}
|
||||
|
||||
// Emit the actual line with the name and optional value
|
||||
match default {
|
||||
Some(v) => emit!("#{}: {}", field.name, PrintExpr(v)),
|
||||
None => emit!("#{}:", field.name),
|
||||
}
|
||||
}
|
||||
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}")),
|
||||
};
|
||||
}
|
||||
|
||||
FieldKind::Leaf { kind: LeafKind::Optional, .. } => emit!("#{}:", field.name),
|
||||
fn start_nested(&mut self, name: &'static str, doc: &[&'static str]) {
|
||||
doc.iter().for_each(|doc| self.comment(doc));
|
||||
self.emit_indentation();
|
||||
writeln!(self.buffer, "{name}:").unwrap();
|
||||
self.depth += 1;
|
||||
}
|
||||
|
||||
FieldKind::Nested { meta } => {
|
||||
emit!("{}:", field.name);
|
||||
format_impl(s, meta, depth + 1, options);
|
||||
}
|
||||
}
|
||||
fn end_nested(&mut self) {
|
||||
self.depth = self.depth.checked_sub(1).expect("formatter bug: ended too many nested");
|
||||
}
|
||||
|
||||
if options.comments {
|
||||
add_empty_line(s);
|
||||
}
|
||||
fn start_main(&mut self) {
|
||||
self.make_gap(1);
|
||||
}
|
||||
|
||||
fn finish(self) -> String {
|
||||
assert_eq!(self.depth, 0, "formatter bug: lingering nested objects");
|
||||
self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to emit `meta::Expr` into YAML.
|
||||
struct PrintExpr(&'static Expr);
|
||||
|
||||
impl From<&'static Expr> for PrintExpr {
|
||||
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 {
|
||||
@@ -247,10 +217,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn no_comments() {
|
||||
let out = format::<test_utils::example1::Conf>(FormatOptions {
|
||||
comments: false,
|
||||
.. FormatOptions::default()
|
||||
});
|
||||
let mut options = FormatOptions::default();
|
||||
options.general.comments = false;
|
||||
let out = format::<test_utils::example1::Conf>(options);
|
||||
assert_str_eq!(&out, include_format_output!("1-no-comments.yaml"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn immediately_nested() {
|
||||
let out = format::<test_utils::example2::Conf>(Default::default());
|
||||
assert_str_eq!(&out, include_format_output!("2-default.yaml"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
# Required! This value must be specified.
|
||||
#site_name =
|
||||
|
||||
|
||||
# Configurations related to the HTTP communication.
|
||||
[http]
|
||||
# The port the server will listen on.
|
||||
@@ -21,7 +20,6 @@
|
||||
# Default value: "127.0.0.1"
|
||||
#bind = "127.0.0.1"
|
||||
|
||||
|
||||
[http.headers]
|
||||
# The header in which the reverse proxy specifies the username.
|
||||
#
|
||||
@@ -33,7 +31,6 @@
|
||||
# Default value: "x-display-name"
|
||||
#display_name = "x-display-name"
|
||||
|
||||
|
||||
# Configuring the logging.
|
||||
[log]
|
||||
# If set to `true`, the app will log to stdout.
|
||||
|
||||
@@ -14,6 +14,12 @@ http:
|
||||
# Required! This value must be specified.
|
||||
#port:
|
||||
|
||||
# The bind address of the server. Can be set to `0.0.0.0` for example, to
|
||||
# allow other users of the network to access the server.
|
||||
#
|
||||
# Default value: 127.0.0.1
|
||||
#bind: 127.0.0.1
|
||||
|
||||
headers:
|
||||
# The header in which the reverse proxy specifies the username.
|
||||
#
|
||||
@@ -25,12 +31,6 @@ http:
|
||||
# Default value: x-display-name
|
||||
#display_name: x-display-name
|
||||
|
||||
# The bind address of the server. Can be set to `0.0.0.0` for example, to
|
||||
# allow other users of the network to access the server.
|
||||
#
|
||||
# Default value: 127.0.0.1
|
||||
#bind: 127.0.0.1
|
||||
|
||||
# Configuring the logging.
|
||||
log:
|
||||
# If set to `true`, the app will log to stdout.
|
||||
|
||||
43
tests/format-output/1-indent-2.toml
Normal file
43
tests/format-output/1-indent-2.toml
Normal file
@@ -0,0 +1,43 @@
|
||||
# A sample configuration for our app.
|
||||
|
||||
# Name of the website.
|
||||
#
|
||||
# Required! This value must be specified.
|
||||
#site_name =
|
||||
|
||||
# Configurations related to the HTTP communication.
|
||||
[http]
|
||||
# The port the server will listen on.
|
||||
#
|
||||
# Can also be specified via environment variable `PORT`.
|
||||
#
|
||||
# Required! This value must be specified.
|
||||
#port =
|
||||
|
||||
# The bind address of the server. Can be set to `0.0.0.0` for example, to
|
||||
# allow other users of the network to access the server.
|
||||
#
|
||||
# Default value: "127.0.0.1"
|
||||
#bind = "127.0.0.1"
|
||||
|
||||
[http.headers]
|
||||
# The header in which the reverse proxy specifies the username.
|
||||
#
|
||||
# Default value: "x-username"
|
||||
#username = "x-username"
|
||||
|
||||
# The header in which the reverse proxy specifies the display name.
|
||||
#
|
||||
# Default value: "x-display-name"
|
||||
#display_name = "x-display-name"
|
||||
|
||||
# Configuring the logging.
|
||||
[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.
|
||||
#file =
|
||||
46
tests/format-output/1-nested-gap-2.toml
Normal file
46
tests/format-output/1-nested-gap-2.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
# A sample configuration for our app.
|
||||
|
||||
# Name of the website.
|
||||
#
|
||||
# Required! This value must be specified.
|
||||
#site_name =
|
||||
|
||||
|
||||
# Configurations related to the HTTP communication.
|
||||
[http]
|
||||
# The port the server will listen on.
|
||||
#
|
||||
# Can also be specified via environment variable `PORT`.
|
||||
#
|
||||
# Required! This value must be specified.
|
||||
#port =
|
||||
|
||||
# The bind address of the server. Can be set to `0.0.0.0` for example, to
|
||||
# allow other users of the network to access the server.
|
||||
#
|
||||
# Default value: "127.0.0.1"
|
||||
#bind = "127.0.0.1"
|
||||
|
||||
|
||||
[http.headers]
|
||||
# The header in which the reverse proxy specifies the username.
|
||||
#
|
||||
# Default value: "x-username"
|
||||
#username = "x-username"
|
||||
|
||||
# The header in which the reverse proxy specifies the display name.
|
||||
#
|
||||
# Default value: "x-display-name"
|
||||
#display_name = "x-display-name"
|
||||
|
||||
|
||||
# Configuring the logging.
|
||||
[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.
|
||||
#file =
|
||||
@@ -1,10 +1,13 @@
|
||||
#site_name:
|
||||
|
||||
http:
|
||||
#port:
|
||||
#bind: 127.0.0.1
|
||||
|
||||
headers:
|
||||
#username: x-username
|
||||
#display_name: x-display-name
|
||||
#bind: 127.0.0.1
|
||||
|
||||
log:
|
||||
#stdout: true
|
||||
#file:
|
||||
|
||||
23
tests/format-output/2-default.toml
Normal file
23
tests/format-output/2-default.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
# A sample configuration for our app.
|
||||
|
||||
[http]
|
||||
[http.headers]
|
||||
# The header in which the reverse proxy specifies the username.
|
||||
#
|
||||
# Default value: "x-username"
|
||||
#username = "x-username"
|
||||
|
||||
# The header in which the reverse proxy specifies the display name.
|
||||
#
|
||||
# Default value: "x-display-name"
|
||||
#display_name = "x-display-name"
|
||||
|
||||
[http.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.
|
||||
#file =
|
||||
23
tests/format-output/2-default.yaml
Normal file
23
tests/format-output/2-default.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
# A sample configuration for our app.
|
||||
|
||||
http:
|
||||
headers:
|
||||
# The header in which the reverse proxy specifies the username.
|
||||
#
|
||||
# Default value: x-username
|
||||
#username: x-username
|
||||
|
||||
# The header in which the reverse proxy specifies the display name.
|
||||
#
|
||||
# Default value: x-display-name
|
||||
#display_name: x-display-name
|
||||
|
||||
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.
|
||||
#file:
|
||||
Reference in New Issue
Block a user