#![deny(missing_docs)]
#![deny(unsafe_code)]
use proc_macro::TokenStream;
use proc_macro_crate::{FoundCrate, crate_name};
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
Attribute, Data, DeriveInput, Field, Fields, GenericArgument, Ident, Path,
PathArguments, Type, parse_macro_input,
};
fn kv_crate_path(container_attrs: &ContainerAttrs) -> TokenStream2 {
if let Some(path) = &container_attrs.crate_path {
return quote! { #path };
}
match crate_name("pkgsrc-kv") {
Ok(FoundCrate::Itself) => quote! { crate },
Ok(FoundCrate::Name(name)) => {
let ident = format_ident!("{}", name);
quote! { ::#ident }
}
Err(_) => quote! { ::pkgsrc_kv },
}
}
#[proc_macro_derive(Kv, attributes(kv))]
pub fn derive_kv(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match generate_impl(&input) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}
fn generate_impl(input: &DeriveInput) -> syn::Result<TokenStream2> {
let name = &input.ident;
let container_attrs = ContainerAttrs::parse(&input.attrs)?;
let kv = kv_crate_path(&container_attrs);
let fields = extract_named_fields(input)?;
let parsed_fields: Vec<ParsedField> = fields
.iter()
.map(ParsedField::from_field)
.collect::<syn::Result<_>>()?;
ensure_at_most_one(&parsed_fields, FieldKind::Collect, "collect")?;
let collect_field =
parsed_fields.iter().find(|f| f.kind == FieldKind::Collect);
let regular_fields: Vec<_> = parsed_fields
.iter()
.filter(|f| f.kind != FieldKind::Collect)
.collect();
let has_lenient = parsed_fields.iter().any(|f| f.lenient);
let warnings_ident = format_ident!("__kv_warnings");
let field_decls = generate_field_declarations(&parsed_fields);
let match_arms = generate_match_arms(
®ular_fields,
has_lenient.then_some(&warnings_ident),
&kv,
);
let unknown_handling = generate_unknown_handling(
container_attrs.allow_unknown,
collect_field,
&kv,
);
let field_extracts: Vec<_> =
parsed_fields.iter().map(|f| f.extract_expr(&kv)).collect();
let field_names: Vec<_> = parsed_fields.iter().map(|f| &f.ident).collect();
let serde_impl = if container_attrs.serde {
generate_serde_impl(name, &parsed_fields)
} else {
TokenStream2::new()
};
let parse_body = quote! {
use #kv::FromKv;
#(#field_decls)*
let input_start = input.as_ptr() as usize;
for line in input.lines() {
if line.is_empty() {
continue;
}
let line_offset = line.as_ptr() as usize - input_start;
let eq_pos = match line.find('=') {
Some(p) => p,
None => {
return Err(#kv::KvError::ParseLine(#kv::Span {
offset: line_offset,
len: line.len(),
}));
}
};
let key = &line[..eq_pos];
let value = &line[eq_pos + 1..];
let value_offset = line_offset + eq_pos + 1;
let value_span = #kv::Span {
offset: value_offset,
len: value.len(),
};
match key {
#(#match_arms)*
#unknown_handling
}
}
};
let construct = quote! {
#name {
#(#field_names: #field_extracts,)*
}
};
let parse_methods = if has_lenient {
quote! {
pub fn parse(input: &str) -> std::result::Result<Self, #kv::KvError> {
let mut #warnings_ident = Vec::new();
Self::parse_with_warnings(input, &mut #warnings_ident)
}
pub fn parse_with_warnings(
input: &str,
#warnings_ident: &mut Vec<#kv::KvWarning>,
) -> std::result::Result<Self, #kv::KvError> {
#parse_body
Ok(#construct)
}
}
} else {
quote! {
pub fn parse(input: &str) -> std::result::Result<Self, #kv::KvError> {
#parse_body
Ok(#construct)
}
}
};
Ok(quote! {
impl #name {
#parse_methods
}
#serde_impl
})
}
fn extract_named_fields(
input: &DeriveInput,
) -> syn::Result<&syn::punctuated::Punctuated<Field, syn::token::Comma>> {
let Data::Struct(data) = &input.data else {
return Err(syn::Error::new_spanned(
input,
"Kv derive only supports structs",
));
};
let Fields::Named(fields) = &data.fields else {
return Err(syn::Error::new_spanned(
input,
"Kv derive only supports structs with named fields",
));
};
Ok(&fields.named)
}
fn ensure_at_most_one(
fields: &[ParsedField],
kind: FieldKind,
attr: &str,
) -> syn::Result<()> {
let mut dups = fields.iter().filter(|f| f.kind == kind).skip(1);
if let Some(dup) = dups.next() {
return Err(syn::Error::new(
dup.ident.span(),
format!("only one `#[kv({attr})]` field is allowed"),
));
}
Ok(())
}
fn generate_field_declarations(fields: &[ParsedField]) -> Vec<TokenStream2> {
fields
.iter()
.map(|f| {
let ident = &f.ident;
let state_ty = f.state_type();
match f.kind {
FieldKind::Collect => {
quote! { let mut #ident: #state_ty = std::collections::HashMap::new(); }
}
_ => quote! { let mut #ident: #state_ty = None; },
}
})
.collect()
}
fn generate_match_arms(
fields: &[&ParsedField],
warnings_ident: Option<&Ident>,
kv: &TokenStream2,
) -> Vec<TokenStream2> {
fields
.iter()
.map(|f| {
let ident = &f.ident;
let key_name = &f.key_name;
if f.lenient {
let inner = &f.inner_type;
match warnings_ident {
Some(warnings) => quote! {
#key_name => {
match <#inner as FromKv>::from_kv(value, value_span) {
Ok(parsed) => #ident = Some(parsed),
Err(_) => {
#ident = None;
#warnings.push(#kv::KvWarning {
variable: key.to_string(),
value: value.to_string(),
span: value_span,
});
}
}
}
},
None => quote! {
#key_name => {
#ident = <#inner as FromKv>::from_kv(value, value_span).ok();
}
},
}
} else {
let merge_expr = f.merge_expr(kv);
quote! {
#key_name => {
#ident = Some(#merge_expr);
}
}
}
})
.collect()
}
fn generate_unknown_handling(
allow_unknown: bool,
collect_field: Option<&ParsedField>,
kv: &TokenStream2,
) -> TokenStream2 {
match collect_field {
Some(field) => {
let ident = &field.ident;
quote! {
_ => {
#ident.insert(key.to_string(), value.to_string());
}
}
}
None if allow_unknown => {
quote! { _ => {} }
}
None => {
quote! {
unknown => {
return Err(#kv::KvError::UnknownVariable {
variable: unknown.to_string(),
span: #kv::Span {
offset: line_offset,
len: unknown.len(),
},
});
}
}
}
}
}
fn generate_serde_impl(name: &Ident, fields: &[ParsedField]) -> TokenStream2 {
let field_defs: Vec<_> = fields
.iter()
.map(|f| {
let ident = &f.ident;
let ty = &f.original_type;
let key_name = &f.key_name;
let serde_attrs = match f.kind {
FieldKind::Required | FieldKind::Vec | FieldKind::MultiLine => {
quote! {
#[serde(rename = #key_name)]
}
}
FieldKind::Optional | FieldKind::OptionVec | FieldKind::OptionMultiLine => {
quote! {
#[serde(rename = #key_name, default, skip_serializing_if = "Option::is_none")]
}
}
FieldKind::Collect => {
quote! {
#[serde(flatten)]
}
}
};
quote! {
#serde_attrs
#ident: #ty
}
})
.collect();
let ser_field_defs: Vec<_> = fields
.iter()
.map(|f| {
let ident = &f.ident;
let key_name = &f.key_name;
match f.kind {
FieldKind::Required | FieldKind::Vec | FieldKind::MultiLine => {
let ty = &f.original_type;
quote! {
#[serde(rename = #key_name)]
#ident: &'a #ty
}
}
FieldKind::Optional
| FieldKind::OptionVec
| FieldKind::OptionMultiLine => {
let inner = extract_type_param(&f.original_type, "Option")
.expect("optional field always has an Option<...> type");
quote! {
#[serde(rename = #key_name, skip_serializing_if = "Option::is_none")]
#ident: Option<&'a #inner>
}
}
FieldKind::Collect => {
let ty = &f.original_type;
quote! {
#[serde(flatten)]
#ident: &'a #ty
}
}
}
})
.collect();
let ser_to_fields: Vec<_> = fields
.iter()
.map(|f| {
let ident = &f.ident;
match f.kind {
FieldKind::Optional
| FieldKind::OptionVec
| FieldKind::OptionMultiLine => {
quote! { #ident: self.#ident.as_ref() }
}
_ => quote! { #ident: &self.#ident },
}
})
.collect();
let ser_lifetime = if fields.is_empty() {
quote! {}
} else {
quote! { <'a> }
};
let from_fields: Vec<_> = fields
.iter()
.map(|f| {
let ident = &f.ident;
quote! { #ident: helper.#ident }
})
.collect();
quote! {
impl serde::Serialize for #name {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#[derive(serde::Serialize)]
struct Helper #ser_lifetime {
#(#ser_field_defs,)*
}
let helper = Helper {
#(#ser_to_fields,)*
};
helper.serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for #name {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct Helper {
#(#field_defs,)*
}
let helper = Helper::deserialize(deserializer)?;
Ok(Self {
#(#from_fields,)*
})
}
}
}
}
#[derive(Default)]
struct ContainerAttrs {
allow_unknown: bool,
crate_path: Option<Path>,
serde: bool,
}
impl ContainerAttrs {
fn parse(attrs: &[Attribute]) -> syn::Result<Self> {
let mut result = Self::default();
for attr in attrs {
if !attr.path().is_ident("kv") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("allow_unknown") {
result.allow_unknown = true;
Ok(())
} else if meta.path.is_ident("crate") {
let lit: syn::LitStr = meta.value()?.parse()?;
result.crate_path = Some(lit.parse()?);
Ok(())
} else if meta.path.is_ident("serde") {
result.serde = true;
Ok(())
} else {
Err(meta.error(
"unknown container attribute; expected `allow_unknown`, `crate`, or `serde`",
))
}
})?;
}
Ok(result)
}
}
#[derive(Default)]
struct FieldAttrs {
variable: Option<String>,
multiline: bool,
collect: bool,
lenient: bool,
}
impl FieldAttrs {
fn parse(attrs: &[Attribute]) -> syn::Result<Self> {
let mut result = Self::default();
for attr in attrs {
if !attr.path().is_ident("kv") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("variable") {
let lit: syn::LitStr = meta.value()?.parse()?;
result.variable = Some(lit.value());
Ok(())
} else if meta.path.is_ident("multiline") {
result.multiline = true;
Ok(())
} else if meta.path.is_ident("collect") {
result.collect = true;
Ok(())
} else if meta.path.is_ident("lenient") {
result.lenient = true;
Ok(())
} else {
Err(meta.error(
"unknown field attribute; expected `variable`, `multiline`, `collect`, or `lenient`",
))
}
})?;
}
Ok(result)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FieldKind {
Required,
Optional,
Vec,
OptionVec,
MultiLine,
OptionMultiLine,
Collect,
}
struct ParsedField {
ident: Ident,
key_name: String,
kind: FieldKind,
inner_type: Type,
original_type: Type,
lenient: bool,
}
impl ParsedField {
fn from_field(field: &Field) -> syn::Result<Self> {
let ident = field.ident.clone().ok_or_else(|| {
syn::Error::new_spanned(field, "expected named field")
})?;
let attrs = FieldAttrs::parse(&field.attrs)?;
if attrs.lenient
&& (extract_type_param(&field.ty, "Option").is_none()
|| extract_option_vec_inner(&field.ty).is_some())
{
return Err(syn::Error::new_spanned(
&field.ty,
"`lenient` attribute requires an `Option<T>` field",
));
}
if attrs.collect {
validate_collect_type(&field.ty, field)?;
return Ok(Self {
ident,
key_name: String::new(),
kind: FieldKind::Collect,
inner_type: field.ty.clone(),
original_type: field.ty.clone(),
lenient: false,
});
}
if attrs.multiline
&& extract_type_param(&field.ty, "Vec").is_none()
&& extract_option_vec_inner(&field.ty).is_none()
{
return Err(syn::Error::new_spanned(
&field.ty,
"`multiline` attribute requires `Vec<T>` or `Option<Vec<T>>` type",
));
}
let key_name = attrs
.variable
.unwrap_or_else(|| ident.to_string().to_uppercase());
let (kind, inner_type) = analyze_type(&field.ty, attrs.multiline);
Ok(Self {
ident,
key_name,
kind,
inner_type,
original_type: field.ty.clone(),
lenient: attrs.lenient,
})
}
fn state_type(&self) -> TokenStream2 {
let inner = &self.inner_type;
match self.kind {
FieldKind::Required | FieldKind::Optional => {
quote! { Option<#inner> }
}
FieldKind::Vec
| FieldKind::OptionVec
| FieldKind::MultiLine
| FieldKind::OptionMultiLine => {
quote! { Option<Vec<#inner>> }
}
FieldKind::Collect => {
quote! { std::collections::HashMap<String, String> }
}
}
}
fn merge_expr(&self, kv: &TokenStream2) -> TokenStream2 {
let inner = &self.inner_type;
let ident = &self.ident;
match self.kind {
FieldKind::Required | FieldKind::Optional => {
quote! {
<#inner as FromKv>::from_kv(value, value_span)?
}
}
FieldKind::Vec | FieldKind::OptionVec => {
quote! {
{
let mut items = Vec::new();
for (word, word_span) in #kv::words_with_spans(value, value_offset) {
items.push(<#inner as FromKv>::from_kv(word, word_span)?);
}
items
}
}
}
FieldKind::MultiLine | FieldKind::OptionMultiLine => {
quote! {
{
let mut vec = #ident.unwrap_or_default();
vec.push(<#inner as FromKv>::from_kv(value, value_span)?);
vec
}
}
}
FieldKind::Collect => {
unreachable!(
"merge_expr is not called for {:?} fields",
self.kind
)
}
}
}
fn extract_expr(&self, kv: &TokenStream2) -> TokenStream2 {
let ident = &self.ident;
let key_name = &self.key_name;
match self.kind {
FieldKind::Required | FieldKind::Vec | FieldKind::MultiLine => {
quote! {
#ident.ok_or_else(|| #kv::KvError::Incomplete(#key_name.to_string()))?
}
}
FieldKind::Optional
| FieldKind::OptionVec
| FieldKind::OptionMultiLine
| FieldKind::Collect => {
quote! { #ident }
}
}
}
}
fn validate_collect_type(ty: &Type, field: &Field) -> syn::Result<()> {
let err = || {
syn::Error::new_spanned(
field,
"`collect` attribute requires `HashMap<String, String>` type",
)
};
let Type::Path(type_path) = ty else {
return Err(err());
};
let Some(segment) = type_path.path.segments.last() else {
return Err(err());
};
if segment.ident != "HashMap" {
return Err(err());
}
let PathArguments::AngleBracketed(args) = &segment.arguments else {
return Err(err());
};
let mut arg_iter = args.args.iter();
let is_valid = matches!(
(arg_iter.next(), arg_iter.next(), arg_iter.next()),
(
Some(GenericArgument::Type(Type::Path(k))),
Some(GenericArgument::Type(Type::Path(v))),
None
) if k.path.is_ident("String") && v.path.is_ident("String")
);
if is_valid { Ok(()) } else { Err(err()) }
}
fn analyze_type(ty: &Type, multiline: bool) -> (FieldKind, Type) {
if let Some(vec_inner) = extract_option_vec_inner(ty) {
let kind = if multiline {
FieldKind::OptionMultiLine
} else {
FieldKind::OptionVec
};
return (kind, vec_inner);
}
if let Some(inner) = extract_type_param(ty, "Option") {
return (FieldKind::Optional, inner);
}
if let Some(inner) = extract_type_param(ty, "Vec") {
let kind = if multiline {
FieldKind::MultiLine
} else {
FieldKind::Vec
};
return (kind, inner);
}
(FieldKind::Required, ty.clone())
}
fn extract_option_vec_inner(ty: &Type) -> Option<Type> {
let option_inner = extract_type_param(ty, "Option")?;
extract_type_param(&option_inner, "Vec")
}
fn extract_type_param(ty: &Type, wrapper: &str) -> Option<Type> {
let Type::Path(type_path) = ty else {
return None;
};
let segment = type_path.path.segments.last()?;
if segment.ident != wrapper {
return None;
}
let PathArguments::AngleBracketed(args) = &segment.arguments else {
return None;
};
let GenericArgument::Type(inner) = args.args.first()? else {
return None;
};
Some(inner.clone())
}