mirror of
https://github.com/OMGeeky/confique.git
synced 2026-02-23 15:38:30 +01:00
Rewrite everything to be based on derives (WIP)
This commit is contained in:
@@ -1,26 +1,18 @@
|
||||
use std::net::IpAddr;
|
||||
use confique::{Config, Partial};
|
||||
|
||||
mod config {
|
||||
use std::path::PathBuf;
|
||||
#[derive(Debug, Config)]
|
||||
struct Http {
|
||||
#[config(default = 8080)]
|
||||
port: u16,
|
||||
|
||||
confique::config! {
|
||||
#![derive_for_all(Debug, Clone)]
|
||||
#[config(default = "127.0.0.1")]
|
||||
bind: IpAddr,
|
||||
|
||||
config: {
|
||||
log: {
|
||||
/// Determines how many messages are logged. Log messages below
|
||||
/// this level are not emitted. Possible values: "trace", "debug",
|
||||
/// "info", "warn", "error" and "off".
|
||||
level: log::LevelFilter = "debug",
|
||||
|
||||
/// If this is set, log messages are also written to this file.
|
||||
#[example = "/var/log/test.log"]
|
||||
file: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
}
|
||||
foo: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
fn main() {
|
||||
|
||||
println!("{:?}", Http::from_partial(<Http as Config>::Partial::default_values()));
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ edition = "2018"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1.0"
|
||||
syn = { version = "1.0", features = ["extra-traits"] }
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
heck = "0.3.2"
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
//! Definition of the intermediate representation or AST.
|
||||
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
|
||||
|
||||
/// The parsed input to the `gen_config` macro.
|
||||
pub(crate) struct Input {
|
||||
pub(crate) root: Node,
|
||||
pub(crate) visibility: Option<TokenStream>,
|
||||
pub(crate) derive_for_all: Option<TokenStream>,
|
||||
}
|
||||
|
||||
/// One node in the tree of the configuration format. Can either be a leaf node
|
||||
/// (a string, int, float or bool value) or an internal node that contains
|
||||
/// children.
|
||||
pub(crate) enum NodeKind {
|
||||
Obj(Obj),
|
||||
Leaf(Leaf),
|
||||
}
|
||||
|
||||
pub(crate) struct Node {
|
||||
/// The doc string lines.
|
||||
pub(crate) doc: Vec<String>,
|
||||
/// Attributes that are used as specified and not interpreted by us.
|
||||
pub(crate) attrs: Vec<syn::Attribute>,
|
||||
|
||||
pub(crate) name: syn::Ident,
|
||||
pub(crate) kind: NodeKind,
|
||||
}
|
||||
|
||||
pub(crate) struct Obj {
|
||||
pub(crate) typename: Option<syn::Ident>,
|
||||
pub(crate) children: Vec<Node>,
|
||||
}
|
||||
|
||||
pub(crate) struct Leaf {
|
||||
pub(crate) ty: syn::Type,
|
||||
pub(crate) default: Option<Expr>,
|
||||
pub(crate) example: Option<Expr>,
|
||||
}
|
||||
|
||||
/// The kinds of expressions (just literals) we allow for default or example
|
||||
/// values.
|
||||
pub(crate) enum Expr {
|
||||
Str(syn::LitStr),
|
||||
Int(syn::LitInt),
|
||||
Float(syn::LitFloat),
|
||||
Bool(syn::LitBool),
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub(crate) fn typename(&self) -> Option<syn::Ident> {
|
||||
match &self.kind {
|
||||
NodeKind::Obj(Obj { typename, .. }) => {
|
||||
use heck::CamelCase;
|
||||
|
||||
let out = typename.clone().unwrap_or_else(|| {
|
||||
Ident::new(&self.name.to_string().to_camel_case(), self.name.span())
|
||||
});
|
||||
|
||||
Some(out)
|
||||
}
|
||||
NodeKind::Leaf(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
417
macro/src/gen.rs
417
macro/src/gen.rs
@@ -1,367 +1,122 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, ToTokens};
|
||||
use quote::{ToTokens, format_ident, quote};
|
||||
use syn::Ident;
|
||||
use std::fmt::{self, Write};
|
||||
|
||||
use crate::ast::{Expr, Input, Leaf, Node, NodeKind, Obj};
|
||||
use crate::ir;
|
||||
|
||||
|
||||
pub(crate) fn gen(input: Input) -> TokenStream {
|
||||
let visibility = input.visibility.clone().unwrap_or(quote! { pub(crate) });
|
||||
let toml = gen_toml(&input);
|
||||
let types = gen_types(&input, &visibility);
|
||||
pub(crate) fn gen(input: ir::Input) -> TokenStream {
|
||||
let partial_mod = gen_partial_mod(&input);
|
||||
let config_impl = gen_config_impl(&input);
|
||||
|
||||
quote! {
|
||||
#visibility const TOML_TEMPLATE: &str = #toml;
|
||||
|
||||
#types
|
||||
#config_impl
|
||||
#partial_mod
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the struct fields for both, the raw struct and the main struct.
|
||||
fn gen_struct_fields(children: &[Node], visibility: &TokenStream) -> (TokenStream, TokenStream) {
|
||||
let mut raw_fields = TokenStream::new();
|
||||
let mut main_fields = TokenStream::new();
|
||||
|
||||
for child in children {
|
||||
let name = &child.name;
|
||||
let doc = &child.doc;
|
||||
|
||||
match &child.kind {
|
||||
NodeKind::Obj(_) => {
|
||||
let child_typename = child.typename().unwrap();
|
||||
let default_path = format!("{}::empty", child_typename);
|
||||
raw_fields.extend(quote! {
|
||||
#[serde(default = #default_path)]
|
||||
#visibility #name: #child_typename,
|
||||
});
|
||||
main_fields.extend(quote! {
|
||||
#visibility #name: #child_typename,
|
||||
});
|
||||
}
|
||||
NodeKind::Leaf(Leaf { ty, .. }) => {
|
||||
let inner = as_option(&ty).unwrap_or(&ty);
|
||||
raw_fields.extend(quote! {
|
||||
#visibility #name: Option<#inner>,
|
||||
});
|
||||
main_fields.extend(quote! {
|
||||
#( #[doc = #doc] )*
|
||||
#visibility #name: #ty,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(raw_fields, main_fields)
|
||||
}
|
||||
|
||||
/// Generates the definition for `default_values`, a function associated with raw types.
|
||||
fn gen_raw_default_constructor(
|
||||
children: &[Node],
|
||||
path: &[String],
|
||||
visibility: &TokenStream,
|
||||
) -> TokenStream {
|
||||
let fields = collect_tokens(children, |node| {
|
||||
let name = &node.name;
|
||||
match &node.kind {
|
||||
NodeKind::Leaf(Leaf { default: None, .. }) => quote! { #name: None, },
|
||||
NodeKind::Leaf(Leaf { default: Some(expr), ty, .. }) => {
|
||||
// TODO: we can specialize this for certain types such that we
|
||||
// don't have to invoke serde.
|
||||
let inner_type = as_option(ty).unwrap_or(ty);
|
||||
let path = append_path(path, name);
|
||||
let msg = format!(
|
||||
"default configuration value for '{}' cannot be deserialized as '{}'",
|
||||
path,
|
||||
inner_type.to_token_stream(),
|
||||
);
|
||||
fn gen_config_impl(input: &ir::Input) -> TokenStream {
|
||||
let name = &input.name;
|
||||
let (partial_mod_name, partial_struct_name) = partial_names(&input.name);
|
||||
|
||||
let field_names = input.fields.iter().map(|f| &f.name);
|
||||
let from_exprs = input.fields.iter().map(|f| {
|
||||
let field_name = &f.name;
|
||||
match unwrap_option(&f.ty) {
|
||||
Some(_) => quote! { partial.#field_name },
|
||||
None => {
|
||||
let path = field_name.to_string();
|
||||
quote! {
|
||||
#name: Some({
|
||||
let result: Result<_, confique::serde::de::value::Error>
|
||||
= Deserialize::deserialize(#expr.into_deserializer());
|
||||
result.expect(#msg)
|
||||
}),
|
||||
}
|
||||
},
|
||||
NodeKind::Obj(_) => {
|
||||
let child_typename = node.typename().unwrap();
|
||||
quote! {
|
||||
#name: #child_typename::default_values(),
|
||||
partial.#field_name.ok_or(confique::Error::MissingValue(#path))?
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
/// Returns an instance of `Self` that contains the specified default
|
||||
/// configuration values. All fields that don't have a default value
|
||||
/// specified are `None`.
|
||||
#visibility fn default_values() -> Self {
|
||||
Self { #fields }
|
||||
}
|
||||
}
|
||||
}
|
||||
impl confique::Config for #name {
|
||||
type Partial = #partial_mod_name::#partial_struct_name;
|
||||
|
||||
/// Generates the definition for `empty`, a function associated with raw types.
|
||||
fn gen_raw_empty_constructor(children: &[Node], visibility: &TokenStream) -> TokenStream {
|
||||
let fields = collect_tokens(children, |node| {
|
||||
let name = &node.name;
|
||||
match &node.kind {
|
||||
NodeKind::Leaf(_) => quote! { #name: None, },
|
||||
NodeKind::Obj(_) => {
|
||||
let child_typename = node.typename().unwrap();
|
||||
quote! {
|
||||
#name: #child_typename::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
/// Returns an instance of `Self` where all values are `None`.
|
||||
#visibility fn empty() -> Self {
|
||||
Self { #fields }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the definition of the `overwrite_with` method on raw types.
|
||||
fn gen_raw_overwrite_with_method(children: &[Node], visibility: &TokenStream) -> TokenStream {
|
||||
let fields = collect_tokens(children, |Node { name, kind, .. }| {
|
||||
match kind {
|
||||
NodeKind::Leaf(_) => quote! { #name: other.#name.or(self.#name), },
|
||||
NodeKind::Obj(_) => quote! { #name: self.#name.overwrite_with(other.#name), },
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
// TODO: Find better name
|
||||
#visibility fn overwrite_with(self, other: Self) -> Self {
|
||||
Self { #fields }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the impl to convert from a raw type to a main type.
|
||||
fn gen_try_from_impl(typename: &Ident, children: &[Node], path: &[String]) -> TokenStream {
|
||||
let fields = collect_tokens(children, |Node { name, kind, .. }| {
|
||||
match kind {
|
||||
NodeKind::Leaf(Leaf { ty, .. }) => {
|
||||
if as_option(ty).is_some() {
|
||||
// If this value is optional, we just move it as it can never fail.
|
||||
quote! { #name: src.#name, }
|
||||
} else {
|
||||
// Otherwise, we return an error if the value hasn't been specified.
|
||||
let path = append_path(path, name);
|
||||
|
||||
quote! {
|
||||
#name: src.#name.ok_or(confique::TryFromError { path: #path })?,
|
||||
}
|
||||
}
|
||||
},
|
||||
NodeKind::Obj(_) => quote! {
|
||||
#name: std::convert::TryFrom::try_from(src.#name)?,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
impl std::convert::TryFrom<raw::#typename> for #typename {
|
||||
type Error = confique::TryFromError;
|
||||
fn try_from(src: raw::#typename) -> Result<Self, Self::Error> {
|
||||
fn from_partial(partial: Self::Partial) -> Result<Self, confique::Error> {
|
||||
Ok(Self {
|
||||
#fields
|
||||
#( #field_names: #from_exprs, )*
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn append_path(path: &[String], name: &Ident) -> String {
|
||||
if path.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{}.{}", path.join("."), name)
|
||||
}
|
||||
|
||||
/// Returns the names of the module and struct for the partial type:
|
||||
/// `(mod_name, struct_name)`.
|
||||
fn partial_names(original_name: &Ident) -> (Ident, Ident) {
|
||||
use heck::SnakeCase;
|
||||
(
|
||||
format_ident!("confique_partial_{}", original_name.to_string().to_snake_case()),
|
||||
format_ident!("Partial{}", original_name),
|
||||
)
|
||||
}
|
||||
|
||||
fn gen_types(input: &Input, visibility: &TokenStream) -> TokenStream {
|
||||
let mut raw_types = TokenStream::new();
|
||||
let mut main_types = TokenStream::new();
|
||||
fn gen_partial_mod(input: &ir::Input) -> TokenStream {
|
||||
let (mod_name, struct_name) = partial_names(&input.name);
|
||||
let visibility = &input.visibility;
|
||||
let inner_visibility = inner_visibility(&input.visibility);
|
||||
|
||||
visit(input, |node, path| {
|
||||
if let NodeKind::Obj(Obj { children, .. }) = &node.kind {
|
||||
let typename = node.typename().unwrap();
|
||||
// Prepare some tokens per field.
|
||||
let field_names = input.fields.iter().map(|f| &f.name).collect::<Vec<_>>();
|
||||
let field_types = input.fields.iter().map(|f| {
|
||||
let inner = unwrap_option(&f.ty).unwrap_or(&f.ty);
|
||||
quote! { Option<#inner> }
|
||||
});
|
||||
let defaults = input.fields.iter().map(|f| {
|
||||
match &f.default {
|
||||
None => quote! { None },
|
||||
Some(default) => {
|
||||
let msg = format!(
|
||||
"default config value for `{}::{}` cannot be deserialized",
|
||||
input.name,
|
||||
f.name,
|
||||
);
|
||||
|
||||
let (raw_fields, main_fields) = gen_struct_fields(&children, visibility);
|
||||
let raw_default_constructor = gen_raw_default_constructor(&children, path, visibility);
|
||||
let raw_empty_constructor = gen_raw_empty_constructor(&children, visibility);
|
||||
let overwrite_with_method = gen_raw_overwrite_with_method(&children, visibility);
|
||||
let try_from_impl = gen_try_from_impl(&typename, &children, path);
|
||||
|
||||
// Raw type definition
|
||||
raw_types.extend(quote! {
|
||||
#[derive(Debug, Deserialize)]
|
||||
#visibility struct #typename {
|
||||
#raw_fields
|
||||
quote! {
|
||||
Some(confique::internal::deserialize_default(#default).expect(#msg))
|
||||
}
|
||||
|
||||
impl #typename {
|
||||
#raw_default_constructor
|
||||
#raw_empty_constructor
|
||||
#overwrite_with_method
|
||||
}
|
||||
});
|
||||
|
||||
// Main type definition
|
||||
let doc = &node.doc;
|
||||
let attrs = &node.attrs;
|
||||
let derives = input.derive_for_all.clone().unwrap_or(quote! { Debug });
|
||||
|
||||
main_types.extend(quote! {
|
||||
#( #[doc = #doc] )*
|
||||
#( #attrs )*
|
||||
#[derive( #derives )]
|
||||
#visibility struct #typename {
|
||||
#main_fields
|
||||
}
|
||||
|
||||
#try_from_impl
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
/// Types where all configuration values are optional.
|
||||
///
|
||||
/// The types in this module also represent the full configuration tree,
|
||||
/// but all values are optional. That's useful for intermediate steps or
|
||||
/// "layers" of configuration sources. Imagine that the three layers:
|
||||
/// environment variables, a TOML file and the fixed default values. The
|
||||
/// only thing that matters is that required values are present after
|
||||
/// merging all sources, but each individual is allowed to lack required
|
||||
/// values.
|
||||
///
|
||||
/// These types implement `serde::Deserialize` and `Debug`.
|
||||
#visibility mod raw {
|
||||
// We have to add this blanket use to be able to refer to all the
|
||||
// types the user referred to.
|
||||
#visibility mod #mod_name {
|
||||
use super::*;
|
||||
|
||||
use confique::serde::{Deserialize, de::IntoDeserializer};
|
||||
|
||||
#raw_types
|
||||
}
|
||||
|
||||
#main_types
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Generates the TOML template file.
|
||||
fn gen_toml(input: &Input) -> String {
|
||||
/// Writes all doc comments to the file.
|
||||
fn write_doc(out: &mut String, doc: &[String]) {
|
||||
for line in doc {
|
||||
writeln!(out, "#{}", line).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds zero, one or two line breaks to make sure that there are at least
|
||||
/// two line breaks at the end of the string.
|
||||
fn add_empty_line(out: &mut String) {
|
||||
match () {
|
||||
() if out.ends_with("\n\n") => {},
|
||||
() if out.ends_with('\n') => out.push('\n'),
|
||||
_ => out.push_str("\n\n"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let mut out = String::new();
|
||||
visit(input, |Node { name, doc, kind, .. }, path| {
|
||||
match kind {
|
||||
NodeKind::Obj(_) => {
|
||||
write_doc(&mut out, doc);
|
||||
|
||||
// If a new subsection starts, we always print the header, even if not
|
||||
// strictly necessary.
|
||||
if path.is_empty() {
|
||||
add_empty_line(&mut out);
|
||||
} else {
|
||||
writeln!(out, "[{}]", path.join(".")).unwrap();
|
||||
}
|
||||
#[derive(confique::serde::Deserialize)]
|
||||
#inner_visibility struct #struct_name {
|
||||
#( #inner_visibility #field_names: #field_types, )*
|
||||
}
|
||||
|
||||
NodeKind::Leaf(Leaf { ty, default, example }) => {
|
||||
write_doc(&mut out, doc);
|
||||
|
||||
// Add note about default value or the value being required.
|
||||
match default {
|
||||
Some(default) => {
|
||||
if !doc.is_empty() {
|
||||
writeln!(out, "#").unwrap();
|
||||
}
|
||||
writeln!(out, "# Default: {}", default).unwrap();
|
||||
}
|
||||
None if as_option(ty).is_some() => {}
|
||||
None => {
|
||||
if !doc.is_empty() {
|
||||
writeln!(out, "#").unwrap();
|
||||
}
|
||||
writeln!(out, "# Required! This value must be specified.").unwrap();
|
||||
impl confique::Partial for #struct_name {
|
||||
fn empty() -> Self {
|
||||
Self {
|
||||
#( #field_names: None, )*
|
||||
}
|
||||
}
|
||||
|
||||
// Commented out key and optional example.
|
||||
match example.as_ref().or(default.as_ref()) {
|
||||
Some(example) => writeln!(out, "#{} = {}", name, example).unwrap(),
|
||||
None => writeln!(out, "#{} =", name).unwrap()
|
||||
fn default_values() -> Self {
|
||||
Self {
|
||||
#( #field_names: #defaults, )*
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
add_empty_line(&mut out);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure there is only a single trailing newline.
|
||||
while out.ends_with("\n\n") {
|
||||
out.pop();
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Visits all nodes in depth-first session (visiting the parent before its
|
||||
/// children).
|
||||
fn visit<F>(input: &Input, mut visitor: F)
|
||||
where
|
||||
F: FnMut(&Node, &[String]),
|
||||
{
|
||||
let mut stack = vec![(&input.root, vec![])];
|
||||
while let Some((node, path)) = stack.pop() {
|
||||
visitor(&node, &path);
|
||||
|
||||
if let NodeKind::Obj(Obj { children, .. }) = &node.kind {
|
||||
for child in children.iter().rev() {
|
||||
let mut child_path = path.clone();
|
||||
child_path.push(child.name.to_string());
|
||||
stack.push((child, child_path));
|
||||
fn with_fallback(self, fallback: Self) -> Self {
|
||||
Self {
|
||||
#( #field_names: self.#field_names.or(fallback.#field_names), )*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates over `it`, calling `f` for each element, collecting all returned
|
||||
/// token streams into one.
|
||||
fn collect_tokens<T>(
|
||||
it: impl IntoIterator<Item = T>,
|
||||
f: impl FnMut(T) -> TokenStream,
|
||||
) -> TokenStream {
|
||||
it.into_iter().map(f).collect()
|
||||
}
|
||||
|
||||
/// Checks if the given type is an `Option` and if so, return the inner type.
|
||||
///
|
||||
@@ -370,7 +125,7 @@ fn collect_tokens<T>(
|
||||
/// can only check if it "looks" like an `Option`. Of course, stuff can go
|
||||
/// wrong. But that's the best we can do and it's highly unlikely that someone
|
||||
/// shadows `Option`.
|
||||
fn as_option(ty: &syn::Type) -> Option<&syn::Type> {
|
||||
fn unwrap_option(ty: &syn::Type) -> Option<&syn::Type> {
|
||||
let ty = match ty {
|
||||
syn::Type::Path(path) => path,
|
||||
_ => return None,
|
||||
@@ -404,8 +159,7 @@ fn as_option(ty: &syn::Type) -> Option<&syn::Type> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ToTokens for Expr {
|
||||
impl ToTokens for ir::Expr {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
match self {
|
||||
Self::Str(lit) => lit.to_tokens(tokens),
|
||||
@@ -416,16 +170,23 @@ impl ToTokens for Expr {
|
||||
}
|
||||
}
|
||||
|
||||
// This `Display` impl is for writing into a TOML file.
|
||||
impl fmt::Display for Expr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
// TODO: not sure if `escape_debug` is really what we want here, but
|
||||
// it's working for now.
|
||||
Self::Str(lit) => write!(f, "\"{}\"", lit.value().escape_debug()),
|
||||
Self::Int(lit) => lit.fmt(f),
|
||||
Self::Float(lit) => lit.fmt(f),
|
||||
Self::Bool(lit) => lit.value.fmt(f),
|
||||
fn inner_visibility(outer: &syn::Visibility) -> TokenStream {
|
||||
match outer {
|
||||
// These visibilities can be used as they are. No adjustment needed.
|
||||
syn::Visibility::Public(_) | syn::Visibility::Crate(_) => quote! { outer },
|
||||
|
||||
// The inherited one is relative to the parent module.
|
||||
syn::Visibility::Inherited => quote! { pub(super) },
|
||||
|
||||
// If the path in the `pub(in <path>)` visibility is absolute, we can
|
||||
// use it like that as well.
|
||||
syn::Visibility::Restricted(r) if r.path.leading_colon.is_some() => quote! { outer },
|
||||
|
||||
// But in the case `pub(in <path>)` with a relative path, we have to
|
||||
// prefix `super::`.
|
||||
syn::Visibility::Restricted(r) => {
|
||||
let path = &r.path;
|
||||
quote! { pub(in super::#path) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
184
macro/src/ir.rs
Normal file
184
macro/src/ir.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
//! Definition of the intermediate representation.
|
||||
|
||||
use syn::{Error, Token, parse::{Parse, ParseStream}, spanned::Spanned};
|
||||
|
||||
|
||||
/// The parsed input to the `gen_config` macro.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Input {
|
||||
pub(crate) doc: Vec<String>,
|
||||
pub(crate) visibility: syn::Visibility,
|
||||
pub(crate) name: syn::Ident,
|
||||
pub(crate) fields: Vec<Field>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Field {
|
||||
pub(crate) doc: Vec<String>,
|
||||
pub(crate) name: syn::Ident,
|
||||
pub(crate) ty: syn::Type,
|
||||
pub(crate) default: Option<Expr>,
|
||||
|
||||
// TODO:
|
||||
// - serde attributes
|
||||
// - attributes
|
||||
// - example
|
||||
}
|
||||
|
||||
/// The kinds of expressions (just literals) we allow for default or example
|
||||
/// values.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Expr {
|
||||
Str(syn::LitStr),
|
||||
Int(syn::LitInt),
|
||||
Float(syn::LitFloat),
|
||||
Bool(syn::LitBool),
|
||||
// TODO: arrays?
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub(crate) fn from_ast(mut input: syn::DeriveInput) -> Result<Self, Error> {
|
||||
let fields = match input.data {
|
||||
syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(f), .. }) => f,
|
||||
_ => return Err(Error::new(
|
||||
input.span(),
|
||||
"`confique::Config` can only be derive for structs with named fields",
|
||||
)),
|
||||
};
|
||||
|
||||
let doc = extract_doc(&mut input.attrs);
|
||||
let fields = fields.named.into_iter()
|
||||
.map(Field::from_ast)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
|
||||
Ok(Self {
|
||||
doc,
|
||||
visibility: input.vis,
|
||||
name: input.ident,
|
||||
fields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Field {
|
||||
fn from_ast(mut field: syn::Field) -> Result<Self, Error> {
|
||||
let doc = extract_doc(&mut field.attrs);
|
||||
let attrs = extract_internal_attrs(&mut field.attrs)?;
|
||||
// TODO: check no other attributes are here
|
||||
|
||||
Ok(Self {
|
||||
doc,
|
||||
default: attrs.default,
|
||||
name: field.ident.expect("bug: expected named field"),
|
||||
ty: field.ty,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Expr {
|
||||
fn from_lit(lit: syn::Lit) -> Result<Self, Error> {
|
||||
match lit {
|
||||
syn::Lit::Str(l) => Ok(Self::Str(l)),
|
||||
syn::Lit::Int(l) => Ok(Self::Int(l)),
|
||||
syn::Lit::Float(l) => Ok(Self::Float(l)),
|
||||
syn::Lit::Bool(l) => Ok(Self::Bool(l)),
|
||||
|
||||
_ => {
|
||||
let msg = "only string, integer, float and bool literals are allowed here";
|
||||
Err(Error::new(lit.span(), msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts all doc string attributes from the list and return them as list of
|
||||
/// strings (in order).
|
||||
fn extract_doc(attrs: &mut Vec<syn::Attribute>) -> Vec<String> {
|
||||
extract_attrs(attrs, |attr| {
|
||||
match attr.parse_meta().ok()? {
|
||||
syn::Meta::NameValue(syn::MetaNameValue {
|
||||
lit: syn::Lit::Str(s),
|
||||
path,
|
||||
..
|
||||
}) if path.is_ident("doc") => Some(s.value()),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_attrs<P, O>(attrs: &mut Vec<syn::Attribute>, mut pred: P) -> Vec<O>
|
||||
where
|
||||
P: FnMut(&syn::Attribute) -> Option<O>,
|
||||
{
|
||||
// TODO: use `Vec::drain_filter` once stabilized. The current impl is O(n²).
|
||||
let mut i = 0;
|
||||
let mut out = Vec::new();
|
||||
while i < attrs.len() {
|
||||
match pred(&attrs[i]) {
|
||||
Some(v) => {
|
||||
out.push(v);
|
||||
attrs.remove(i);
|
||||
}
|
||||
None => i += 1,
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn extract_internal_attrs(
|
||||
attrs: &mut Vec<syn::Attribute>,
|
||||
) -> Result<InternalAttrs, Error> {
|
||||
let internal_attrs = extract_attrs(attrs, |attr| {
|
||||
if attr.path.is_ident("config") {
|
||||
// TODO: clone not necessary once we use drain_filter
|
||||
Some(attr.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let mut out = InternalAttrs::default();
|
||||
for attr in internal_attrs {
|
||||
match attr.parse_args::<InternalAttr>()? {
|
||||
InternalAttr::Default(expr) => {
|
||||
if out.default.is_some() {
|
||||
let msg = format!(
|
||||
"duplicate '{}' confique attribute",
|
||||
attr.path.get_ident().unwrap()
|
||||
);
|
||||
return Err(Error::new(attr.span(), msg));
|
||||
}
|
||||
|
||||
out.default = Some(expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InternalAttrs {
|
||||
default: Option<Expr>,
|
||||
}
|
||||
|
||||
enum InternalAttr {
|
||||
Default(Expr),
|
||||
}
|
||||
|
||||
impl Parse for InternalAttr {
|
||||
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
|
||||
let ident: syn::Ident = input.parse()?;
|
||||
match &*ident.to_string() {
|
||||
"default" => {
|
||||
let _: Token![=] = input.parse()?;
|
||||
let expr = Expr::from_lit(input.parse()?)?;
|
||||
Ok(Self::Default(expr))
|
||||
}
|
||||
|
||||
_ => Err(syn::Error::new(ident.span(), "unknown confique attribute")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
use proc_macro::TokenStream as TokenStream1;
|
||||
|
||||
|
||||
mod ast;
|
||||
mod gen;
|
||||
mod parse;
|
||||
mod ir;
|
||||
|
||||
|
||||
/// Defines a configuration in a special syntax. TODO: explain what this
|
||||
/// generates.
|
||||
#[proc_macro]
|
||||
#[proc_macro_derive(Config, attributes(config))]
|
||||
pub fn config(input: TokenStream1) -> TokenStream1 {
|
||||
syn::parse2::<ast::Input>(input.into())
|
||||
syn::parse2::<syn::DeriveInput>(input.into())
|
||||
.and_then(ir::Input::from_ast)
|
||||
.map(gen::gen)
|
||||
.unwrap_or_else(|e| e.to_compile_error())
|
||||
.into()
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::ToTokens;
|
||||
use syn::{
|
||||
Error, Ident,
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
spanned::Spanned,
|
||||
};
|
||||
|
||||
use crate::ast::{Expr, Input, Leaf, Node, NodeKind, Obj};
|
||||
|
||||
|
||||
|
||||
impl Parse for Input {
|
||||
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
|
||||
let mut outer_attrs = input.call(syn::Attribute::parse_inner)?;
|
||||
let visibility = extract_visibility(&mut outer_attrs)?;
|
||||
let derive_for_all = extract_single_list_attr("derive_for_all", &mut outer_attrs)?;
|
||||
assert_no_extra_attrs(&outer_attrs)?;
|
||||
|
||||
// Parse top level object.
|
||||
let root: Node = input.parse()?;
|
||||
if root.name != "config" {
|
||||
return Err(syn::Error::new(
|
||||
root.name.span(),
|
||||
"top level object must have the name 'config'",
|
||||
));
|
||||
}
|
||||
|
||||
// Make sure we have at most one trailing comma
|
||||
if input.peek(syn::Token![,]) {
|
||||
let _: syn::Token![,] = input.parse().unwrap();
|
||||
}
|
||||
|
||||
if !input.is_empty() {
|
||||
return Err(syn::Error::new(
|
||||
input.span(),
|
||||
"unexpected additional tokens (only one root element allowed)",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self { root, visibility, derive_for_all })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Parse for Node {
|
||||
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
|
||||
let mut attrs = input.call(syn::Attribute::parse_outer)?;
|
||||
let doc = extract_doc(&mut attrs)?;
|
||||
|
||||
// All nodes start with an identifier and a colon.
|
||||
let name = input.parse()?;
|
||||
let _: syn::Token![:] = input.parse()?;
|
||||
|
||||
let out = if input.lookahead1().peek(syn::token::Brace) {
|
||||
// --- A nested Internal ---
|
||||
let typename = extract_typename(&mut attrs)?;
|
||||
let forwarded_attrs = extract_attrs(&["derive"], &mut attrs);
|
||||
|
||||
let inner;
|
||||
syn::braced!(inner in input);
|
||||
let fields = inner.call(<Punctuated<_, syn::Token![,]>>::parse_terminated)?;
|
||||
|
||||
Self {
|
||||
doc,
|
||||
attrs: forwarded_attrs,
|
||||
name,
|
||||
kind: NodeKind::Obj(Obj {
|
||||
typename,
|
||||
children: fields.into_iter().collect(),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
// --- A single value ---
|
||||
|
||||
// Type is mandatory.
|
||||
let ty = input.parse()?;
|
||||
|
||||
// Optional default value.
|
||||
let default = if input.lookahead1().peek(syn::Token![=]) {
|
||||
let _: syn::Token![=] = input.parse()?;
|
||||
Some(input.parse()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Optional example value.
|
||||
let example = attrs.iter()
|
||||
.position(|attr| attr.path.is_ident("example"))
|
||||
.map(|i| {
|
||||
let attr = attrs.remove(i);
|
||||
parse_attr_value::<Expr>(attr.tokens)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
Self {
|
||||
doc,
|
||||
attrs: vec![],
|
||||
name,
|
||||
kind: NodeKind::Leaf(Leaf { ty, default, example }),
|
||||
}
|
||||
};
|
||||
|
||||
assert_no_extra_attrs(&attrs)?;
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Expr {
|
||||
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
|
||||
let lit = input.parse::<syn::Lit>()?;
|
||||
let out = match lit {
|
||||
syn::Lit::Str(l) => Self::Str(l),
|
||||
syn::Lit::Int(l) => Self::Int(l),
|
||||
syn::Lit::Float(l) => Self::Float(l),
|
||||
syn::Lit::Bool(l) => Self::Bool(l),
|
||||
|
||||
_ => {
|
||||
let msg = "only string, integer, float and bool literals are allowed here";
|
||||
return Err(Error::new(lit.span(), msg));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes sure that the given list is empty or returns an error otherwise.
|
||||
fn assert_no_extra_attrs(attrs: &[syn::Attribute]) -> Result<(), Error> {
|
||||
if let Some(attr) = attrs.get(0) {
|
||||
let msg = "unknown/unexpected/duplicate attribute in this position";
|
||||
return Err(Error::new(attr.span(), msg));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parses the tokenstream as a `T` preceeded by a `=`. This is useful for
|
||||
/// attributes of the form `#[foo = <T>]`.
|
||||
fn parse_attr_value<T: Parse>(tokens: TokenStream) -> Result<T, Error> {
|
||||
use syn::parse::Parser;
|
||||
|
||||
fn parser<T: Parse>(input: ParseStream) -> Result<T, Error> {
|
||||
let _: syn::Token![=] = input.parse()?;
|
||||
input.parse()
|
||||
}
|
||||
|
||||
parser.parse2(tokens)
|
||||
}
|
||||
|
||||
/// Extract all doc attributes from the list and return them as simple strings.
|
||||
fn extract_doc(attrs: &mut Vec<syn::Attribute>) -> Result<Vec<String>, Error> {
|
||||
let out = attrs.iter()
|
||||
.filter(|attr| attr.path.is_ident("doc"))
|
||||
.map(|attr| parse_attr_value::<syn::LitStr>(attr.tokens.clone()).map(|lit| lit.value()))
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
// I know this is algorithmically not optimal, but `drain_filter` is still
|
||||
// unstable and I can't be bothered to write the proper algorithm right now.
|
||||
attrs.retain(|attr| !attr.path.is_ident("doc"));
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Extracts all attributes with a path contained in `names`.
|
||||
fn extract_attrs(names: &[&str], attrs: &mut Vec<syn::Attribute>) -> Vec<syn::Attribute> {
|
||||
let (matches, rest) = attrs.drain(..)
|
||||
.partition(|attr| names.iter().any(|n| attr.path.is_ident(n)));
|
||||
|
||||
*attrs = rest;
|
||||
matches
|
||||
}
|
||||
|
||||
fn extract_single_attr(
|
||||
name: &str,
|
||||
attrs: &mut Vec<syn::Attribute>,
|
||||
) -> Result<Option<syn::Attribute>, Error> {
|
||||
let attr = match attrs.iter().position(|attr| attr.path.is_ident(name)) {
|
||||
None => return Ok(None),
|
||||
Some(pos) => attrs.remove(pos),
|
||||
};
|
||||
|
||||
if let Some(dupe) = attrs.iter().find(|attr| attr.path.is_ident(name)) {
|
||||
let msg = format!("duplicate `{}` attribute", name);
|
||||
return Err(Error::new(dupe.span(), msg));
|
||||
}
|
||||
|
||||
Ok(Some(attr))
|
||||
}
|
||||
|
||||
fn extract_single_name_value_attr(
|
||||
name: &str,
|
||||
attrs: &mut Vec<syn::Attribute>,
|
||||
) -> Result<Option<syn::Lit>, Error> {
|
||||
let attr = match extract_single_attr(name, attrs)? {
|
||||
None => return Ok(None),
|
||||
Some(attr) => attr,
|
||||
};
|
||||
|
||||
match attr.parse_meta()? {
|
||||
syn::Meta::NameValue(nv) => Ok(Some(nv.lit)),
|
||||
other => {
|
||||
let msg = format!(r#"expected `name = "value"` attribute syntax for `{}`"#, name);
|
||||
Err(Error::new(other.span(), msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_single_list_attr(
|
||||
name: &str,
|
||||
attrs: &mut Vec<syn::Attribute>,
|
||||
) -> Result<Option<TokenStream>, Error> {
|
||||
let attr = match extract_single_attr(name, attrs)? {
|
||||
None => return Ok(None),
|
||||
Some(attr) => attr,
|
||||
};
|
||||
|
||||
match attr.parse_meta()? {
|
||||
syn::Meta::List(list) => Ok(Some(list.nested.to_token_stream())),
|
||||
other => {
|
||||
let msg = format!(r#"expected `{}(...)` attribute syntax"#, name);
|
||||
return Err(Error::new(other.span(), msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_string_lit(lit: syn::Lit) -> Result<String, Error> {
|
||||
match lit {
|
||||
syn::Lit::Str(s) => Ok(s.value()),
|
||||
_ => Err(Error::new(lit.span(), "expected string literal")),
|
||||
}
|
||||
}
|
||||
|
||||
/// `#[visibility = "..."]`
|
||||
fn extract_visibility(attrs: &mut Vec<syn::Attribute>) -> Result<Option<TokenStream>, Error> {
|
||||
extract_single_name_value_attr("visibility", attrs)?
|
||||
.map(|v| Ok::<_, syn::Error>(assert_string_lit(v)?.parse::<TokenStream>()?))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// `#[typename = "..."]`
|
||||
fn extract_typename(attrs: &mut Vec<syn::Attribute>) -> Result<Option<Ident>, Error> {
|
||||
extract_single_name_value_attr("typename", attrs)?
|
||||
.map(|lit| {
|
||||
let span = lit.span();
|
||||
let s = assert_string_lit(lit)?;
|
||||
Ok(Ident::new(&s, span))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
11
src/internal.rs
Normal file
11
src/internal.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
//! These functions are used by the code generated by the macro, but is not
|
||||
//! intended to be used directly. None of this is covered by semver! Do not use
|
||||
//! any of this directly.
|
||||
|
||||
pub fn deserialize_default<I, O>(src: I) -> Result<O, serde::de::value::Error>
|
||||
where
|
||||
I: for<'de> serde::de::IntoDeserializer<'de>,
|
||||
O: for<'de> serde::Deserialize<'de>,
|
||||
{
|
||||
O::deserialize(src.into_deserializer())
|
||||
}
|
||||
52
src/lib.rs
52
src/lib.rs
@@ -1,35 +1,53 @@
|
||||
use std::fmt;
|
||||
|
||||
pub use confique_macro::config as config;
|
||||
use serde::Deserialize;
|
||||
|
||||
|
||||
pub use serde;
|
||||
pub use confique_macro::Config;
|
||||
|
||||
|
||||
#[cfg(feature = "doc-example")]
|
||||
pub mod example;
|
||||
// #[cfg(feature = "doc-example")]
|
||||
// pub mod example;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod internal;
|
||||
|
||||
|
||||
/// Error for the `TryFrom` conversion from raw types to the main types.
|
||||
///
|
||||
/// This error is returned when a required value is `None` in the raw type.
|
||||
#[derive(Clone)]
|
||||
pub struct TryFromError {
|
||||
/// This is only public so that macro generated code can created instances
|
||||
/// of this type.
|
||||
#[doc(hidden)]
|
||||
pub path: &'static str,
|
||||
pub trait Config: Sized {
|
||||
type Partial: Partial;
|
||||
|
||||
fn from_partial(partial: Self::Partial) -> Result<Self, Error>;
|
||||
}
|
||||
|
||||
impl fmt::Display for TryFromError {
|
||||
pub trait Partial: for<'de> Deserialize<'de> {
|
||||
fn empty() -> Self;
|
||||
fn default_values() -> Self;
|
||||
fn with_fallback(self, fallback: Self) -> Self;
|
||||
}
|
||||
|
||||
|
||||
pub enum Error {
|
||||
/// Returned by `Config::from_partial` when the partial does not contain
|
||||
/// values for all required configuration values. The string is a
|
||||
/// human-readable path to the value, e.g. `http.port`.
|
||||
MissingValue(&'static str),
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
std::write!(f, "required configuration value is missing: '{}'", self.path)
|
||||
match self {
|
||||
Self::MissingValue(path) => {
|
||||
std::write!(f, "required configuration value is missing: '{}'", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TryFromError {
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt::Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TryFromError {}
|
||||
|
||||
Reference in New Issue
Block a user