Initial commit
This commit is contained in:
parent
96d6603264
commit
ed32fb9d0f
14
Cargo.toml
14
Cargo.toml
@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lib-rs"
|
name = "macroconf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "library template"
|
description = "macro for creating configurations using miniconf"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://git.mkaenner.de/max/lib-rs"
|
repository = "https://git.mkaenner.de/max/macroconf.git"
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
unsafe_code = "forbid"
|
unsafe_code = "forbid"
|
||||||
@ -14,8 +14,16 @@ enum_glob_use = "deny"
|
|||||||
pedantic = "deny"
|
pedantic = "deny"
|
||||||
nursery = "deny"
|
nursery = "deny"
|
||||||
unwrap_used = "deny"
|
unwrap_used = "deny"
|
||||||
|
expect_used = "deny"
|
||||||
cargo = "warn"
|
cargo = "warn"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||||
|
proc-macro2 = "1.0"
|
||||||
|
quote = "1.0"
|
||||||
|
convert_case = "0.6.0"
|
||||||
|
480
src/lib.rs
480
src/lib.rs
@ -1,2 +1,482 @@
|
|||||||
|
use std::iter::once;
|
||||||
|
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::{Span as Span2, TokenStream as TokenStream2};
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{
|
||||||
|
parse_macro_input, parse_quote, Attribute, DataStruct, DeriveInput, Expr, ExprLit, Field,
|
||||||
|
Ident, Lit, Type, Visibility,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Creates structs for the values to extend them with extra metadata.
|
||||||
|
///
|
||||||
|
/// supported metadata is `min`, `max` and `default`. Doc comments are parsed as `description`
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if it is unable to generate a config struct from the input
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn config(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
let mut input = parse_macro_input!(item as DeriveInput);
|
||||||
|
let mut new_types = vec![];
|
||||||
|
|
||||||
|
match input.data {
|
||||||
|
syn::Data::Struct(DataStruct {
|
||||||
|
struct_token: _,
|
||||||
|
ref mut fields,
|
||||||
|
semi_token: _,
|
||||||
|
}) => {
|
||||||
|
for field in fields.iter_mut() {
|
||||||
|
if let Some(new_type) = generate_helper_struct(field, &input.ident, &input.vis) {
|
||||||
|
new_types.push(new_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syn::Data::Enum(_) => panic!("Enums are not supported"),
|
||||||
|
syn::Data::Union(_) => panic!("Unions are not supported"),
|
||||||
|
}
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#input
|
||||||
|
#(#new_types)*
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_helper_struct(
|
||||||
|
field: &mut Field,
|
||||||
|
input_ident: &Ident,
|
||||||
|
input_visibility: &Visibility,
|
||||||
|
) -> Option<TokenStream2> {
|
||||||
|
let ty = field.ty.clone();
|
||||||
|
let new_type_ident = format_ident!(
|
||||||
|
"__{}{}",
|
||||||
|
input_ident,
|
||||||
|
field
|
||||||
|
.ident
|
||||||
|
.as_ref()
|
||||||
|
.map_or("Value".to_owned(), |v| v.to_string().to_case(Case::Pascal))
|
||||||
|
);
|
||||||
|
let mut new_type_impls = TokenStream2::new();
|
||||||
|
let mut new_type_miniconf_names = vec![];
|
||||||
|
let mut new_type_miniconf_consts = vec![];
|
||||||
|
let mut extra_new_checks = TokenStream2::new();
|
||||||
|
let mut generate_new_type = false;
|
||||||
|
for attr in &field.attrs {
|
||||||
|
if let Some((new_type_impl, new_check, const_ident, key)) =
|
||||||
|
parse_min(&new_type_ident, attr, &ty)
|
||||||
|
{
|
||||||
|
new_type_impls.extend(new_type_impl);
|
||||||
|
new_type_miniconf_consts.push(const_ident);
|
||||||
|
new_type_miniconf_names.push(key);
|
||||||
|
extra_new_checks.extend(new_check);
|
||||||
|
generate_new_type = true;
|
||||||
|
}
|
||||||
|
if let Some((new_type_impl, new_check, const_ident, key)) =
|
||||||
|
parse_max(&new_type_ident, attr, &ty)
|
||||||
|
{
|
||||||
|
new_type_impls.extend(new_type_impl);
|
||||||
|
new_type_miniconf_consts.push(const_ident);
|
||||||
|
new_type_miniconf_names.push(key);
|
||||||
|
extra_new_checks.extend(new_check);
|
||||||
|
generate_new_type = true;
|
||||||
|
}
|
||||||
|
if let Some((new_type_impl, const_ident, key)) = parse_default(&new_type_ident, attr, &ty) {
|
||||||
|
new_type_impls.extend(new_type_impl);
|
||||||
|
new_type_miniconf_consts.push(const_ident);
|
||||||
|
new_type_miniconf_names.push(key);
|
||||||
|
generate_new_type = true;
|
||||||
|
}
|
||||||
|
if let Some((new_type_impl, const_ident, key)) = parse_description(&new_type_ident, attr) {
|
||||||
|
new_type_impls.extend(new_type_impl);
|
||||||
|
new_type_miniconf_consts.push(const_ident);
|
||||||
|
new_type_miniconf_names.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !generate_new_type {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
field.attrs.retain(|attr| {
|
||||||
|
!["min", "max", "default"]
|
||||||
|
.iter()
|
||||||
|
.any(|key| attr.path().is_ident(key))
|
||||||
|
});
|
||||||
|
|
||||||
|
field.attrs.push(parse_quote!(#[tree(depth(1))]));
|
||||||
|
let vis = if matches!(field.vis, Visibility::Public(_))
|
||||||
|
|| matches!(field.vis, Visibility::Inherited)
|
||||||
|
{
|
||||||
|
input_visibility
|
||||||
|
} else {
|
||||||
|
&field.vis
|
||||||
|
};
|
||||||
|
field.ty = parse_quote!(#new_type_ident);
|
||||||
|
let miniconf_fields = new_type_miniconf_names.len() + 1;
|
||||||
|
|
||||||
|
let new = generate_new(&new_type_ident, &ty, &extra_new_checks);
|
||||||
|
let serde = generate_serde(&new_type_ident, &ty, !extra_new_checks.is_empty());
|
||||||
|
|
||||||
|
let tree_key = generate_tree_key(&new_type_ident, new_type_miniconf_names.iter());
|
||||||
|
let tree_serialize = generate_tree_serialize(&new_type_ident, &new_type_miniconf_consts[..]);
|
||||||
|
let tree_deserialize = generate_tree_deserialize(&new_type_ident, miniconf_fields);
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
|
||||||
|
#vis struct #new_type_ident(#ty);
|
||||||
|
|
||||||
|
#new_type_impls
|
||||||
|
|
||||||
|
#new
|
||||||
|
#serde
|
||||||
|
|
||||||
|
#tree_key
|
||||||
|
#tree_serialize
|
||||||
|
#tree_deserialize
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_min(
|
||||||
|
ident: &Ident,
|
||||||
|
attr: &Attribute,
|
||||||
|
ty: &Type,
|
||||||
|
) -> Option<(TokenStream2, Option<TokenStream2>, Ident, &'static str)> {
|
||||||
|
const KEY: &str = "min";
|
||||||
|
if !attr.path().is_ident(KEY) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let const_ident = Ident::new(KEY.to_case(Case::Upper).as_str(), Span2::mixed_site());
|
||||||
|
let (value, new_check) = attr.meta.require_path_only().map_or_else(
|
||||||
|
|_| {
|
||||||
|
let value = &attr.meta.require_name_value().unwrap().value;
|
||||||
|
(
|
||||||
|
quote!(#value),
|
||||||
|
Some(quote! {
|
||||||
|
if (value < Self::#const_ident.0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|_| (quote!(#ty::#const_ident), None),
|
||||||
|
);
|
||||||
|
let impl_quote = quote! {
|
||||||
|
impl #ident {
|
||||||
|
const #const_ident: Self = Self(#value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some((impl_quote, new_check, const_ident, KEY))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_max(
|
||||||
|
ident: &Ident,
|
||||||
|
attr: &Attribute,
|
||||||
|
ty: &Type,
|
||||||
|
) -> Option<(TokenStream2, Option<TokenStream2>, Ident, &'static str)> {
|
||||||
|
const KEY: &str = "max";
|
||||||
|
if !attr.path().is_ident(KEY) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let const_ident = Ident::new(KEY.to_case(Case::Upper).as_str(), Span2::mixed_site());
|
||||||
|
let (value, new_check) = attr.meta.require_path_only().map_or_else(
|
||||||
|
|_| {
|
||||||
|
let value = &attr.meta.require_name_value().unwrap().value;
|
||||||
|
(
|
||||||
|
quote!(#value),
|
||||||
|
Some(quote! {
|
||||||
|
if (value > Self::#const_ident.0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|_| (quote!(#ty::#const_ident), None),
|
||||||
|
);
|
||||||
|
let impl_quote = quote! {
|
||||||
|
impl #ident {
|
||||||
|
const #const_ident: Self = Self(#value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some((impl_quote, new_check, const_ident, KEY))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_default(
|
||||||
|
ident: &Ident,
|
||||||
|
attr: &Attribute,
|
||||||
|
ty: &Type,
|
||||||
|
) -> Option<(TokenStream2, Ident, &'static str)> {
|
||||||
|
const KEY: &str = "default";
|
||||||
|
if !attr.path().is_ident(KEY) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let const_ident = Ident::new(KEY.to_case(Case::Upper).as_str(), Span2::mixed_site());
|
||||||
|
let value = attr.meta.require_path_only().map_or_else(
|
||||||
|
|_| {
|
||||||
|
let value = &attr.meta.require_name_value().unwrap().value;
|
||||||
|
quote!(#value)
|
||||||
|
},
|
||||||
|
|_| quote!(#ty::#const_ident),
|
||||||
|
);
|
||||||
|
let impl_quote = quote! {
|
||||||
|
impl #ident {
|
||||||
|
const #const_ident: Self = Self(#value);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::core::default::Default for #ident {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::#const_ident
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some((impl_quote, const_ident, KEY))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_description(
|
||||||
|
ident: &Ident,
|
||||||
|
attr: &Attribute,
|
||||||
|
) -> Option<(TokenStream2, Ident, &'static str)> {
|
||||||
|
const KEY: &str = "description";
|
||||||
|
if !attr.path().is_ident("doc") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let const_ident = Ident::new(KEY.to_case(Case::Upper).as_str(), Span2::mixed_site());
|
||||||
|
let value = &attr.meta.require_name_value().ok()?.value;
|
||||||
|
let value = match value {
|
||||||
|
Expr::Lit(ExprLit {
|
||||||
|
attrs: _,
|
||||||
|
lit: Lit::Str(ref string),
|
||||||
|
}) => string.value(),
|
||||||
|
_ => panic!("doc comment must be a string"),
|
||||||
|
};
|
||||||
|
let trimmed_value = value.trim();
|
||||||
|
let impl_quote = quote! {
|
||||||
|
impl #ident {
|
||||||
|
const #const_ident: &'static str = #trimmed_value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some((impl_quote, const_ident, KEY))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_new(ident: &Ident, ty: &Type, extra_checks: &TokenStream2) -> TokenStream2 {
|
||||||
|
if extra_checks.is_empty() {
|
||||||
|
quote! {
|
||||||
|
impl #ident {
|
||||||
|
pub const fn new(value: #ty) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::core::ops::Deref for #ident {
|
||||||
|
type Target = #ty;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::core::ops::DerefMut for #ident {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let const_new = if [
|
||||||
|
parse_quote!(u8),
|
||||||
|
parse_quote!(u16),
|
||||||
|
parse_quote!(u32),
|
||||||
|
parse_quote!(u64),
|
||||||
|
parse_quote!(u128),
|
||||||
|
parse_quote!(i8),
|
||||||
|
parse_quote!(i16),
|
||||||
|
parse_quote!(i32),
|
||||||
|
parse_quote!(i64),
|
||||||
|
parse_quote!(i128),
|
||||||
|
]
|
||||||
|
.contains(ty)
|
||||||
|
{
|
||||||
|
Some(quote!(const))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
impl #ident {
|
||||||
|
pub #const_new fn new(value: #ty) -> ::core::option::Option<Self> {
|
||||||
|
#extra_checks
|
||||||
|
::core::option::Option::Some(Self(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const unsafe fn new_unchecked(value: #ty) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::core::ops::Deref for #ident {
|
||||||
|
type Target = #ty;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_serde(ident: &Ident, ty: &Type, checked_new: bool) -> TokenStream2 {
|
||||||
|
let conversion = if checked_new {
|
||||||
|
quote! {
|
||||||
|
Self::new(value).ok_or_else(|| {
|
||||||
|
D::Error::custom("checking value bounds")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
Ok(Self::new(value))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
impl ::serde::Serialize for #ident {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> ::core::result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: ::serde::Serializer,
|
||||||
|
{
|
||||||
|
self.0.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> ::serde::Deserialize<'de> for #ident {
|
||||||
|
fn deserialize<D>(deserializer: D) -> ::core::result::Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: ::serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
type T = #ty;
|
||||||
|
let value = T::deserialize(deserializer)?;
|
||||||
|
#conversion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_tree_key<'a>(
|
||||||
|
ident: &Ident,
|
||||||
|
keys: impl Iterator<Item = &'a &'a str> + ExactSizeIterator + Clone,
|
||||||
|
) -> TokenStream2 {
|
||||||
|
let keys = once(&"value").chain(keys);
|
||||||
|
let num_keys = keys
|
||||||
|
.size_hint()
|
||||||
|
.1
|
||||||
|
.expect("safe because both iterators (once and original keys) are exact");
|
||||||
|
let max_length = keys.clone().map(|v| v.len()).max();
|
||||||
|
quote! {
|
||||||
|
impl #ident {
|
||||||
|
const __MINICONF_NAMES: [&'static str; #num_keys] = [#(#keys,)*];
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::miniconf::TreeKey<1> for #ident {
|
||||||
|
fn name_to_index(name: &str) -> ::core::option::Option<usize> {
|
||||||
|
Self::__MINICONF_NAMES.iter().position(|&n| n == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn traverse_by_key<K, F, E>(mut keys: K, mut func: F) -> ::core::result::Result<usize, ::miniconf::Error<E>>
|
||||||
|
where
|
||||||
|
K: ::core::iter::Iterator,
|
||||||
|
K::Item: ::miniconf::Key,
|
||||||
|
// Writing this to return an iterator instead of using a callback
|
||||||
|
// would have worse performance (O(n^2) instead of O(n) for matching)
|
||||||
|
F: FnMut(usize, &str) -> ::core::result::Result<(), E>,
|
||||||
|
{
|
||||||
|
let ::core::option::Option::Some(key) = keys.next() else { return ::core::result::Result::Ok(0) };
|
||||||
|
let index = ::miniconf::Key::find::<1, Self>(&key).ok_or(::miniconf::Error::NotFound(1))?;
|
||||||
|
let name = Self::__MINICONF_NAMES
|
||||||
|
.get(index)
|
||||||
|
.ok_or(::miniconf::Error::NotFound(1))?;
|
||||||
|
func(index, name)?;
|
||||||
|
::miniconf::Increment::increment(::core::result::Result::Ok(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata() -> miniconf::Metadata {
|
||||||
|
let mut metadata = miniconf::Metadata::default();
|
||||||
|
metadata.max_depth = 1;
|
||||||
|
metadata.count = #num_keys;
|
||||||
|
metadata.max_length = #max_length;
|
||||||
|
metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_tree_serialize(ident: &Ident, consts: &[Ident]) -> TokenStream2 {
|
||||||
|
let matches = consts.iter().enumerate().map(|(i, ident)| {
|
||||||
|
let index = i + 1;
|
||||||
|
quote! {
|
||||||
|
#index => ::miniconf::Serialize::serialize(&Self::#ident, ser)?,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
quote! {
|
||||||
|
impl ::miniconf::TreeSerialize<1> for #ident {
|
||||||
|
fn serialize_by_key<K, S>(
|
||||||
|
&self,
|
||||||
|
mut keys: K,
|
||||||
|
ser: S,
|
||||||
|
) -> ::core::result::Result<usize, ::miniconf::Error<S::Error>>
|
||||||
|
where
|
||||||
|
K: ::core::iter::Iterator,
|
||||||
|
K::Item: ::miniconf::Key,
|
||||||
|
S: ::serde::Serializer,
|
||||||
|
{
|
||||||
|
let ::core::option::Option::Some(key) = keys.next() else {
|
||||||
|
return ::miniconf::Increment::increment({
|
||||||
|
::miniconf::Serialize::serialize(&self.0, ser)?;
|
||||||
|
::core::result::Result::Ok(0)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let index = ::miniconf::Key::find::<1, Self>(&key).ok_or(miniconf::Error::NotFound(1))?;
|
||||||
|
if keys.next().is_some() {
|
||||||
|
return ::core::result::Result::Err(::miniconf::Error::TooLong(1));
|
||||||
|
}
|
||||||
|
::miniconf::Increment::increment({
|
||||||
|
match index {
|
||||||
|
0 => ::miniconf::Serialize::serialize(&self.0, ser)?,
|
||||||
|
#(#matches)*
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
::core::result::Result::Ok(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_tree_deserialize(ident: &Ident, num_keys: usize) -> TokenStream2 {
|
||||||
|
quote! {
|
||||||
|
impl<'de> ::miniconf::TreeDeserialize<'de, 1> for #ident {
|
||||||
|
fn deserialize_by_key<K, D>(
|
||||||
|
&mut self,
|
||||||
|
mut keys: K,
|
||||||
|
de: D,
|
||||||
|
) -> ::core::result::Result<usize, ::miniconf::Error<D::Error>>
|
||||||
|
where
|
||||||
|
K: ::core::iter::Iterator,
|
||||||
|
K::Item: ::miniconf::Key,
|
||||||
|
D: ::serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let ::core::option::Option::Some(key) = keys.next() else {
|
||||||
|
self.0 = ::miniconf::Deserialize::deserialize(de)?;
|
||||||
|
return ::core::result::Result::Ok(0);
|
||||||
|
};
|
||||||
|
let index = ::miniconf::Key::find::<1, Self>(&key).ok_or(::miniconf::Error::NotFound(1))?;
|
||||||
|
if keys.next().is_some() {
|
||||||
|
return ::core::result::Result::Err(miniconf::Error::TooLong(1));
|
||||||
|
}
|
||||||
|
match index {
|
||||||
|
0 => ::miniconf::Increment::increment({
|
||||||
|
self.0 = ::miniconf::Deserialize::deserialize(de)?;
|
||||||
|
Ok(0)
|
||||||
|
}),
|
||||||
|
1..=#num_keys => ::core::result::Result::Err(::miniconf::Error::Absent(0)),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user