mirror of
https://github.com/OMGeeky/confique.git
synced 2026-02-02 06:21:16 +01:00
Add version from other project
This was mostly what I implemented as utility library for another project. But I figured I can also extract it as it's useful on its own.
This commit is contained in:
@@ -4,6 +4,9 @@ version = "0.1.0"
|
||||
authors = ["Lukas Kalbertodt <lukas.kalbertodt@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
confique-macro = { path = "macro" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
log = { version = "0.4", features = ["serde", "std"] }
|
||||
|
||||
22
examples/simple.rs
Normal file
22
examples/simple.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
mod config {
|
||||
use std::path::PathBuf;
|
||||
|
||||
confique::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/tobira.log"]
|
||||
file: Option<PathBuf>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn main() {
|
||||
|
||||
}
|
||||
14
macro/Cargo.toml
Normal file
14
macro/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "confique-macro"
|
||||
version = "0.0.1"
|
||||
authors = ["Lukas Kalbertodt <lukas.kalbertodt@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1.0"
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
heck = "0.3.2"
|
||||
42
macro/src/ast.rs
Normal file
42
macro/src/ast.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
//! Definition of the intermediate representation or AST.
|
||||
|
||||
/// The parsed input to the `gen_config` macro.
|
||||
pub(crate) struct Input {
|
||||
pub(crate) root: Node,
|
||||
}
|
||||
|
||||
/// 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 Node {
|
||||
Internal {
|
||||
doc: Vec<String>,
|
||||
name: syn::Ident,
|
||||
children: Vec<Node>,
|
||||
},
|
||||
Leaf {
|
||||
doc: Vec<String>,
|
||||
name: syn::Ident,
|
||||
ty: syn::Type,
|
||||
default: Option<Expr>,
|
||||
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 name(&self) -> &syn::Ident {
|
||||
match self {
|
||||
Self::Internal { name, .. } => name,
|
||||
Self::Leaf { name, .. } => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
394
macro/src/gen.rs
Normal file
394
macro/src/gen.rs
Normal file
@@ -0,0 +1,394 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::Ident;
|
||||
use std::fmt::{self, Write};
|
||||
|
||||
use crate::ast::{Expr, Input, Node};
|
||||
|
||||
|
||||
pub(crate) fn gen(input: Input) -> TokenStream {
|
||||
let visibility = quote! { pub(crate) };
|
||||
let toml = gen_toml(&input);
|
||||
let root_mod = gen_root_mod(&input, &visibility);
|
||||
let raw_mod = gen_raw_mod(&input, &visibility);
|
||||
let util_mod = gen_util_mod(&visibility);
|
||||
|
||||
quote! {
|
||||
const TOML_TEMPLATE: &str = #toml;
|
||||
|
||||
#root_mod
|
||||
#raw_mod
|
||||
#util_mod
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_util_mod(visibility: &TokenStream) -> TokenStream {
|
||||
quote! {
|
||||
mod util {
|
||||
use std::fmt::{self, Write};
|
||||
|
||||
#[derive(Debug)]
|
||||
#visibility struct TryFromError {
|
||||
#visibility path: &'static str,
|
||||
}
|
||||
|
||||
impl fmt::Display for TryFromError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
std::write!(f, "required configuration value is missing: '{}'", self.path)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TryFromError {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_raw_mod(input: &Input, visibility: &TokenStream) -> TokenStream {
|
||||
let mut contents = TokenStream::new();
|
||||
visit(input, |node, path| {
|
||||
if let Node::Internal { name, children, .. } = node {
|
||||
let type_name = to_camel_case(name);
|
||||
|
||||
let raw_fields = collect_tokens(children, |node| {
|
||||
match node {
|
||||
Node::Leaf { name, ty, .. } => {
|
||||
let inner = as_option(&ty).unwrap_or(&ty);
|
||||
quote! { #visibility #name: Option<#inner>, }
|
||||
},
|
||||
Node::Internal { name, .. } => {
|
||||
let child_type_name = to_camel_case(name);
|
||||
quote! {
|
||||
#[serde(default)]
|
||||
#visibility #name: #child_type_name,
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
let default_fields = collect_tokens(children, |node| {
|
||||
match node {
|
||||
Node::Leaf { name, default: None, .. } => quote! { #name: None, },
|
||||
Node::Leaf { name, default: Some(expr), ty, .. } => {
|
||||
let inner_type = as_option(ty).unwrap_or(ty);
|
||||
let path = format!("{}.{}", path.join("."), name);
|
||||
let msg = format!(
|
||||
"default configuration value for '{}' cannot be deserialized as '{}'",
|
||||
path,
|
||||
inner_type.to_token_stream(),
|
||||
);
|
||||
|
||||
quote! {
|
||||
#name: Some({
|
||||
let result: Result<_, ::confique::serde::de::value::Error>
|
||||
= Deserialize::deserialize(#expr.into_deserializer());
|
||||
result.expect(#msg)
|
||||
}),
|
||||
}
|
||||
},
|
||||
Node::Internal { name, .. } => {
|
||||
let child_type_name = to_camel_case(name);
|
||||
quote! {
|
||||
#name: #child_type_name::default_values(),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let overwrite_with_fields = collect_tokens(children, |node| {
|
||||
match node {
|
||||
Node::Leaf { name, .. } => quote! {
|
||||
#name: other.#name.or(self.#name),
|
||||
},
|
||||
Node::Internal { name, .. } => quote! {
|
||||
#name: self.#name.overwrite_with(other.#name),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
contents.extend(quote! {
|
||||
#[derive(Debug, Default, ::confique::serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#visibility struct #type_name {
|
||||
#raw_fields
|
||||
}
|
||||
|
||||
impl #type_name {
|
||||
#visibility fn default_values() -> Self {
|
||||
Self { #default_fields }
|
||||
}
|
||||
|
||||
#visibility fn overwrite_with(self, other: Self) -> Self {
|
||||
Self { #overwrite_with_fields }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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 source can be missing
|
||||
/// required values.
|
||||
///
|
||||
/// These types implement `serde::Deserialize`.
|
||||
mod raw {
|
||||
use super::*;
|
||||
use ::confique::serde::{Deserialize, de::IntoDeserializer};
|
||||
|
||||
#contents
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_root_mod(input: &Input, visibility: &TokenStream) -> TokenStream {
|
||||
let mut out = TokenStream::new();
|
||||
visit(input, |node, path| {
|
||||
if let Node::Internal { name, doc, children } = node {
|
||||
let type_name = to_camel_case(name);
|
||||
|
||||
let user_fields = collect_tokens(children, |node| {
|
||||
match node {
|
||||
Node::Leaf { name, doc, ty, .. } => quote! {
|
||||
#( #[doc = #doc] )*
|
||||
#visibility #name: #ty,
|
||||
},
|
||||
Node::Internal { name, .. } => {
|
||||
let child_type_name = to_camel_case(name);
|
||||
quote! {
|
||||
#visibility #name: #child_type_name,
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
let try_from_fields = collect_tokens(children, |node| {
|
||||
match node {
|
||||
Node::Leaf { name, ty, .. } => {
|
||||
match as_option(ty) {
|
||||
// If this value is optional, we just move it as it can never fail.
|
||||
Some(_) => quote! { #name: src.#name, },
|
||||
|
||||
// Otherwise, we return an error if the value hasn't been specified.
|
||||
None => {
|
||||
let path = match path.is_empty() {
|
||||
true => name.to_string(),
|
||||
false => format!("{}.{}", path.join("."), name),
|
||||
};
|
||||
|
||||
quote! {
|
||||
#name: src.#name
|
||||
.ok_or(self::util::TryFromError { path: #path })?,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Node::Internal { name, .. } => quote! {
|
||||
#name: std::convert::TryFrom::try_from(src.#name)?,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
out.extend(quote! {
|
||||
#( #[doc = #doc] )*
|
||||
#[derive(Debug)]
|
||||
#visibility struct #type_name {
|
||||
#user_fields
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<raw::#type_name> for #type_name {
|
||||
type Error = util::TryFromError;
|
||||
fn try_from(src: raw::#type_name) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
#try_from_fields
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// 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, path| {
|
||||
match node {
|
||||
Node::Internal { doc, .. } => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Node::Leaf { doc, name, 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();
|
||||
}
|
||||
}
|
||||
|
||||
// We check that already when parsing.
|
||||
let example = example.as_ref()
|
||||
.or(default.as_ref())
|
||||
.expect("neither example nor default");
|
||||
|
||||
// Commented out example.
|
||||
writeln!(out, "#{} = {}", name, example).unwrap();
|
||||
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 Node::Internal { children, .. } = node {
|
||||
for child in children.iter().rev() {
|
||||
let mut child_path = path.clone();
|
||||
child_path.push(child.name().to_string());
|
||||
stack.push((child, child_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
fn to_camel_case(ident: &Ident) -> Ident {
|
||||
use heck::CamelCase;
|
||||
|
||||
Ident::new(&ident.to_string().to_camel_case(), ident.span())
|
||||
}
|
||||
|
||||
/// Checks if the given type is an `Option` and if so, return the inner type.
|
||||
///
|
||||
/// Note: this function clearly shows one of the major shortcomings of proc
|
||||
/// macros right now: we do not have access to the compiler's type tables and
|
||||
/// 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> {
|
||||
let ty = match ty {
|
||||
syn::Type::Path(path) => path,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
if ty.qself.is_some() || ty.path.leading_colon.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let valid_paths = [
|
||||
&["Option"] as &[_],
|
||||
&["std", "option", "Option"],
|
||||
&["core", "option", "Option"],
|
||||
];
|
||||
if !valid_paths.iter().any(|vp| ty.path.segments.iter().map(|s| &s.ident).eq(*vp)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let args = match &ty.path.segments.last().unwrap().arguments {
|
||||
syn::PathArguments::AngleBracketed(args) => args,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
if args.args.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
match &args.args[0] {
|
||||
syn::GenericArgument::Type(t) => Some(t),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ToTokens for Expr {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
match self {
|
||||
Self::Str(lit) => lit.to_tokens(tokens),
|
||||
Self::Int(lit) => lit.to_tokens(tokens),
|
||||
Self::Float(lit) => lit.to_tokens(tokens),
|
||||
Self::Bool(lit) => lit.to_tokens(tokens),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
17
macro/src/lib.rs
Normal file
17
macro/src/lib.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use proc_macro::TokenStream as TokenStream1;
|
||||
|
||||
|
||||
mod ast;
|
||||
mod gen;
|
||||
mod parse;
|
||||
|
||||
|
||||
/// Defines a configuration in a special syntax. TODO: explain what this
|
||||
/// generates.
|
||||
#[proc_macro]
|
||||
pub fn config(input: TokenStream1) -> TokenStream1 {
|
||||
syn::parse2::<ast::Input>(input.into())
|
||||
.map(gen::gen)
|
||||
.unwrap_or_else(|e| e.to_compile_error())
|
||||
.into()
|
||||
}
|
||||
143
macro/src/parse.rs
Normal file
143
macro/src/parse.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use syn::{
|
||||
Error, Ident,
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
spanned::Spanned,
|
||||
};
|
||||
|
||||
use crate::ast::{Expr, Input, Node};
|
||||
|
||||
|
||||
|
||||
impl Parse for Input {
|
||||
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
|
||||
let mut outer_attrs = input.call(syn::Attribute::parse_inner)?;
|
||||
let doc = extract_doc(&mut outer_attrs)?;
|
||||
let children = input.call(<Punctuated<_, syn::Token![,]>>::parse_terminated)?;
|
||||
assert_no_extra_attrs(&outer_attrs)?;
|
||||
|
||||
let root = Node::Internal {
|
||||
doc,
|
||||
name: Ident::new("config", Span::call_site()),
|
||||
children: children.into_iter().collect(),
|
||||
};
|
||||
|
||||
Ok(Self { root })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 inner;
|
||||
syn::braced!(inner in input);
|
||||
let fields = inner.call(<Punctuated<_, syn::Token![,]>>::parse_terminated)?;
|
||||
|
||||
Self::Internal {
|
||||
doc,
|
||||
name,
|
||||
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()?;
|
||||
|
||||
if example.is_none() && default.is_none() {
|
||||
let msg = "either a default value or an example value has to be specified";
|
||||
return Err(Error::new(name.span(), msg));
|
||||
}
|
||||
|
||||
Self::Leaf { doc, name, 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)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
pub use confique_macro::config as config;
|
||||
|
||||
pub use serde;
|
||||
|
||||
Reference in New Issue
Block a user