#![allow(clippy::get_first)]
#![allow(clippy::len_zero)]
#![allow(clippy::tabs_in_doc_comments)]
extern crate proc_macro2;
use std::path::PathBuf;
use heck::{AsShoutySnakeCase, AsSnakeCase};
use proc_macro::TokenStream;
use quote::{format_ident, quote, quote_spanned};
use syn::{
Data, DeriveInput, Fields, Ident, LitInt, Token,
parse::{Parse, ParseStream},
parse_macro_input, token,
};
#[proc_macro_attribute]
pub fn wrap_err(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as syn::Item);
match input {
syn::Item::Struct(mut s) => {
let name = s.ident.clone();
let vis = s.vis.clone();
let user_fields: Vec<(syn::Ident, syn::Type)> = match &s.fields {
syn::Fields::Named(fields) => fields.named.iter().map(|f| (f.ident.clone().unwrap(), f.ty.clone())).collect(),
_ => panic!("#[wrap_err] on a struct requires named fields"),
};
match &mut s.fields {
syn::Fields::Named(fields) => {
fields.named.push(syn::parse_quote! { backtrace: ::std::backtrace::Backtrace });
fields.named.push(syn::parse_quote! { spantrace: ::tracing_error::SpanTrace });
}
_ => unreachable!(),
}
let param_names: Vec<&syn::Ident> = user_fields.iter().map(|(n, _)| n).collect();
let param_types: Vec<&syn::Type> = user_fields.iter().map(|(_, t)| t).collect();
quote! {
#s
impl #name {
#vis fn new(#(#param_names: #param_types),*) -> Self {
Self {
#(#param_names,)*
backtrace: ::std::backtrace::Backtrace::capture(),
spantrace: ::tracing_error::SpanTrace::capture(),
}
}
}
}
.into()
}
syn::Item::Enum(mut e) => {
let name = e.ident.clone();
let vis = e.vis.clone();
struct LeafVariant {
ident: syn::Ident,
user_fields: Vec<(syn::Ident, syn::Type)>,
}
struct ForeignVariant {
ident: syn::Ident,
inner_type: syn::Type,
}
let mut leaf_variants: Vec<LeafVariant> = Vec::new();
let mut foreign_variants: Vec<ForeignVariant> = Vec::new();
for variant in &mut e.variants {
let is_leaf = variant.attrs.iter().any(|a| a.path().is_ident("leaf"));
let is_foreign = variant.attrs.iter().any(|a| a.path().is_ident("foreign"));
let is_own = variant.attrs.iter().any(|a| a.path().is_ident("own"));
if is_leaf {
variant.attrs.retain(|a| !a.path().is_ident("leaf"));
match &mut variant.fields {
syn::Fields::Named(fields) => {
let user_fields = fields.named.iter().map(|f| (f.ident.clone().unwrap(), f.ty.clone())).collect();
leaf_variants.push(LeafVariant {
ident: variant.ident.clone(),
user_fields,
});
fields.named.push(syn::parse_quote! { backtrace: ::std::backtrace::Backtrace });
fields.named.push(syn::parse_quote! { spantrace: ::tracing_error::SpanTrace });
}
syn::Fields::Unit => {
leaf_variants.push(LeafVariant {
ident: variant.ident.clone(),
user_fields: vec![],
});
variant.fields = syn::Fields::Named(syn::parse_quote! {{
backtrace: ::std::backtrace::Backtrace,
spantrace: ::tracing_error::SpanTrace
}});
}
_ => panic!("#[leaf] variants must have named or unit fields"),
}
} else if is_foreign {
variant.attrs.retain(|a| !a.path().is_ident("foreign"));
let inner_type = match &variant.fields {
syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => fields.unnamed[0].ty.clone(),
_ => panic!("#[foreign] variants must be tuple variants with exactly one field, e.g. `Io(std::io::Error)`"),
};
foreign_variants.push(ForeignVariant {
ident: variant.ident.clone(),
inner_type: inner_type.clone(),
});
if !variant.attrs.iter().any(|a| a.path().is_ident("error")) {
variant.attrs.push(syn::parse_quote! { #[error("{source}")] });
}
variant.fields = syn::Fields::Named(syn::parse_quote! {{
source: #inner_type,
backtrace: ::std::backtrace::Backtrace,
spantrace: ::tracing_error::SpanTrace
}});
} else if is_own {
variant.attrs.retain(|a| !a.path().is_ident("own"));
match &mut variant.fields {
syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
if !variant.attrs.iter().any(|a| a.path().is_ident("error")) {
variant.attrs.push(syn::parse_quote! { #[error(transparent)] });
}
let field = &mut fields.unnamed[0];
field.attrs.push(syn::parse_quote! { #[from] });
field.attrs.push(syn::parse_quote! { #[backtrace] });
}
_ => panic!("#[own] variants must be tuple variants with exactly one field, e.g. `Inner(InnerError)`"),
}
}
}
let constructors = leaf_variants.iter().map(|lv| {
let variant_ident = &lv.ident;
let method_name = format_ident!("new_{}", AsSnakeCase(lv.ident.to_string()).to_string());
let param_names: Vec<&syn::Ident> = lv.user_fields.iter().map(|(n, _)| n).collect();
let param_types: Vec<&syn::Type> = lv.user_fields.iter().map(|(_, t)| t).collect();
quote! {
#vis fn #method_name(#(#param_names: #param_types),*) -> Self {
Self::#variant_ident {
#(#param_names,)*
backtrace: ::std::backtrace::Backtrace::capture(),
spantrace: ::tracing_error::SpanTrace::capture(),
}
}
}
});
let from_impls = foreign_variants.iter().map(|fv| {
let variant_ident = &fv.ident;
let inner_type = &fv.inner_type;
quote! {
impl From<#inner_type> for #name {
fn from(source: #inner_type) -> Self {
Self::#variant_ident {
source,
backtrace: ::std::backtrace::Backtrace::capture(),
spantrace: ::tracing_error::SpanTrace::capture(),
}
}
}
}
});
quote! {
#e
impl #name {
#(#constructors)*
}
#(#from_impls)*
}
.into()
}
_ => panic!("#[wrap_err] can only be applied to structs or enums"),
}
}
#[proc_macro]
pub fn graphemics(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::Ident);
let s = input.to_string();
let mut split_caps = Vec::new();
let mut current_word = String::new();
for c in s.chars() {
if c.is_uppercase() && !current_word.is_empty() {
split_caps.push(current_word);
current_word = String::new();
}
current_word.push(c);
}
if !current_word.is_empty() {
split_caps.push(current_word);
}
let acronym = split_caps.iter().map(|s| s.chars().next().unwrap()).collect::<String>().to_lowercase();
let acronym_caps = acronym.to_uppercase();
let same_lower = s.to_lowercase();
let same_upper = s.to_uppercase();
let same = s.clone();
let snake_case = split_caps.iter().map(|s| s.to_lowercase()).collect::<Vec<String>>().join("_");
let graphems = [acronym, acronym_caps, same_lower, same_upper, same, snake_case];
let mut unique_items = Vec::new();
for item in graphems.into_iter() {
if !unique_items.contains(&item) {
unique_items.push(item);
}
}
let unique_items_valid = unique_items.into_iter().filter(|s| s.len() != 1).collect::<Vec<String>>();
let expanded = quote! {
{
let mut result: Vec<&'static str> = Vec::new();
#(
result.push(#unique_items_valid);
)*
result
}
};
TokenStream::from(expanded)
}
#[proc_macro_derive(CompactFormatNamed, attributes(compact))]
pub fn derive_compact_format_named(input: TokenStream) -> TokenStream {
let (cleaned_input, inline_defaults) = strip_field_defaults(input);
let ast = parse_macro_input!(cleaned_input as DeriveInput);
let name = &ast.ident;
let fields = if let Data::Struct(syn::DataStruct {
fields: Fields::Named(syn::FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
} else {
unimplemented!()
};
let struct_default = ast.attrs.iter().any(|attr| {
if let syn::Meta::List(meta_list) = &attr.meta {
if meta_list.path.is_ident("compact") {
return meta_list.tokens.to_string().trim() == "default";
}
}
false
});
let mut first_chars: Vec<char> = Vec::new();
for field in fields {
let first_char = field.ident.as_ref().unwrap().to_string().chars().next().unwrap();
if !first_chars.contains(&first_char) {
first_chars.push(first_char);
} else {
panic!("Field names must be unique");
}
}
let n_fields = fields.len();
let field_defaults: Vec<Option<proc_macro2::TokenStream>> = fields
.iter()
.map(|f| {
if struct_default {
let ident = &f.ident;
return Some(quote! { Self::default().#ident });
}
for attr in &f.attrs {
if let syn::Meta::List(meta_list) = &attr.meta {
if meta_list.path.is_ident("compact") {
let tokens_str = meta_list.tokens.to_string();
let trimmed = tokens_str.trim();
if trimmed == "default" {
let ty = &f.ty;
return Some(quote! { <#ty as Default>::default() });
}
if let Some(expr_str) = trimmed.strip_prefix("default =").or_else(|| trimmed.strip_prefix("default=")) {
let expr: proc_macro2::TokenStream = expr_str.trim().parse().expect("invalid expression in #[compact(default = ...)]");
return Some(expr);
}
}
}
}
let field_name = f.ident.as_ref().unwrap().to_string();
if let Some(expr_str) = inline_defaults.get(&field_name) {
let expr: proc_macro2::TokenStream = expr_str.parse().expect("invalid inline default expression");
return Some(expr);
}
None
})
.collect();
let map_fields_to_chars = fields.iter().zip(field_defaults.iter()).map(|(f, default)| {
let ident = &f.ident;
let ty = &f.ty;
let first_char = ident.as_ref().unwrap().to_string().chars().next().unwrap();
match default {
Some(fallback) => quote! {
#ident: match provided_params.get(&#first_char) {
Some(v) => v.parse::<#ty>()?,
None => #fallback,
},
},
None => {
let field_name = ident.as_ref().unwrap().to_string();
quote! {
#ident: match provided_params.get(&#first_char) {
Some(v) => v.parse::<#ty>()?,
None => v_utils::__internal::eyre::bail!("missing required field '{}'", #field_name),
},
}
}
}
});
let display_fields = fields.iter().map(|f| {
let ident = &f.ident;
let first_char = ident.as_ref().unwrap().to_string().chars().next().unwrap();
quote! {
write!(f, ":{}{}", #first_char, self.#ident)?;
}
});
let expanded = quote! {
impl std::str::FromStr for #name {
type Err = v_utils::__internal::eyre::Report;
fn from_str(s: &str) -> v_utils::__internal::eyre::Result<Self> {
let (name, params_part) = s.split_once(':').unwrap_or((s, ""));
fn split_respecting_braces(s: &str) -> Vec<&str> {
let mut result = Vec::new();
let mut depth = 0;
let mut start = 0;
for (i, c) in s.char_indices() {
match c {
'{' => depth += 1,
'}' => depth -= 1,
':' if depth == 0 => {
result.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
if start < s.len() {
result.push(&s[start..]);
}
result
}
let params_split = if params_part == "" || params_part == "_" {
Vec::new()
} else {
split_respecting_braces(params_part)
};
if params_split.len() > #n_fields {
v_utils::__internal::eyre::bail!("Expected at most {} fields, got {}", #n_fields, params_split.len());
}
let graphemics = v_utils::macros::graphemics!(#name);
if !graphemics.contains(&name) {
v_utils::__internal::eyre::bail!("Incorrect name provided. Expected one of: {:?}", graphemics);
}
let mut provided_params: std::collections::HashMap<char, &str> = std::collections::HashMap::new();
for param in params_split {
if let Some(first_char) = param.chars().next() {
let value = ¶m[1..];
provided_params.insert(first_char, value);
}
}
Ok(#name {
#(#map_fields_to_chars)*
})
}
}
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let graphemics = v_utils::macros::graphemics!(#name);
let a_struct_name = graphemics[0];
write!(f, "{}", a_struct_name)?;
#(#display_fields)*
std::result::Result::Ok(())
}
}
};
expanded.into()
}
#[proc_macro_derive(TryParseVariants)]
pub fn derive_try_parse_variants(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let variants = match &ast.data {
Data::Enum(e) => &e.variants,
_ => panic!("TryParseVariants can only be derived on enums"),
};
let arms = variants.iter().map(|v| {
let variant_ident = &v.ident;
let inner_ty = match &v.fields {
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => &fields.unnamed.first().unwrap().ty,
_ => panic!("TryParseVariants: variant `{variant_ident}` must be a single-field tuple variant"),
};
quote! {
if let Ok(v) = s.parse::<#inner_ty>() {
return Ok(#name::#variant_ident(v));
}
}
});
let expanded = quote! {
impl std::str::FromStr for #name {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
#(#arms)*
Err(s.to_string())
}
}
};
expanded.into()
}
#[deprecated(since = "3.0.0", note = "Use CompactFormatNamed instead")]
#[proc_macro_derive(CompactFormat)]
pub fn derive_compact_format(input: TokenStream) -> TokenStream {
derive_compact_format_named(input)
}
#[proc_macro_derive(CompactFormatMap)]
pub fn derive_compact_format_map(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let fields = if let Data::Struct(syn::DataStruct {
fields: Fields::Named(syn::FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
} else {
unimplemented!()
};
let mut first_chars: Vec<char> = Vec::new();
for field in fields {
let first_char = field.ident.as_ref().unwrap().to_string().chars().next().unwrap();
if !first_chars.contains(&first_char) {
first_chars.push(first_char);
} else {
panic!("Field names must have unique first characters");
}
}
let n_fields = fields.len();
let map_fields_to_chars = fields.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
let first_char = ident.as_ref().unwrap().to_string().chars().next().unwrap();
quote! {
#ident: provided_params.get(&#first_char)
.ok_or_else(|| v_utils::__internal::eyre::eyre!("Missing field '{}' (key '{}')", stringify!(#ident), #first_char))?
.parse::<#ty>()?,
}
});
let display_fields = fields.iter().enumerate().map(|(i, f)| {
let ident = &f.ident;
let first_char = ident.as_ref().unwrap().to_string().chars().next().unwrap();
if i == 0 {
quote! {
write!(f, "{}={}", #first_char, self.#ident)?;
}
} else {
quote! {
write!(f, ";{}={}", #first_char, self.#ident)?;
}
}
});
let expanded = quote! {
impl std::str::FromStr for #name {
type Err = v_utils::__internal::eyre::Report;
fn from_str(s: &str) -> v_utils::__internal::eyre::Result<Self> {
let inner = s.strip_prefix('{')
.and_then(|s| s.strip_suffix('}'))
.ok_or_else(|| v_utils::__internal::eyre::eyre!("CompactFormatMap must be wrapped in {{...}}, got: {}", s))?;
if inner.is_empty() && #n_fields > 0 {
v_utils::__internal::eyre::bail!("Expected {} fields, got empty map", #n_fields);
}
let pairs: Vec<&str> = if inner.is_empty() { Vec::new() } else { inner.split(';').collect() };
if pairs.len() != #n_fields {
v_utils::__internal::eyre::bail!("Expected {} fields, got {}", #n_fields, pairs.len());
}
let mut provided_params: std::collections::HashMap<char, &str> = std::collections::HashMap::new();
for pair in pairs {
let (key, value) = pair.split_once('=')
.ok_or_else(|| v_utils::__internal::eyre::eyre!("Invalid key=value pair: {}", pair))?;
if let Some(first_char) = key.chars().next() {
if key.len() != 1 {
v_utils::__internal::eyre::bail!("Key must be a single character, got: {}", key);
}
provided_params.insert(first_char, value);
}
}
Ok(#name {
#(#map_fields_to_chars)*
})
}
}
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{{")?;
#(#display_fields)*
write!(f, "}}")?;
std::result::Result::Ok(())
}
}
};
expanded.into()
}
#[proc_macro_derive(OptionalFieldsFromVecStr)]
pub fn derive_optional_fields_from_vec_str(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let fields = if let Data::Struct(syn::DataStruct {
fields: Fields::Named(syn::FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
} else {
unimplemented!()
};
let init_nones = fields.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
quote! {
let mut #ident: #ty = None;
}
});
let write_fields = fields.iter().map(|f| {
let ident = &f.ident;
quote! {
#ident,
}
});
let conversions = fields.iter().map(|field| {
let field_name = &field.ident;
let field_type = match &field.ty {
syn::Type::Path(type_path) if type_path.path.segments.last().unwrap().ident == "Option" => {
let generic_arg = match type_path.path.segments.last().unwrap().arguments {
syn::PathArguments::AngleBracketed(ref args) => &args.args[0],
_ => panic!("Expected generic argument for Option"),
};
quote! { #generic_arg }
}
_ => panic!("All fields must be of type Option<T>"),
};
quote! {
if #field_name.is_none() {
if let std::result::Result::Ok(value) = s.as_ref().parse::<#field_type>() {
#field_name = core::option::Option::Some(value);
continue;
}
}
}
});
let expanded = quote! {
impl<S: AsRef<str>> TryFrom<Vec<S>> for #name {
type Error = &'static str;
fn try_from(strings: Vec<S>) -> core::result::Result<Self, Self::Error> {
#(#init_nones)*
for s in strings {
#(#conversions)*
return std::result::Result::Err("Could not parse string");
}
std::result::Result::Ok(#name {
#(#write_fields)*
})
}
}
};
expanded.into()
}
#[proc_macro_derive(VecFieldsFromVecStr)]
pub fn derive_optioinal_vec_fields_from_vec_str(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as syn::DeriveInput);
let name = &ast.ident;
let fields = if let syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
} else {
unimplemented!()
};
let init_empty_vecs = fields.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
quote! {
let mut #ident: #ty = Vec::new();
}
});
let write_fields = fields.iter().map(|f| {
let ident = &f.ident;
quote! {
#ident,
}
});
let conversions = fields.iter().map(|field| {
let field_name = &field.ident;
let field_type = match &field.ty {
syn::Type::Path(type_path) if type_path.path.segments.last().unwrap().ident == "Vec" => {
let generic_arg = match type_path.path.segments.last().unwrap().arguments {
syn::PathArguments::AngleBracketed(ref args) => &args.args[0],
_ => panic!("Expected generic argument for Vec"),
};
quote! { #generic_arg }
}
_ => panic!("All fields must be of type Vec<T>"),
};
quote! {
if let std::result::Result::Ok(value) = s.as_ref().parse::<#field_type>() {
#field_name.push(value);
continue;
}
}
});
let expanded = quote! {
impl<S: AsRef<str>> TryFrom<Vec<S>> for #name {
type Error = &'static str;
fn try_from(strings: Vec<S>) -> core::result::Result<Self, Self::Error> {
#(#init_empty_vecs)*
for s in strings {
#(#conversions)*
return std::result::Result::Err("Could not parse string");
}
std::result::Result::Ok(#name {
#(#write_fields)*
})
}
}
};
expanded.into()
}
#[proc_macro_derive(MyConfigPrimitives, attributes(private_value, serde, settings, primitives, default))]
pub fn deserialize_with_private_values(input: TokenStream) -> TokenStream {
let (input, inline_defaults) = strip_field_defaults(input);
let ast = parse_macro_input!(input as syn::DeriveInput);
let name = &ast.ident;
let fields = if let syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
} else {
unimplemented!()
};
let mut skip_serialize = false;
for attr in &ast.attrs {
if !attr.path().is_ident("primitives") {
continue;
}
let ident = match attr.parse_args::<syn::Ident>() {
Ok(i) => i,
Err(_) =>
return syn::Error::new_spanned(attr, "`#[primitives(...)]` on a struct expects a single identifier: `skip_serialize`")
.to_compile_error()
.into(),
};
if ident == "skip_serialize" {
skip_serialize = true;
} else {
return syn::Error::new_spanned(&ident, format!("unknown `#[primitives({ident})]` on struct; the only supported value is `skip_serialize`"))
.to_compile_error()
.into();
}
}
for f in fields {
for attr in &f.attrs {
if !attr.path().is_ident("primitives") {
continue;
}
let ident = match attr.parse_args::<syn::Ident>() {
Ok(i) => i,
Err(_) =>
return syn::Error::new_spanned(attr, "`#[primitives(...)]` on a field expects a single identifier: `skip`")
.to_compile_error()
.into(),
};
if ident != "skip" {
return syn::Error::new_spanned(&ident, format!("unknown `#[primitives({ident})]` on field; the only supported value is `skip`"))
.to_compile_error()
.into();
}
}
}
let mut default_fns: Vec<proc_macro2::TokenStream> = Vec::new();
let mut serialize_field_calls: Vec<proc_macro2::TokenStream> = Vec::new();
let (helper_fields, init_fields): (Vec<_>, Vec<_>) = fields
.iter()
.map(|f| {
let ident = &f.ident;
let ty = &f.ty;
let type_string = quote! { #ty }.to_string();
let ident_str = ident.as_ref().expect("named fields only").to_string();
let ser_call = if type_string == "SecretString" || type_string == "Option < SecretString >" {
quote! { state.serialize_field(#ident_str, &"***")?; }
} else {
quote! { state.serialize_field(#ident_str, &self.#ident)?; }
};
serialize_field_calls.push(ser_call);
let has_private_value_attr = f.attrs.iter().any(|attr| {
attr.path().is_ident("private_value")
});
let has_primitives_skip_attr = f.attrs.iter().any(|attr| {
if attr.path().is_ident("primitives") {
if let Ok(nested) = attr.parse_args::<syn::Ident>() {
return nested == "skip";
}
}
false
});
let settings_default: Option<proc_macro2::TokenStream> = f.attrs.iter().find_map(|attr| {
if !attr.path().is_ident("settings") {
return None;
}
SettingsFieldAttrs::parse(std::slice::from_ref(attr)).ok().and_then(|a| a.default).map(|e| quote! { #e })
});
let settings_default_present = settings_default.is_some();
let smart_default_present = f.attrs.iter().any(|attr| attr.path().is_ident("default"));
let field_name_str = ident.as_ref().expect("named fields only").to_string();
let inline_default_present = inline_defaults.contains_key(&field_name_str);
let default_expr: Option<proc_macro2::TokenStream> = f.attrs.iter().find_map(|attr| {
if attr.path().is_ident("default") {
attr.parse_args::<syn::Expr>().ok().map(|e| quote! { #e })
} else {
None
}
}).or(settings_default).or_else(|| {
inline_defaults.get(&field_name_str).map(|s| s.parse::<proc_macro2::TokenStream>().expect("inline default expression must parse as tokens"))
});
if has_private_value_attr && smart_default_present {
panic!("field `{field_name_str}`: `#[private_value]` is incompatible with `#[default(expr)]` (SmartDefault). Private values are resolved at deserialization from string / `{{ env = \"...\" }}`; supply the default through the environment instead.");
}
if has_private_value_attr && inline_default_present {
panic!("field `{field_name_str}`: `#[private_value]` is incompatible with the nightly `field: T = expr` default-field-value syntax. Private values are resolved at deserialization from string / `{{ env = \"...\" }}`; supply the default through the environment instead.");
}
if has_private_value_attr && settings_default_present {
panic!("field `{field_name_str}`: `#[private_value]` is incompatible with `#[settings(default = expr)]`. Private values are resolved at deserialization from string / `{{ env = \"...\" }}`; supply the default through the environment instead.");
}
let has_serde_default = f.attrs.iter().any(|attr| {
if !attr.path().is_ident("serde") {
return false;
}
let Ok(nested) = attr.parse_args::<syn::Meta>() else { return false };
matches!(&nested, syn::Meta::Path(p) if p.is_ident("default"))
|| matches!(&nested, syn::Meta::NameValue(nv) if nv.path.is_ident("default"))
});
let mut forwarded_attrs: Vec<proc_macro2::TokenStream> = f.attrs.iter().filter(|attr| {
!attr.path().is_ident("private_value") && !attr.path().is_ident("settings") && !attr.path().is_ident("primitives") && !attr.path().is_ident("default")
}).map(|attr| quote! { #attr }).collect();
let is_option = if let syn::Type::Path(type_path) = ty {
is_option_type(type_path)
} else {
false
};
if let (Some(expr), false) = (&default_expr, has_serde_default) {
let (helper_ty, wrapped) = if has_primitives_skip_attr {
(quote! { #ty }, quote! { #expr })
} else if is_option {
let inner = if let syn::Type::Path(tp) = ty { extract_option_inner_type(tp) } else { unreachable!("is_option implies Type::Path") };
match quote! { #inner }.to_string().as_str() {
"String" => (quote! { Option<PrivateValue> }, quote! { (#expr).map(PrivateValue::Direct) }),
"SecretString" => (quote! { Option<PrivateValue> }, quote! { (#expr).map(|s| PrivateValue::Direct(secrecy::ExposeSecret::expose_secret(&s).to_string())) }),
"PathBuf" => (quote! { Option<v_utils::io::ExpandedPath> }, quote! { (#expr).map(v_utils::io::ExpandedPath::from) }),
_ => (quote! { #ty }, quote! { #expr }),
}
} else {
match type_string.as_str() {
"String" => (quote! { PrivateValue }, quote! { PrivateValue::Direct(#expr) }),
"SecretString" => (quote! { PrivateValue }, quote! { PrivateValue::Direct(secrecy::ExposeSecret::expose_secret(&(#expr)).to_string()) }),
"PathBuf" => (quote! { v_utils::io::ExpandedPath }, quote! { v_utils::io::ExpandedPath::from(#expr) }),
_ => (quote! { #ty }, quote! { #expr }),
}
};
let fn_name = syn::Ident::new(&format!("__default_{}", ident.as_ref().unwrap()), proc_macro2::Span::call_site());
default_fns.push(quote! {
fn #fn_name() -> #helper_ty { #wrapped }
});
let fn_name_str = fn_name.to_string();
forwarded_attrs.push(quote! { #[serde(default = #fn_name_str)] });
}
if has_primitives_skip_attr {
(quote! {
#(#forwarded_attrs)*
#ident: #ty
}, quote! { #ident: helper.#ident })
} else if has_private_value_attr {
if is_option {
let inner_type = if let syn::Type::Path(type_path) = ty {
extract_option_inner_type(type_path)
} else {
panic!("Option type expected for #[private_value] on Option field")
};
(
quote! {
#(#forwarded_attrs)*
#ident: Option<PrivateValue>
},
quote! {
#ident: match helper.#ident {
Some(pv) => match pv.into_string_optional().map_err(|e| v_utils::__internal::serde::de::Error::custom(format!("Failed to convert {} to string: {}", stringify!(#ident), e)))? {
Some(s) => Some(<#inner_type as std::str::FromStr>::from_str(&s).map_err(|e| v_utils::__internal::serde::de::Error::custom(format!("Failed to parse {} from string: {:?}", stringify!(#ident), e)))?),
None => None,
},
None => None,
}
},
)
} else {
(
quote! {
#(#forwarded_attrs)*
#ident: PrivateValue
},
quote! { #ident: <#ty as std::str::FromStr>::from_str(&helper.#ident.into_string().map_err(|e| v_utils::__internal::serde::de::Error::custom(format!("Failed to convert {} to string: {}", stringify!(#ident), e)))?).map_err(|e| v_utils::__internal::serde::de::Error::custom(format!("Failed to parse {} from string: {:?}", stringify!(#ident), e)))? },
)
}
} else if is_option {
if let syn::Type::Path(type_path) = ty {
let inner_type = extract_option_inner_type(type_path);
let inner_type_string = quote! { #inner_type }.to_string();
match inner_type_string.as_str() {
"String" => (
quote! {
#(#forwarded_attrs)*
#ident: Option<PrivateValue>
},
quote! {
#ident: match helper.#ident {
Some(pv) => Some(pv.into_string().map_err(|e| v_utils::__internal::serde::de::Error::custom(format!("Failed to convert {} to string: {}", stringify!(#ident), e)))?),
None => None,
}
},
),
"PathBuf" => (
quote! {
#(#forwarded_attrs)*
#ident: Option<v_utils::io::ExpandedPath>
},
quote! { #ident: helper.#ident.map(|ep| ep.0) },
),
"SecretString" => (
quote! {
#(#forwarded_attrs)*
#ident: Option<PrivateValue>
},
quote! {
#ident: match helper.#ident {
Some(pv) => Some(secrecy::SecretString::new(pv.into_string().map_err(|e| v_utils::__internal::serde::de::Error::custom(format!("Failed to convert {} to string: {}", stringify!(#ident), e)))?.into_boxed_str())),
None => None,
}
},
),
_ => (quote! {
#(#forwarded_attrs)*
#ident: #ty
}, quote! { #ident: helper.#ident }),
}
} else {
(quote! {
#(#forwarded_attrs)*
#ident: #ty
}, quote! { #ident: helper.#ident })
}
} else {
match type_string.as_str() {
"String" => (
quote! {
#(#forwarded_attrs)*
#ident: PrivateValue
},
quote! { #ident: helper.#ident.into_string().map_err(|e| v_utils::__internal::serde::de::Error::custom(format!("Failed to convert {} to string: {}", stringify!(#ident), e)))? },
),
"PathBuf" => (quote! {
#(#forwarded_attrs)*
#ident: v_utils::io::ExpandedPath
}, quote! { #ident: helper.#ident.0 }),
"SecretString" => (
quote! {
#(#forwarded_attrs)*
#ident: PrivateValue
},
quote! { #ident: secrecy::SecretString::new(helper.#ident.into_string().map_err(|e| v_utils::__internal::serde::de::Error::custom(format!("Failed to convert {} to string: {}", stringify!(#ident), e)))?.into_boxed_str()) },
),
_ => (quote! {
#(#forwarded_attrs)*
#ident: #ty
}, quote! { #ident: helper.#ident }),
}
}
})
.unzip();
let q = quote! {
impl<'de> v_utils::__internal::serde::Deserialize<'de> for #name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: v_utils::__internal::serde::de::Deserializer<'de>,
{
use v_utils::__internal::eyre::WrapErr;
#[derive(Clone, Debug)]
enum PrivateValue {
Direct(String),
Env { env: String },
}
impl Default for PrivateValue {
fn default() -> Self {
PrivateValue::Direct(String::new())
}
}
impl PrivateValue {
pub fn into_string(&self) -> v_utils::__internal::eyre::Result<String> {
match self {
PrivateValue::Direct(s) => Ok(s.clone()),
PrivateValue::Env { env } => std::env::var(env).wrap_err_with(|| format!("Environment variable '{}' not found", env)),
}
}
pub fn into_string_optional(&self) -> v_utils::__internal::eyre::Result<Option<String>> {
match self {
PrivateValue::Direct(s) => Ok(Some(s.clone())),
PrivateValue::Env { env } => match std::env::var(env) {
Ok(s) => Ok(Some(s)),
Err(std::env::VarError::NotPresent) => Ok(None),
Err(e) => Err(v_utils::__internal::eyre::eyre!("Failed to read environment variable '{}': {}", env, e)),
},
}
}
}
impl<'de> v_utils::__internal::serde::Deserialize<'de> for PrivateValue {
fn deserialize<D>(deserializer: D) -> Result<PrivateValue, D::Error>
where
D: v_utils::__internal::serde::de::Deserializer<'de>,
{
struct PrivateValueVisitor;
impl<'de> v_utils::__internal::serde::de::Visitor<'de> for PrivateValueVisitor {
type Value = PrivateValue;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a value (string, number, bool, etc.) or a map with a single key 'env'")
}
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
where
E: v_utils::__internal::serde::de::Error,
{
Ok(PrivateValue::Direct(value.to_string()))
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: v_utils::__internal::serde::de::Error,
{
Ok(PrivateValue::Direct(value.to_string()))
}
fn visit_i128<E>(self, value: i128) -> Result<Self::Value, E>
where
E: v_utils::__internal::serde::de::Error,
{
Ok(PrivateValue::Direct(value.to_string()))
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: v_utils::__internal::serde::de::Error,
{
Ok(PrivateValue::Direct(value.to_string()))
}
fn visit_u128<E>(self, value: u128) -> Result<Self::Value, E>
where
E: v_utils::__internal::serde::de::Error,
{
Ok(PrivateValue::Direct(value.to_string()))
}
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
where
E: v_utils::__internal::serde::de::Error,
{
Ok(PrivateValue::Direct(value.to_string()))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: v_utils::__internal::serde::de::Error,
{
Ok(PrivateValue::Direct(value.to_owned()))
}
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
where
M: v_utils::__internal::serde::de::MapAccess<'de>,
{
let key: String = access.next_key()?.ok_or_else(|| v_utils::__internal::serde::de::Error::custom("expected a key"))?;
if key == "env" {
let value: String = access.next_value()?;
Ok(PrivateValue::Env { env: value })
} else {
Err(v_utils::__internal::serde::de::Error::custom("expected key to be 'env'"))
}
}
}
deserializer.deserialize_any(PrivateValueVisitor)
}
}
#(#default_fns)*
#[derive(v_utils::__internal::serde::Deserialize)]
#[serde(crate = "v_utils::__internal::serde")]
struct Helper {
#(#helper_fields),*
}
let helper = Helper::deserialize(deserializer)?;
Ok(#name {
#(#init_fields),*
})
}
}
};
let name_str = name.to_string();
let field_count = serialize_field_calls.len();
let serialize_impl = if skip_serialize {
quote! {}
} else {
quote! {
impl v_utils::__internal::serde::Serialize for #name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: v_utils::__internal::serde::Serializer,
{
use v_utils::__internal::serde::ser::SerializeStruct as _;
let mut state = serializer.serialize_struct(#name_str, #field_count)?;
#(#serialize_field_calls)*
state.end()
}
}
}
};
let combined = quote! {
#q
#serialize_impl
};
combined.into()
}
#[proc_macro_derive(ConfigJsonSchema, attributes(schemars, serde, settings, primitives, private_value, default))]
pub fn derive_config_json_schema(input: TokenStream) -> TokenStream {
let (input, _inline_defaults) = strip_field_defaults(input);
let ast = parse_macro_input!(input as syn::DeriveInput);
let name = &ast.ident;
let name_str = name.to_string();
let fields = if let syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
} else {
unimplemented!("ConfigJsonSchema only supports named-field structs")
};
let mirror_fields = fields.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
let attrs = f
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc") || attr.path().is_ident("schemars") || attr.path().is_ident("serde"));
quote! {
#(#attrs)*
#ident: #ty
}
});
quote! {
const _: () = {
#[derive(::v_utils::__internal::schemars::JsonSchema)]
#[schemars(crate = "::v_utils::__internal::schemars", rename = #name_str)]
#[allow(dead_code)]
struct __ConfigSchemaMirror {
#(#mirror_fields),*
}
impl ::v_utils::__internal::schemars::JsonSchema for #name {
fn inline_schema() -> bool {
<__ConfigSchemaMirror as ::v_utils::__internal::schemars::JsonSchema>::inline_schema()
}
fn schema_name() -> std::borrow::Cow<'static, str> {
<__ConfigSchemaMirror as ::v_utils::__internal::schemars::JsonSchema>::schema_name()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
<__ConfigSchemaMirror as ::v_utils::__internal::schemars::JsonSchema>::schema_id()
}
fn json_schema(generator: &mut ::v_utils::__internal::schemars::SchemaGenerator) -> ::v_utils::__internal::schemars::Schema {
<__ConfigSchemaMirror as ::v_utils::__internal::schemars::JsonSchema>::json_schema(generator)
}
}
};
}
.into()
}
#[proc_macro]
pub fn make_df(input: TokenStream) -> TokenStream {
let DataFrameDef { values_vec, fields, .. } = parse_macro_input!(input as DataFrameDef);
fn vec_name(name: &Ident) -> Ident {
let vec_name = format!("{name}s");
syn::Ident::new(&vec_name, name.span())
}
let vec_declarations = fields.iter().map(|field| {
let vec_ident = vec_name(&field.name);
let ty = &field.dtype;
quote! {
let mut #vec_ident: Vec<#ty> = Vec::new();
}
});
let indices = fields.iter().map(|field| {
let idx = &field.index;
quote! {
value.get(#idx)
}
});
let push_statements = fields.iter().map(|field| {
let name = &field.name;
let vec_name = vec_name(name);
let dtype = &field.dtype;
let as_method = syn::Ident::new(&format!("as_{dtype}"), dtype.span());
quote! {
#vec_name.push(#name.#as_method().unwrap_or_else(|| #name.as_str().unwrap().parse::<#dtype>().unwrap()));
}
});
let df_fields = fields.iter().map(|field| {
let vec_name = vec_name(&field.name);
let name_str = &field.name.to_string();
quote! {
#name_str => #vec_name
}
});
let temp_vars = fields
.iter()
.map(|field| {
let name = format!("{}", field.name);
syn::Ident::new(&name, proc_macro2::Span::call_site())
})
.collect::<Vec<_>>();
quote! {
{
#(#vec_declarations)*
for value in #values_vec {
if let (#(Some(#temp_vars)),*) = (#(#indices),*) {
#(#push_statements;)*
}
}
let df = polars::df![
#(#df_fields),*
].expect("Failed to create DataFrame");
df
}
}
.into()
}
#[proc_macro_derive(WrapNew)]
pub fn wrap_new(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let inner_type = match &input.data {
Data::Struct(data) => match &data.fields {
syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => &fields.unnamed.first().unwrap().ty,
_ => panic!("NewWrapper can only be derived for tuple structs with one field"),
},
_ => panic!("NewWrapper can only be derived for tuple structs"),
};
let expanded = quote! {
impl #name {
pub fn new() -> Self {
Self(<#inner_type>::new())
}
}
};
TokenStream::from(expanded)
}
#[proc_macro_derive(ScreamIt)]
pub fn scream_it(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let variants = if let Data::Enum(syn::DataEnum { variants, .. }) = &input.data {
variants
} else {
panic!("#[derive(ScreamIt)] can only be used on enums");
};
let display_impl = {
let arms = variants.iter().map(|variant| {
let variant_name = &variant.ident;
let screamed_name = AsShoutySnakeCase(variant_name.to_string()).to_string();
quote! {
Self::#variant_name => write!(f, #screamed_name),
}
});
quote! {
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#(#arms)*
}
}
}
}
};
let from_str_impl = {
let arms = variants.iter().map(|variant| {
let variant_name = &variant.ident;
let screamed_name = AsShoutySnakeCase(variant_name.to_string()).to_string();
quote! {
#screamed_name => Ok(Self::#variant_name),
}
});
quote! {
impl std::str::FromStr for #name {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
#(#arms)*
_ => Err(()),
}
}
}
}
};
let serialize_impl = quote! {
impl serde::Serialize for #name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
};
let deserialize_impl = quote! {
impl<'de> serde::Deserialize<'de> for #name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(|_| serde::de::Error::custom("invalid enum value"))
}
}
};
let expanded = quote! {
#display_impl
#from_str_impl
#serialize_impl
#deserialize_impl
};
TokenStream::from(expanded)
}
#[cfg(feature = "cli")]
#[proc_macro_derive(Settings, attributes(settings))]
pub fn derive_setings(input: TokenStream) -> proc_macro::TokenStream {
let input = strip_field_default_values(input);
let ast = parse_macro_input!(input as syn::DeriveInput);
let name = &ast.ident;
let fields = if let syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
} else {
unimplemented!()
};
for field in fields {
if let Err(e) = SettingsFieldAttrs::parse(&field.attrs) {
return e.to_compile_error().into();
}
}
let mut use_env = false;
let mut config_name: Option<String> = None;
for attr in &ast.attrs {
if !attr.path().is_ident("settings") {
continue;
}
let parsed = attr.parse_args_with(|input: syn::parse::ParseStream| {
loop {
let ident: syn::Ident = input.parse()?;
if ident == "use_env" {
let _: Token![=] = input.parse()?;
let lit: syn::LitBool = input.parse()?;
use_env = lit.value;
} else if ident == "config_name" {
let _: Token![=] = input.parse()?;
let lit: syn::LitStr = input.parse()?;
config_name = Some(lit.value());
} else {
return Err(unknown_attr_ident(&ident, &["use_env", "config_name"]));
}
if input.is_empty() {
return Ok(());
}
let _: Token![,] = input.parse()?;
}
});
if let Err(e) = parsed {
return e.to_compile_error().into();
}
}
let config_name_expr = match &config_name {
Some(s) => quote! { #s },
None => quote! { env!("CARGO_PKG_NAME") },
};
#[cfg(feature = "xdg")]
let xdg_conf_dir = quote_spanned! { proc_macro2::Span::call_site()=>
let xdg_dirs = ::v_utils::__internal::xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"));
let xdg_conf_dir = xdg_dirs.get_config_home().unwrap().parent().unwrap().display().to_string();
};
#[cfg(not(feature = "xdg"))]
let xdg_conf_dir = quote_spanned! { proc_macro2::Span::call_site()=>
let xdg_conf_dir = ::v_utils::__internal::xdg_config_fallback();
};
let all_field_names: Vec<_> = fields.iter().map(|f| f.ident.as_ref().unwrap().to_string()).collect();
let field_name_strings = all_field_names.iter().map(|name| quote! { #name });
let field_default_entries = fields.iter().map(|f| {
let name = f.ident.as_ref().unwrap().to_string();
let ty = &f.ty;
quote! {
{
let field_wrapper = __settings_default_provider::Wrapper::<#ty>(std::marker::PhantomData);
match (&field_wrapper).get_defaults() {
Some(v) => { map.insert(#name.to_string(), v); }
None => {
unset.push(#name);
map.insert(#name.to_string(), ::v_utils::__internal::serde_json::Value::String(::v_utils::__internal::REQUIRED_PLACEHOLDER.to_string()));
}
}
}
}
});
let try_build = quote! {
mod __settings_default_provider {
#[allow(unused_imports)]
use super::*;
pub struct Wrapper<T>(pub std::marker::PhantomData<T>);
pub trait GetDefault<T> {
fn get_default_for_path(&self, field_path: &str) -> Option<::v_utils::__internal::serde_json::Value>;
}
impl<T> GetDefault<T> for &Wrapper<T> {
fn get_default_for_path(&self, _field_path: &str) -> Option<::v_utils::__internal::serde_json::Value> {
None
}
}
impl<T> GetDefault<T> for Wrapper<T>
where
T: Default + ::v_utils::__internal::serde::Serialize,
{
fn get_default_for_path(&self, field_path: &str) -> Option<::v_utils::__internal::serde_json::Value> {
let default_instance = T::default();
let serialized = ::v_utils::__internal::serde_json::to_value(&default_instance).ok()?;
let mut current = &serialized;
for part in field_path.split('.') {
current = current.get(part)?;
}
Some(current.clone())
}
}
pub trait ComputeDiff<T> {
fn compute_diff(&self, current: &T) -> Option<String>;
}
impl<T> ComputeDiff<T> for &Wrapper<T> {
fn compute_diff(&self, _current: &T) -> Option<String> {
None
}
}
impl<T> ComputeDiff<T> for Wrapper<T>
where
T: Default + ::v_utils::__internal::serde::Serialize,
{
fn compute_diff(&self, current: &T) -> Option<String> {
let default_instance = T::default();
let current_json = ::v_utils::__internal::serde_json::to_value(current).ok()?;
let default_json = ::v_utils::__internal::serde_json::to_value(&default_instance).ok()?;
let mut diffs = Vec::new();
collect_diffs(¤t_json, &default_json, String::new(), &mut diffs);
if diffs.is_empty() {
None
} else {
Some(diffs.join("\n"))
}
}
}
fn collect_diffs(
current: &::v_utils::__internal::serde_json::Value,
default: &::v_utils::__internal::serde_json::Value,
prefix: String,
diffs: &mut Vec<String>,
) {
use ::v_utils::__internal::serde_json::Value;
match (current, default) {
(Value::Object(curr_map), Value::Object(def_map)) => {
for (key, curr_val) in curr_map {
let path = if prefix.is_empty() {
key.clone()
} else {
format!("{}.{}", prefix, key)
};
if let Some(def_val) = def_map.get(key) {
collect_diffs(curr_val, def_val, path, diffs);
} else {
diffs.push(format!("{}: -> {}", path, format_value(curr_val)));
}
}
}
_ => {
if current != default {
diffs.push(format!("{}: {} -> {}", prefix, format_value(default), format_value(current)));
}
}
}
}
fn format_value(value: &::v_utils::__internal::serde_json::Value) -> String {
use ::v_utils::__internal::serde_json::Value;
match value {
Value::String(s) => format!("\"{}\"", s),
Value::Null => "null".to_string(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(format_value).collect();
format!("[{}]", items.join(", "))
}
Value::Object(obj) => {
let items: Vec<String> = obj.iter()
.map(|(k, v)| format!("{}: {}", k, format_value(v)))
.collect();
format!("{{{}}}", items.join(", "))
}
}
}
pub trait GetDefaults<T> {
fn get_defaults(&self) -> Option<::v_utils::__internal::serde_json::Value>;
}
impl<T> GetDefaults<T> for &Wrapper<T> {
fn get_defaults(&self) -> Option<::v_utils::__internal::serde_json::Value> {
None
}
}
impl<T> GetDefaults<T> for Wrapper<T>
where
T: Default + ::v_utils::__internal::serde::Serialize,
{
fn get_defaults(&self) -> Option<::v_utils::__internal::serde_json::Value> {
let default_instance = T::default();
::v_utils::__internal::serde_json::to_value(&default_instance).ok()
}
}
pub trait HasDefault<T> {
fn has_default(&self) -> bool;
}
impl<T> HasDefault<T> for &Wrapper<T> {
fn has_default(&self) -> bool { false }
}
impl<T: Default> HasDefault<T> for Wrapper<T> {
fn has_default(&self) -> bool { true }
}
pub trait HasSerialize<T> {
fn has_serialize(&self) -> bool;
}
impl<T> HasSerialize<T> for &Wrapper<T> {
fn has_serialize(&self) -> bool { false }
}
impl<T: ::v_utils::__internal::serde::Serialize> HasSerialize<T> for Wrapper<T> {
fn has_serialize(&self) -> bool { true }
}
pub trait GetSchema<T> {
fn get_schema(&self) -> Option<String>;
}
impl<T> GetSchema<T> for &Wrapper<T> {
fn get_schema(&self) -> Option<String> {
None
}
}
impl<T> GetSchema<T> for Wrapper<T>
where
T: ::v_utils::__internal::schemars::JsonSchema,
{
fn get_schema(&self) -> Option<String> {
let schema = ::v_utils::__internal::schemars::schema_for!(T);
::v_utils::__internal::serde_json::to_string_pretty(&schema).ok()
}
}
}
impl #name {
pub fn try_build(flags: SettingsFlags) -> Result<Self, ::v_utils::__internal::SettingsError> {
Self::try_build_internal(flags, true)
}
fn try_build_internal(flags: SettingsFlags, allow_extend: bool) -> Result<Self, ::v_utils::__internal::SettingsError> {
let path = flags.config.as_ref().map(|p| p.0.clone());
let app_name = env!("CARGO_PKG_NAME");
let config_name = #config_name_expr;
#xdg_conf_dir
let location_bases = [
format!("{xdg_conf_dir}/{config_name}"),
format!("{xdg_conf_dir}/{config_name}/config"),
];
let supported_exts = ["nix", "toml", "json", "yaml", "json5", "ron", "ini"];
let locations: Vec<std::path::PathBuf> = location_bases.iter().flat_map(|base| supported_exts.iter().map(move |ext| std::path::PathBuf::from(format!("{base}.{ext}")))).collect();
let mut builder = ::v_utils::__internal::config::Config::builder().add_source(::v_utils::__internal::config::Environment::with_prefix(app_name).separator("__"));
let mut err_msg = "Could not construct config from aggregated sources (conf, env, flags).".to_owned();
#[allow(unused_imports)]
use ::v_utils::__internal::eyre::WrapErr as _;
let (raw, file_config, config_path): (::v_utils::__internal::config::Config, Option<::v_utils::__internal::config::Config>, Option<std::path::PathBuf>) = match path {
Some(path) => {
if path.to_str().map(|s| s.ends_with(".nix")).unwrap_or(false) {
let json_str = Self::eval_nix_file(path.to_str().unwrap())?;
let file_builder = ::v_utils::__internal::config::Config::builder().add_source(::v_utils::__internal::config::File::from_str(&json_str, ::v_utils::__internal::config::FileFormat::Json));
let file_only = file_builder.clone().build().ok();
let builder = builder.add_source(::v_utils::__internal::config::File::from_str(&json_str, ::v_utils::__internal::config::FileFormat::Json)).add_source(flags.clone());
(builder.build()?, file_only, Some(path))
} else {
let file_builder = ::v_utils::__internal::config::Config::builder().add_source(::v_utils::__internal::config::File::from(path.clone()).required(true));
let file_only = file_builder.clone().build().ok();
let builder = builder.add_source(::v_utils::__internal::config::File::from(path.clone()).required(true)).add_source(flags.clone());
(builder.build()?, file_only, Some(path))
}
}
None => {
let conf_files_found: Vec<_> = locations.iter().filter(|p| p.exists()).collect();
match conf_files_found.len() {
0 => {
eprintln!("warning: no config file found for `{config_name}`, building from env + flags only. Searched in {locations:?}");
err_msg.push_str(&format!("\nNOTE: conf file is missing. Searched in {:?}", locations));
(builder.add_source(flags.clone()).build()?, None, None)
},
1 => {
let found_path = conf_files_found[0];
if found_path.extension().map(|e| e == "nix").unwrap_or(false) {
let json_str = Self::eval_nix_file(found_path.to_str().unwrap())?;
let file_builder = ::v_utils::__internal::config::Config::builder().add_source(::v_utils::__internal::config::File::from_str(&json_str, ::v_utils::__internal::config::FileFormat::Json));
let file_only = file_builder.clone().build().ok();
let builder = builder.add_source(::v_utils::__internal::config::File::from_str(&json_str, ::v_utils::__internal::config::FileFormat::Json)).add_source(flags.clone());
(builder.build()?, file_only, Some(found_path.clone()))
} else {
let file_builder = ::v_utils::__internal::config::Config::builder().add_source(::v_utils::__internal::config::File::from(found_path.as_path()).required(true));
let file_only = file_builder.clone().build().ok();
builder = builder.add_source(::v_utils::__internal::config::File::from(found_path.as_path()).required(true)).add_source(flags.clone());
(builder.build()?, file_only, Some(found_path.clone()))
}
},
_ => {
return Err(::v_utils::__internal::SettingsError::MultipleConfigs {
paths: conf_files_found.into_iter().cloned().collect(),
}.into());
}
}
}
};
if let Some(ref file_cfg) = file_config {
Self::warn_unknown_fields(file_cfg);
}
{
let mut unset = Vec::new();
if let Ok(table) = raw.clone().try_deserialize::<std::collections::HashMap<String, ::v_utils::__internal::serde_json::Value>>() {
for (key, value) in &table {
Self::collect_placeholder_paths(value, key.clone(), &mut unset);
}
}
if !unset.is_empty() {
unset.sort();
return Err(::v_utils::__internal::SettingsError::Unset { paths: unset, config_path });
}
}
match raw.try_deserialize() {
Ok(config) => Ok(config),
Err(e) => {
let error_str = e.to_string();
if allow_extend {
if let Some(missing_field) = Self::parse_missing_field(&error_str) {
if let Some(ref config_path) = config_path {
use __settings_default_provider::GetDefault as _;
let wrapper = __settings_default_provider::Wrapper::<Self>(std::marker::PhantomData);
if let Some(default_value) = (&wrapper).get_default_for_path(&missing_field) {
let prompt = format!(
"Missing configuration field \"{}\". Extend config with default value {}?",
missing_field,
default_value
);
if flags.yes || matches!(::v_utils::io::confirmation(&prompt).flush_blocking(), ::v_utils::io::ConfirmResult::Yes) {
if let Err(extend_err) = Self::extend_config_file(config_path, &missing_field, &default_value) {
eprintln!("Warning: Failed to extend config: {}", extend_err);
} else {
eprintln!("Extended config with default for \"{}\"", missing_field);
return Self::try_build_internal(flags, true);
}
}
}
}
}
}
Err(::v_utils::__internal::eyre::eyre!("{}\n\nRoot cause: {}", err_msg, e).into())
}
}
}
fn warn_unknown_fields(file_config: &::v_utils::__internal::config::Config) {
use std::collections::{HashMap, HashSet};
let known_fields: HashSet<&str> = [#(#field_name_strings),*].iter().copied().collect();
if let Ok(table) = file_config.clone().try_deserialize::<HashMap<String, ::v_utils::__internal::serde_json::Value>>() {
for field_name in table.keys() {
if !known_fields.contains(field_name.as_str()) {
eprintln!("warning: unknown configuration field '{field_name}' will be ignored");
}
}
}
}
fn collect_placeholder_paths(
value: &::v_utils::__internal::serde_json::Value,
path: String,
found: &mut Vec<String>,
) {
use ::v_utils::__internal::serde_json::Value;
match value {
Value::String(s) if s == ::v_utils::__internal::REQUIRED_PLACEHOLDER => found.push(path),
Value::Object(map) => {
for (key, nested) in map {
Self::collect_placeholder_paths(nested, format!("{path}.{key}"), found);
}
}
_ => {}
}
}
fn eval_nix_file(path: &str) -> Result<String, ::v_utils::__internal::eyre::Report> {
use ::v_utils::__internal::eyre::WrapErr as _;
if std::fs::read_to_string(path).map(|s| s.trim().is_empty()).unwrap_or(false) {
return Err(::v_utils::__internal::eyre::eyre!(
"Config file `{path}` is empty. Delete it to write a fresh default config, or fill in valid Nix."
));
}
let output = std::process::Command::new("nix")
.arg("eval")
.arg("--json")
.arg("--impure")
.arg("--expr")
.arg(format!("import {}", path))
.output()
.wrap_err("Failed to execute nix command. Is nix installed?")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(::v_utils::__internal::eyre::eyre!("Nix evaluation failed: {}", stderr));
}
Ok(String::from_utf8(output.stdout)?)
}
fn parse_missing_field(error_str: &str) -> Option<String> {
if let Some(start) = error_str.find("missing configuration field \"") {
let rest = &error_str[start + 29..]; if let Some(end) = rest.find('"') {
return Some(rest[..end].to_string());
}
}
if let Some(start) = error_str.find("missing field `") {
let rest = &error_str[start + 15..]; if let Some(end) = rest.find('`') {
return Some(rest[..end].to_string());
}
}
None
}
fn extend_config_file(
config_path: &std::path::Path,
field_path: &str,
value: &::v_utils::__internal::serde_json::Value,
) -> Result<(), ::v_utils::__internal::eyre::Report> {
use ::v_utils::__internal::eyre::WrapErr as _;
let ext = config_path.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"toml" => Self::extend_toml_file(config_path, field_path, value),
"nix" => Self::extend_nix_file(config_path, field_path, value),
_ => Err(::v_utils::__internal::eyre::eyre!(
"Extending config not supported for format: {}",
ext
)),
}
}
fn extend_toml_file(
config_path: &std::path::Path,
field_path: &str,
value: &::v_utils::__internal::serde_json::Value,
) -> Result<(), ::v_utils::__internal::eyre::Report> {
use ::v_utils::__internal::eyre::WrapErr as _;
let content = std::fs::read_to_string(config_path)
.wrap_err_with(|| format!("Failed to read config file: {}", config_path.display()))?;
let mut doc: ::v_utils::__internal::toml::Table = content.parse()
.wrap_err("Failed to parse TOML config")?;
let parts: Vec<&str> = field_path.split('.').collect();
Self::set_toml_value(&mut doc, &parts, value)?;
let new_content = ::v_utils::__internal::toml::to_string_pretty(&doc)
.wrap_err("Failed to serialize TOML")?;
std::fs::write(config_path, new_content)
.wrap_err_with(|| format!("Failed to write config file: {}", config_path.display()))?;
Ok(())
}
fn set_toml_value(
table: &mut ::v_utils::__internal::toml::Table,
path: &[&str],
value: &::v_utils::__internal::serde_json::Value,
) -> Result<(), ::v_utils::__internal::eyre::Report> {
if path.is_empty() {
return Err(::v_utils::__internal::eyre::eyre!("Empty path"));
}
if path.len() == 1 {
let toml_value = Self::json_to_toml(value)?;
table.insert(path[0].to_string(), toml_value);
Ok(())
} else {
let key = path[0];
let nested = table.entry(key.to_string())
.or_insert_with(|| ::v_utils::__internal::toml::Value::Table(::v_utils::__internal::toml::Table::new()));
if let ::v_utils::__internal::toml::Value::Table(ref mut nested_table) = nested {
Self::set_toml_value(nested_table, &path[1..], value)
} else {
Err(::v_utils::__internal::eyre::eyre!(
"Expected table at '{}', found different type",
key
))
}
}
}
fn json_to_toml(json: &::v_utils::__internal::serde_json::Value) -> Result<::v_utils::__internal::toml::Value, ::v_utils::__internal::eyre::Report> {
use ::v_utils::__internal::serde_json::Value as JsonValue;
use ::v_utils::__internal::toml::Value as TomlValue;
Ok(match json {
JsonValue::Null => return Err(::v_utils::__internal::eyre::eyre!("TOML doesn't support null values")),
JsonValue::Bool(b) => TomlValue::Boolean(*b),
JsonValue::Number(n) => {
if let Some(i) = n.as_i64() {
TomlValue::Integer(i)
} else if let Some(f) = n.as_f64() {
TomlValue::Float(f)
} else {
return Err(::v_utils::__internal::eyre::eyre!("Unsupported number type"));
}
}
JsonValue::String(s) => TomlValue::String(s.clone()),
JsonValue::Array(arr) => {
let toml_arr: Result<Vec<_>, _> = arr.iter().map(Self::json_to_toml).collect();
TomlValue::Array(toml_arr?)
}
JsonValue::Object(obj) => {
let mut table = ::v_utils::__internal::toml::Table::new();
for (k, v) in obj {
table.insert(k.clone(), Self::json_to_toml(v)?);
}
TomlValue::Table(table)
}
})
}
fn extend_nix_file(
config_path: &std::path::Path,
field_path: &str,
value: &::v_utils::__internal::serde_json::Value,
) -> Result<(), ::v_utils::__internal::eyre::Report> {
use ::v_utils::__internal::eyre::WrapErr as _;
let content = std::fs::read_to_string(config_path)
.wrap_err_with(|| format!("Failed to read config file: {}", config_path.display()))?;
let parts: Vec<&str> = field_path.split('.').collect();
let nix_value = Self::json_to_nix(value);
let new_content = Self::insert_nix_field(&content, &parts, &nix_value)?;
std::fs::write(config_path, new_content)
.wrap_err_with(|| format!("Failed to write config file: {}", config_path.display()))?;
Ok(())
}
fn json_to_nix(json: &::v_utils::__internal::serde_json::Value) -> String {
use ::v_utils::__internal::serde_json::Value as JsonValue;
match json {
JsonValue::Null => "null".to_string(),
JsonValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
JsonValue::Number(n) => n.to_string(),
JsonValue::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
JsonValue::Array(arr) => {
let items: Vec<String> = arr.iter().map(Self::json_to_nix).collect();
format!("[ {} ]", items.join(" "))
}
JsonValue::Object(obj) => {
let items: Vec<String> = obj.iter()
.map(|(k, v)| format!("{} = {};", k, Self::json_to_nix(v)))
.collect();
format!("{{ {} }}", items.join(" "))
}
}
}
fn insert_nix_field(content: &str, path: &[&str], nix_value: &str) -> Result<String, ::v_utils::__internal::eyre::Report> {
if path.is_empty() {
return Err(::v_utils::__internal::eyre::eyre!("Empty path"));
}
if path.len() == 1 {
Self::insert_at_level(content, path[0], nix_value, 0)
} else {
let parent_key = path[0];
let remaining_path = &path[1..];
if let Some(parent_start) = content.find(&format!("{} = {{", parent_key))
.or_else(|| content.find(&format!("{}={{", parent_key)))
.or_else(|| content.find(&format!("{} ={{", parent_key)))
.or_else(|| content.find(&format!("{}= {{", parent_key)))
{
let brace_pos = content[parent_start..].find('{')
.map(|p| parent_start + p)
.ok_or_else(|| ::v_utils::__internal::eyre::eyre!("Malformed Nix: no opening brace for {}", parent_key))?;
let (block_content, close_pos) = Self::find_matching_brace(&content[brace_pos..])?;
let close_pos = brace_pos + close_pos;
let updated_block = Self::insert_nix_field(&format!("{{{}}}", block_content), remaining_path, nix_value)?;
Ok(format!(
"{}{}{}",
&content[..brace_pos],
updated_block,
&content[close_pos + 1..]
))
} else {
let full_nix_value = Self::build_nested_nix(remaining_path, nix_value);
Self::insert_at_level(content, parent_key, &full_nix_value, 0)
}
}
}
fn build_nested_nix(path: &[&str], value: &str) -> String {
if path.is_empty() {
value.to_string()
} else if path.len() == 1 {
format!("{{ {} = {}; }}", path[0], value)
} else {
let inner = Self::build_nested_nix(&path[1..], value);
format!("{{ {} = {}; }}", path[0], inner)
}
}
fn find_matching_brace(s: &str) -> Result<(String, usize), ::v_utils::__internal::eyre::Report> {
let chars: Vec<char> = s.chars().collect();
if chars.is_empty() || chars[0] != '{' {
return Err(::v_utils::__internal::eyre::eyre!("Expected opening brace"));
}
let mut depth = 0;
let mut in_string = false;
let mut escape_next = false;
for (i, &c) in chars.iter().enumerate() {
if escape_next {
escape_next = false;
continue;
}
if c == '\\' && in_string {
escape_next = true;
continue;
}
if c == '"' {
in_string = !in_string;
continue;
}
if in_string {
continue;
}
if c == '{' {
depth += 1;
} else if c == '}' {
depth -= 1;
if depth == 0 {
let content: String = chars[1..i].iter().collect();
return Ok((content, i));
}
}
}
Err(::v_utils::__internal::eyre::eyre!("No matching closing brace found"))
}
fn insert_at_level(content: &str, key: &str, value: &str, _target_depth: usize) -> Result<String, ::v_utils::__internal::eyre::Report> {
let chars: Vec<char> = content.chars().collect();
let mut depth = 0;
let mut in_string = false;
let mut escape_next = false;
let mut last_close_at_depth = None;
for (i, &c) in chars.iter().enumerate() {
if escape_next {
escape_next = false;
continue;
}
if c == '\\' && in_string {
escape_next = true;
continue;
}
if c == '"' {
in_string = !in_string;
continue;
}
if in_string {
continue;
}
if c == '{' {
depth += 1;
} else if c == '}' {
if depth == 1 {
last_close_at_depth = Some(i);
}
depth -= 1;
}
}
if let Some(pos) = last_close_at_depth {
let (before, after) = content.split_at(pos);
let insertion = format!(" {} = {};\n", key, value);
let needs_newline = !before.ends_with('\n') && !before.ends_with('{');
let prefix = if needs_newline { "\n" } else { "" };
Ok(format!("{}{}{}{}", before, prefix, insertion, after))
} else {
Err(::v_utils::__internal::eyre::eyre!("Could not find insertion point in Nix file"))
}
}
pub fn diff_from_defaults(&self) -> Option<String> {
use __settings_default_provider::ComputeDiff as _;
let wrapper = __settings_default_provider::Wrapper::<Self>(std::marker::PhantomData);
(&wrapper).compute_diff(self)
}
pub fn write_schema() -> Result<std::path::PathBuf, ::v_utils::__internal::eyre::Report> {
use __settings_default_provider::GetSchema as _;
use ::v_utils::__internal::eyre::WrapErr as _;
let wrapper = __settings_default_provider::Wrapper::<Self>(std::marker::PhantomData);
let schema = (&wrapper).get_schema()
.ok_or_else(|| ::v_utils::__internal::eyre::eyre!(
"write_schema requires `{}` to `#[derive(schemars::JsonSchema)]`",
std::any::type_name::<Self>(),
))?;
let config_name = #config_name_expr;
#xdg_conf_dir
let schema_path = std::path::PathBuf::from(format!("{xdg_conf_dir}/{config_name}.schema.json"));
if let Some(parent) = schema_path.parent() {
std::fs::create_dir_all(parent)
.wrap_err_with(|| format!("Failed to create config directory: {}", parent.display()))?;
}
std::fs::write(&schema_path, schema)
.wrap_err_with(|| format!("Failed to write schema file: {}", schema_path.display()))?;
Ok(schema_path)
}
pub fn write_module() -> Result<std::path::PathBuf, ::v_utils::__internal::eyre::Report> {
use __settings_default_provider::GetSchema as _;
use ::v_utils::__internal::eyre::WrapErr as _;
let wrapper = __settings_default_provider::Wrapper::<Self>(std::marker::PhantomData);
let schema_str = (&wrapper).get_schema()
.ok_or_else(|| ::v_utils::__internal::eyre::eyre!(
"write_module requires `{}` to `#[derive(schemars::JsonSchema)]`",
std::any::type_name::<Self>(),
))?;
let schema: ::v_utils::__internal::serde_json::Value = ::v_utils::__internal::serde_json::from_str(&schema_str)
.wrap_err("schemars produced invalid JSON")?;
let module = ::v_utils::__internal::schema_to_nix_module(&schema)?;
let config_name = #config_name_expr;
#xdg_conf_dir
let module_path = std::path::PathBuf::from(format!("{xdg_conf_dir}/{config_name}.module.nix"));
if let Some(parent) = module_path.parent() {
std::fs::create_dir_all(parent)
.wrap_err_with(|| format!("Failed to create config directory: {}", parent.display()))?;
}
std::fs::write(&module_path, module)
.wrap_err_with(|| format!("Failed to write module file: {}", module_path.display()))?;
Ok(module_path)
}
fn defaults_with_placeholders() -> Result<(::v_utils::__internal::serde_json::Value, Vec<&'static str>), ::v_utils::__internal::eyre::Report> {
use __settings_default_provider::{GetDefaults as _, HasDefault as _, HasSerialize as _};
let wrapper = __settings_default_provider::Wrapper::<Self>(std::marker::PhantomData);
if let Some(defaults) = (&wrapper).get_defaults() {
return Ok((defaults, Vec::new()));
}
if (&wrapper).has_default() && (&wrapper).has_serialize() {
return Err(::v_utils::__internal::eyre::eyre!(
"write_defaults: `{}` implements Default + Serialize, but `serde_json::to_value(&Self::default())` returned Err (likely a Serialize impl rejecting some value, e.g. non-string map key or `f64::NAN`)",
std::any::type_name::<Self>()
));
}
let mut map = ::v_utils::__internal::serde_json::Map::new();
let mut unset: Vec<&'static str> = Vec::new();
#(#field_default_entries)*
Ok((::v_utils::__internal::serde_json::Value::Object(map), unset))
}
pub fn write_defaults() -> Result<std::path::PathBuf, ::v_utils::__internal::eyre::Report> {
use ::v_utils::__internal::eyre::WrapErr as _;
let (defaults, unset) = Self::defaults_with_placeholders()?;
for field in &unset {
eprintln!(
"warning: `{field}` has no default — written as \"{}\". Open the config and set it by hand.",
::v_utils::__internal::REQUIRED_PLACEHOLDER
);
}
let config_name = #config_name_expr;
#xdg_conf_dir
let location_bases = [
format!("{xdg_conf_dir}/{config_name}"),
format!("{xdg_conf_dir}/{config_name}/config"),
];
let supported_exts = ["nix", "toml", "json", "yaml", "json5", "ron", "ini"];
let existing_config: Option<std::path::PathBuf> = location_bases.iter()
.flat_map(|base| supported_exts.iter().map(move |ext| std::path::PathBuf::from(format!("{base}.{ext}"))))
.find(|p| p.exists());
match existing_config {
Some(config_path) => {
Self::merge_defaults_into_config(&config_path, &defaults)?;
Ok(config_path)
}
None => {
let new_config_path = std::path::PathBuf::from(format!("{xdg_conf_dir}/{config_name}.nix"));
if let Some(parent) = new_config_path.parent() {
std::fs::create_dir_all(parent)
.wrap_err_with(|| format!("Failed to create config directory: {}", parent.display()))?;
}
let nix_content = Self::json_to_nix_file(&defaults);
std::fs::write(&new_config_path, nix_content)
.wrap_err_with(|| format!("Failed to write config file: {}", new_config_path.display()))?;
Ok(new_config_path)
}
}
}
fn merge_defaults_into_config(
config_path: &std::path::Path,
defaults: &::v_utils::__internal::serde_json::Value,
) -> Result<(), ::v_utils::__internal::eyre::Report> {
use ::v_utils::__internal::eyre::WrapErr as _;
let ext = config_path.extension().and_then(|e| e.to_str()).unwrap_or("");
let existing_json: ::v_utils::__internal::serde_json::Value = match ext {
"nix" => {
let json_str = Self::eval_nix_file(config_path.to_str().unwrap())?;
::v_utils::__internal::serde_json::from_str(&json_str)
.wrap_err("Failed to parse Nix config as JSON")?
}
"toml" => {
let content = std::fs::read_to_string(config_path)
.wrap_err_with(|| format!("Failed to read config file: {}", config_path.display()))?;
let table: ::v_utils::__internal::toml::Table = content.parse()
.wrap_err("Failed to parse TOML config")?;
::v_utils::__internal::serde_json::to_value(&table)
.wrap_err("Failed to convert TOML to JSON")?
}
_ => {
let content = std::fs::read_to_string(config_path)
.wrap_err_with(|| format!("Failed to read config file: {}", config_path.display()))?;
::v_utils::__internal::serde_json::from_str(&content)
.wrap_err("Failed to parse config as JSON")?
}
};
let mut missing_fields = Vec::new();
Self::find_missing_fields(defaults, &existing_json, String::new(), &mut missing_fields);
if missing_fields.is_empty() {
return Ok(()); }
for (path, value) in missing_fields {
Self::extend_config_file(config_path, &path, &value)?;
}
Ok(())
}
fn find_missing_fields(
defaults: &::v_utils::__internal::serde_json::Value,
existing: &::v_utils::__internal::serde_json::Value,
prefix: String,
missing: &mut Vec<(String, ::v_utils::__internal::serde_json::Value)>,
) {
use ::v_utils::__internal::serde_json::Value;
if let Value::Object(def_map) = defaults {
let existing_map = existing.as_object();
for (key, def_val) in def_map {
let path = if prefix.is_empty() {
key.clone()
} else {
format!("{}.{}", prefix, key)
};
match existing_map.and_then(|m| m.get(key)) {
Some(existing_val) => {
if def_val.is_object() && existing_val.is_object() {
Self::find_missing_fields(def_val, existing_val, path, missing);
}
}
None => {
missing.push((path, def_val.clone()));
}
}
}
}
}
fn json_to_nix_file(json: &::v_utils::__internal::serde_json::Value) -> String {
Self::json_to_nix_value(json, 0)
}
fn json_to_nix_value(json: &::v_utils::__internal::serde_json::Value, indent: usize) -> String {
use ::v_utils::__internal::serde_json::Value;
let indent_str = " ".repeat(indent);
let inner_indent = " ".repeat(indent + 1);
match json {
Value::Null => "null".to_string(),
Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
Value::Number(n) => n.to_string(),
Value::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
Value::Array(arr) => {
if arr.is_empty() {
"[]".to_string()
} else {
let items: Vec<String> = arr.iter()
.map(|v| format!("{}{}", inner_indent, Self::json_to_nix_value(v, indent + 1)))
.collect();
format!("[\n{}\n{}]", items.join("\n"), indent_str)
}
}
Value::Object(obj) => {
if obj.is_empty() {
"{}".to_string()
} else {
let items: Vec<String> = obj.iter()
.map(|(k, v)| format!("{}{} = {};", inner_indent, k, Self::json_to_nix_value(v, indent + 1)))
.collect();
format!("{{\n{}\n{}}}", items.join("\n"), indent_str)
}
}
}
}
}
};
let flag_quotes = fields.iter().filter_map(|field| {
let ty = &field.ty;
let field_attrs = SettingsFieldAttrs::parse(&field.attrs).expect("validated up front");
if field_attrs.skip_flag && field_attrs.skip_env {
return None;
}
if field_attrs.skip_flag {
return None;
}
let ident = &field.ident;
Some(match field_attrs.flatten {
true => {
let inner_type = if let syn::Type::Path(type_path) = ty {
if is_option_type(type_path) { extract_option_inner_type(type_path) } else { ty }
} else {
ty
};
quote! {
#[clap(flatten)]
#ident: <#inner_type as v_utils::macros::SettingsNested>::Flags,
}
}
false => {
let clap_ty = clap_compatible_option_wrapped_ty(ty);
let inner_type = match ty {
syn::Type::Path(type_path) if is_option_type(type_path) => extract_option_inner_type(type_path),
_ => ty,
};
let delimiter = match inner_type {
syn::Type::Path(type_path) if is_vec_type(type_path) => quote! { , value_delimiter = ',' },
_ => quote! {},
};
if use_env && !field_attrs.skip_env {
let env_var_name = AsShoutySnakeCase(ident.as_ref().unwrap().to_string()).to_string();
quote! {
#[arg(long, env = #env_var_name #delimiter)]
#ident: #clap_ty,
}
} else {
quote! {
#[arg(long #delimiter)]
#ident: #clap_ty,
}
}
}
})
});
let source_quotes = fields.iter().filter_map(|field| {
let ty = &field.ty;
let field_attrs = SettingsFieldAttrs::parse(&field.attrs).expect("validated up front");
if field_attrs.skip_flag {
return None;
}
let ident = &field.ident;
Some(match field_attrs.flatten {
true => {
let inner_type = if let syn::Type::Path(type_path) = ty {
if is_option_type(type_path) { extract_option_inner_type(type_path) } else { ty }
} else {
ty
};
quote! {
<#inner_type as v_utils::macros::SettingsNested>::collect_config(&self.#ident, &mut map);
}
}
false => {
let value_kind = clap_to_config(ident.as_ref().unwrap(), ty);
let field_name_string = format!("{}", ident.as_ref().unwrap());
quote! {
if let Some(#ident) = &self.#ident {
map.insert(
#field_name_string.to_owned(),
v_utils::__internal::config::Value::new(Some(&"flags".to_owned()), #value_kind),
);
}
}
}
})
});
let settings_args = quote_spanned! { proc_macro2::Span::call_site()=>
#[allow(dead_code)]
#[derive(Clone, Debug, Default, PartialEq, clap::Args)] pub struct SettingsFlags {
#[arg(short, long)]
config: Option<v_utils::io::ExpandedPath>,
#[arg(short, long)]
pub yes: bool,
#(#flag_quotes)*
}
impl v_utils::__internal::config::Source for SettingsFlags {
fn clone_into_box(&self) -> Box<dyn v_utils::__internal::config::Source + Send + Sync> {
Box::new((*self).clone())
}
fn collect(&self) -> Result<v_utils::__internal::config::Map<String, v_utils::__internal::config::Value>, v_utils::__internal::config::ConfigError> {
let mut map = v_utils::__internal::config::Map::new();
#(#source_quotes)*
if self.yes {
map.insert(
"yes".to_owned(),
v_utils::__internal::config::Value::new(Some(&"flags".to_owned()), v_utils::__internal::config::ValueKind::Boolean(true)),
);
}
Ok(map)
}
}
};
let settings_command = quote_spanned! { proc_macro2::Span::call_site()=>
#[derive(clap::Subcommand)]
pub enum SettingsCommand {
WriteDefaults,
Diff,
Schema,
Module,
}
};
let handle_command = quote! {
impl #name {
pub fn handle_settings_command(cmd: SettingsCommand, flags: SettingsFlags) -> ! {
match cmd {
SettingsCommand::WriteDefaults => match Self::write_defaults() {
Ok(path) => {
println!("Wrote defaults to: {}", path.display());
std::process::exit(0);
}
Err(e) => {
eprintln!("Failed to write defaults: {e}");
std::process::exit(1);
}
},
SettingsCommand::Diff => {
let config = match Self::try_build(flags) {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to load settings: {e}");
std::process::exit(1);
}
};
match config.diff_from_defaults() {
Some(diff) => println!("{diff}"),
None => println!("All settings match defaults"),
}
std::process::exit(0);
}
SettingsCommand::Schema => match Self::write_schema() {
Ok(path) => {
println!("Wrote schema to: {}", path.display());
std::process::exit(0);
}
Err(e) => {
eprintln!("Failed to write schema: {e}");
std::process::exit(1);
}
},
SettingsCommand::Module => match Self::write_module() {
Ok(path) => {
println!("Wrote module to: {}", path.display());
std::process::exit(0);
}
Err(e) => {
eprintln!("Failed to write module: {e}");
std::process::exit(1);
}
},
}
}
}
};
let expanded = quote! {
#try_build
#settings_args
#settings_command
#handle_command
};
TokenStream::from(expanded)
}
#[proc_macro_derive(SettingsNested, attributes(settings))]
pub fn derive_settings_nested(input: TokenStream) -> TokenStream {
let input = strip_field_default_values(input);
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let snake_case_name = AsSnakeCase(name.to_string()).to_string();
let fields = if let Data::Struct(syn::DataStruct {
fields: Fields::Named(syn::FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
} else {
unimplemented!()
};
for field in fields {
if let Err(e) = SettingsFieldAttrs::parse(&field.attrs) {
return e.to_compile_error().into();
}
}
let mut prefix = None;
let mut use_env = false;
for attr in &ast.attrs {
if !attr.path().is_ident("settings") {
continue;
}
let parsed = attr.parse_args_with(|input: syn::parse::ParseStream| {
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
if ident == "prefix" {
let _: Token![=] = input.parse()?;
let lit: syn::LitStr = input.parse()?;
prefix = Some(lit.value());
} else if ident == "use_env" {
let _: Token![=] = input.parse()?;
let lit: syn::LitBool = input.parse()?;
use_env = lit.value;
} else {
return Err(unknown_attr_ident(&ident, &["prefix", "use_env"]));
}
let _ = input.parse::<Option<Token![,]>>();
}
Ok(())
});
if let Err(e) = parsed {
return e.to_compile_error().into();
}
}
let prefix = prefix.unwrap_or(snake_case_name);
let config_prefix = prefix.replace('_', ".");
let prefixed_flags = fields.iter().filter_map(|field| {
let ident = &field.ident;
let ty = &field.ty;
let field_attrs = SettingsFieldAttrs::parse(&field.attrs).expect("validated up front");
if field_attrs.skip_flag {
return None;
}
if field_attrs.flatten {
let inner_type = if let syn::Type::Path(type_path) = ty {
if is_option_type(type_path) { extract_option_inner_type(type_path) } else { ty }
} else {
ty
};
Some(quote! {
#[clap(flatten)]
#ident: <#inner_type as v_utils::macros::SettingsNested>::Flags,
})
} else {
let clap_ty = clap_compatible_option_wrapped_ty(ty);
let prefixed_field_name = format_ident!("{}_{}", prefix, ident.as_ref().unwrap());
if use_env && !field_attrs.skip_env {
let env_var_name = AsShoutySnakeCase(prefixed_field_name.to_string()).to_string();
Some(quote! {
#[arg(long, env = #env_var_name)]
#prefixed_field_name: #clap_ty,
})
} else {
Some(quote! {
#[arg(long)]
#prefixed_field_name: #clap_ty,
})
}
}
});
let config_inserts = fields.iter().filter_map(|field| {
let ident = &field.ident;
let ty = &field.ty;
let field_attrs = SettingsFieldAttrs::parse(&field.attrs).expect("validated up front");
if field_attrs.skip_flag {
return None;
}
if field_attrs.flatten {
let inner_type = if let syn::Type::Path(type_path) = ty {
if is_option_type(type_path) { extract_option_inner_type(type_path) } else { ty }
} else {
ty
};
Some(quote! {
<#inner_type as v_utils::macros::SettingsNested>::collect_config(&flags.#ident, map);
})
} else {
let config_value_kind = clap_to_config(ident.as_ref().unwrap(), ty);
let prefixed_field_name = format_ident!("{}_{}", prefix, ident.as_ref().unwrap());
let config_value_path = format!("{config_prefix}.{}", ident.as_ref().unwrap());
let source_tag = format!("flags:{prefix}");
Some(quote! {
if let Some(#ident) = &flags.#prefixed_field_name {
map.insert(
#config_value_path.to_owned(),
v_utils::__internal::config::Value::new(Some(&#source_tag.to_owned()), #config_value_kind),
);
}
})
}
});
let produced_struct_name = format_ident!("__SettingsNested{name}");
let expanded = quote! {
#[allow(dead_code)]
#[doc(hidden)]
#[derive(Clone, Debug, Default, PartialEq, clap::Args)]
pub struct #produced_struct_name {
#(#prefixed_flags)*
}
impl v_utils::macros::SettingsNested for #name {
type Flags = #produced_struct_name;
fn collect_config(flags: &Self::Flags, map: &mut v_utils::__internal::config::Map<String, v_utils::__internal::config::Value>) {
#(#config_inserts)*
}
}
};
TokenStream::from(expanded)
}
#[cfg(feature = "cli")]
#[proc_macro_derive(LiveSettings)]
pub fn derive_live_settings(input: TokenStream) -> TokenStream {
let input = strip_field_default_values(input);
let ast = parse_macro_input!(input as syn::DeriveInput);
let name = &ast.ident;
let mut config_name: Option<String> = None;
for attr in &ast.attrs {
if !attr.path().is_ident("settings") {
continue;
}
let _ = attr.parse_args_with(|input: syn::parse::ParseStream| {
loop {
let ident: syn::Ident = input.parse()?;
let _: Token![=] = input.parse()?;
if ident == "config_name" {
let lit: syn::LitStr = input.parse()?;
config_name = Some(lit.value());
} else {
input.parse::<proc_macro2::TokenTree>()?;
}
if input.is_empty() {
return Ok(());
}
let _: Token![,] = input.parse()?;
}
});
}
let config_name_expr = match &config_name {
Some(s) => quote! { #s },
None => quote! { env!("CARGO_PKG_NAME") },
};
#[cfg(feature = "xdg")]
let xdg_conf_dir = quote_spanned! { proc_macro2::Span::call_site()=>
let xdg_dirs = ::v_utils::__internal::xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"));
let xdg_conf_dir = xdg_dirs.get_config_home().unwrap().parent().unwrap().display().to_string();
};
#[cfg(not(feature = "xdg"))]
let xdg_conf_dir = quote_spanned! { proc_macro2::Span::call_site()=>
let xdg_conf_dir = ::v_utils::__internal::xdg_config_fallback();
};
let expanded = quote! {
#[derive(Clone)]
pub struct LiveSettings {
config_path: Option<std::path::PathBuf>,
inner: std::sync::Arc<std::sync::RwLock<__LiveSettingsTimeCapsule>>,
flags: SettingsFlags,
}
struct __LiveSettingsTimeCapsule {
value: #name,
loaded_at: std::time::SystemTime,
update_freq: std::time::Duration,
}
impl std::fmt::Debug for LiveSettings {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LiveSettings").field("config_path", &self.config_path).finish()
}
}
impl LiveSettings {
pub fn new(flags: SettingsFlags, update_freq: std::time::Duration) -> ::v_utils::__internal::eyre::Result<Self> {
let config_path = Self::resolve_config_path(&flags)?;
let settings = #name::try_build(flags.clone())?;
Ok(Self {
config_path,
inner: std::sync::Arc::new(std::sync::RwLock::new(__LiveSettingsTimeCapsule {
value: settings,
loaded_at: std::time::SystemTime::now(),
update_freq,
})),
flags,
})
}
fn resolve_config_path(flags: &SettingsFlags) -> Result<Option<std::path::PathBuf>, ::v_utils::__internal::SettingsError> {
if let Some(ref path) = flags.config {
return Ok(Some(path.0.clone()));
}
let config_name = #config_name_expr;
#xdg_conf_dir
let location_bases = [
format!("{xdg_conf_dir}/{config_name}"),
format!("{xdg_conf_dir}/{config_name}/config"),
];
let supported_exts = ["nix", "toml", "json", "yaml", "json5", "ron", "ini"];
let mut found: Vec<std::path::PathBuf> = Vec::new();
for base in location_bases.iter() {
for ext in supported_exts.iter() {
let path = std::path::PathBuf::from(format!("{base}.{ext}"));
if path.exists() {
found.push(path);
}
}
}
match found.len() {
0 => Ok(None),
1 => Ok(Some(found.into_iter().next().unwrap())),
_ => Err(::v_utils::__internal::SettingsError::MultipleConfigs { paths: found }),
}
}
pub fn config(&self) -> Result<#name, ::v_utils::__internal::SettingsError> {
Self::resolve_config_path(&self.flags)?;
let now = std::time::SystemTime::now();
let should_reload = {
let capsule = self.inner.read().unwrap();
let age = now.duration_since(capsule.loaded_at).unwrap_or_default();
if age < capsule.update_freq {
return Ok(capsule.value.clone());
}
self.config_path
.as_ref()
.and_then(|path| std::fs::metadata(path).ok())
.and_then(|meta| meta.modified().ok())
.map(|file_mtime| {
let since_file_change = now.duration_since(file_mtime).unwrap_or_default();
since_file_change < age
})
.unwrap_or(false)
};
if should_reload {
if let Ok(new_settings) = #name::try_build(self.flags.clone()) {
let mut capsule = self.inner.write().unwrap();
capsule.value = new_settings;
capsule.loaded_at = now;
} else {
let mut capsule = self.inner.write().unwrap();
capsule.loaded_at = now;
}
} else {
let mut capsule = self.inner.write().unwrap();
capsule.loaded_at = now;
}
Ok(self.inner.read().unwrap().value.clone())
}
}
};
TokenStream::from(expanded)
}
fn strip_field_default_values(input: TokenStream) -> TokenStream {
use proc_macro2::{Delimiter, Group, TokenStream as TS2, TokenTree};
fn process(stream: TS2, inside_struct_body: bool) -> TS2 {
let mut out = TS2::new();
let mut iter = stream.into_iter().peekable();
let mut saw_struct_or_enum = false;
let mut body_done = false;
while let Some(tt) = iter.next() {
match &tt {
TokenTree::Ident(id) if !inside_struct_body && (id == "struct" || id == "enum") => {
saw_struct_or_enum = true;
out.extend([tt]);
}
TokenTree::Group(g) if !inside_struct_body && saw_struct_or_enum && !body_done && g.delimiter() == Delimiter::Brace => {
let inner = process(g.stream(), true);
let mut ng = Group::new(Delimiter::Brace, inner);
ng.set_span(g.span());
out.extend([TokenTree::Group(ng)]);
body_done = true;
}
TokenTree::Group(g) if inside_struct_body && g.delimiter() == Delimiter::Brace => {
let inner = process(g.stream(), true);
let mut ng = Group::new(Delimiter::Brace, inner);
ng.set_span(g.span());
out.extend([TokenTree::Group(ng)]);
}
TokenTree::Punct(p) if inside_struct_body && p.as_char() == '=' => {
while let Some(next) = iter.peek() {
if let TokenTree::Punct(p2) = next {
if p2.as_char() == ',' {
break;
}
}
iter.next();
}
}
_ => out.extend([tt]),
}
}
out
}
process(input.into(), false).into()
}
fn strip_field_defaults(input: TokenStream) -> (TokenStream, std::collections::HashMap<String, String>) {
use proc_macro::{Delimiter, TokenTree};
let mut defaults = std::collections::HashMap::new();
let tokens: Vec<TokenTree> = input.into_iter().collect();
let mut output = Vec::new();
for tt in &tokens {
match tt {
TokenTree::Group(g) if g.delimiter() == Delimiter::Brace => {
let inner = strip_fields_in_brace(g.stream(), &mut defaults);
let mut new_group = proc_macro::Group::new(Delimiter::Brace, inner);
new_group.set_span(g.span());
output.push(TokenTree::Group(new_group));
}
other => output.push(other.clone()),
}
}
(output.into_iter().collect(), defaults)
}
fn strip_fields_in_brace(stream: TokenStream, defaults: &mut std::collections::HashMap<String, String>) -> TokenStream {
use proc_macro::{Spacing, TokenTree};
let tokens: Vec<TokenTree> = stream.into_iter().collect();
let mut output: Vec<TokenTree> = Vec::new();
let mut i = 0;
let mut current_field_name: Option<String> = None;
let mut saw_colon = false;
while i < tokens.len() {
match &tokens[i] {
TokenTree::Ident(id) => {
if i + 1 < tokens.len() {
if let TokenTree::Punct(p) = &tokens[i + 1] {
if p.as_char() == ':' && p.spacing() == Spacing::Alone {
current_field_name = Some(id.to_string());
saw_colon = false;
}
}
}
output.push(tokens[i].clone());
i += 1;
}
TokenTree::Punct(p) if p.as_char() == ':' && p.spacing() == Spacing::Alone => {
saw_colon = true;
output.push(tokens[i].clone());
i += 1;
}
TokenTree::Punct(p) if p.as_char() == '=' && saw_colon => {
i += 1; let mut expr_tokens = Vec::new();
while i < tokens.len() {
if let TokenTree::Punct(p) = &tokens[i] {
if p.as_char() == ',' {
break;
}
}
expr_tokens.push(tokens[i].clone());
i += 1;
}
if let Some(ref name) = current_field_name {
let expr_str: TokenStream = expr_tokens.into_iter().collect();
defaults.insert(name.clone(), expr_str.to_string());
}
saw_colon = false;
}
TokenTree::Punct(p) if p.as_char() == ',' => {
saw_colon = false;
current_field_name = None;
output.push(tokens[i].clone());
i += 1;
}
TokenTree::Group(g) => {
output.push(TokenTree::Group(g.clone()));
i += 1;
}
_ => {
output.push(tokens[i].clone());
i += 1;
}
}
}
output.into_iter().collect()
}
fn _dbg_token_stream(expanded: proc_macro2::TokenStream, name: &str) -> proc_macro2::TokenStream {
let fpath = format!("/tmp/{}_expanded/{name}.rs", env!("CARGO_PKG_NAME"));
std::fs::create_dir_all(PathBuf::from(&fpath).parent().unwrap()).unwrap();
std::fs::write(&fpath, expanded.to_string()).unwrap();
std::process::Command::new("rustfmt").arg("--edition=2024").arg(&fpath).output().unwrap();
quote! {include!(#fpath); }
}
macro_rules! _dbg_tree {
($target:expr) => {
let fpath = concat!("/tmp/", env!("CARGO_PKG_NAME"), "_dbg.rs");
let dbg_str = format!("{:#?}", $target);
std::fs::write(fpath, dbg_str).unwrap();
};
}
fn is_option_type(type_path: &syn::TypePath) -> bool {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Option";
}
false
}
fn is_vec_type(type_path: &syn::TypePath) -> bool {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Vec";
}
false
}
fn is_type(type_path: &syn::TypePath, type_name: &str) -> bool {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == type_name;
}
false
}
fn extract_option_inner_type(type_path: &syn::TypePath) -> &syn::Type {
if let Some(segment) = type_path.path.segments.last() {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first() {
return inner_type;
}
}
}
panic!("Failed to extract inner type from Option")
}
fn extract_vec_inner_type(type_path: &syn::TypePath) -> &syn::Type {
if let Some(segment) = type_path.path.segments.last() {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first() {
return inner_type;
}
}
}
panic!("Failed to extract inner type from Vec")
}
fn _single_option_wrapped_ty(ty: &syn::Type) -> proc_macro2::TokenStream {
match ty {
syn::Type::Path(type_path) =>
if type_path.path.segments.last().unwrap().ident == "Option" {
quote! { #ty }
} else {
quote! { Option<#ty> }
},
_ => quote! { Option<#ty> },
}
}
fn unknown_attr_ident(ident: &syn::Ident, valid: &[&str]) -> syn::Error {
syn::Error::new(ident.span(), format!("unknown `{ident}`; valid values are: {}", valid.join(", ")))
}
#[derive(Default)]
struct SettingsFieldAttrs {
flatten: bool,
skip_flag: bool,
skip_env: bool,
default: Option<syn::Expr>,
}
impl SettingsFieldAttrs {
fn parse(attrs: &[syn::Attribute]) -> syn::Result<Self> {
let mut result = Self::default();
for attr in attrs {
if attr.path().is_ident("settings") {
attr.parse_args_with(|input: syn::parse::ParseStream| {
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
if ident == "flatten" {
result.flatten = true;
} else if ident == "default" {
let _: Token![=] = input.parse()?;
result.default = Some(input.parse()?);
} else if ident == "skip" {
if input.peek(token::Paren) {
let content;
syn::parenthesized!(content in input);
while !content.is_empty() {
let target: syn::Ident = content.parse()?;
if target == "flag" {
result.skip_flag = true;
} else if target == "env" {
result.skip_env = true;
} else {
return Err(unknown_attr_ident(&target, &["flag", "env"]));
}
let _ = content.parse::<Option<Token![,]>>();
}
} else {
result.skip_flag = true;
result.skip_env = true;
}
} else {
return Err(unknown_attr_ident(&ident, &["flatten", "skip", "skip(flag)", "skip(env)", "default"]));
}
let _ = input.parse::<Option<Token![,]>>();
}
Ok(())
})?;
}
}
Ok(result)
}
}
struct Field {
_parens: token::Paren,
index: LitInt,
_comma1: Token![,],
dtype: Ident,
_comma2: Token![,],
name: Ident,
}
impl Parse for Field {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let content;
Ok(Field {
_parens: syn::parenthesized!(content in input),
index: content.parse()?,
_comma1: content.parse()?,
dtype: content.parse()?,
_comma2: content.parse()?,
name: content.parse()?,
})
}
}
struct DataFrameDef {
values_vec: Ident,
_arrow: Token![=>],
fields: Vec<Field>,
}
impl Parse for DataFrameDef {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let values_vec: Ident = input.parse()?;
let _arrow: Token![=>] = input.parse()?;
let mut fields = Vec::new();
while !input.is_empty() {
fields.push(input.parse()?);
}
Ok(DataFrameDef { values_vec, _arrow, fields })
}
}
fn clap_to_config(ident: &syn::Ident, ty: &syn::Type) -> proc_macro2::TokenStream {
let inner_type = match ty {
syn::Type::Path(type_path) if is_option_type(type_path) => extract_option_inner_type(type_path),
_ => ty,
};
match inner_type {
syn::Type::Path(type_path) if is_type(type_path, "bool") => {
quote! { v_utils::__internal::config::ValueKind::Boolean(*#ident) }
}
syn::Type::Path(type_path) if is_vec_type(type_path) => {
quote! {
{
let mut array = Vec::new();
for item in #ident.iter() {
array.push(v_utils::__internal::config::Value::new(
None,
v_utils::__internal::config::ValueKind::String(item.to_string())
));
}
v_utils::__internal::config::ValueKind::Array(array)
}
}
}
_ => {
quote! { v_utils::__internal::config::ValueKind::String(#ident.to_string()) }
}
}
}
fn clap_compatible_option_wrapped_ty(ty: &syn::Type) -> proc_macro2::TokenStream {
let inner_type = match ty {
syn::Type::Path(type_path) if is_option_type(type_path) => extract_option_inner_type(type_path),
_ => ty,
};
match inner_type {
syn::Type::Path(type_path) if is_type(type_path, "bool") => {
quote! { Option<bool> }
}
syn::Type::Path(type_path) if is_vec_type(type_path) => {
let vec_inner = extract_vec_inner_type(type_path);
if let syn::Type::Path(inner_path) = vec_inner {
if is_type(inner_path, "bool") {
quote! { Option<Vec<bool>> }
} else {
quote! { Option<Vec<String>> }
}
} else {
quote! { Option<Vec<String>> }
}
}
_ => quote! { Option<String> },
}
}