#![deny(missing_docs)]
#![deny(unsafe_code)]
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
parse_macro_input, Attribute, Data, DeriveInput, Field, Fields,
GenericArgument, Ident, PathArguments, Type,
};
#[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 fields = extract_named_fields(input)?;
let parsed_fields: Vec<ParsedField> = fields
.iter()
.map(ParsedField::from_field)
.collect::<syn::Result<_>>()?;
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 field_decls = generate_field_declarations(&parsed_fields);
let match_arms = generate_match_arms(®ular_fields);
let unknown_handling =
generate_unknown_handling(&container_attrs, collect_field);
let field_extracts: Vec<_> = parsed_fields
.iter()
.map(ParsedField::extract_expr)
.collect();
let field_names: Vec<_> = parsed_fields.iter().map(|f| &f.ident).collect();
let serde_impl = generate_serde_impl(name, &parsed_fields);
Ok(quote! {
impl #name {
pub fn parse(input: &str) -> std::result::Result<Self, ::pkgsrc::kv::Error> {
use ::pkgsrc::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(::pkgsrc::kv::Error::ParseLine(::pkgsrc::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 = ::pkgsrc::kv::Span {
offset: value_offset,
len: value.len(),
};
match key {
#(#match_arms)*
#unknown_handling
}
}
Ok(#name {
#(#field_names: #field_extracts,)*
})
}
}
#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 generate_field_declarations(fields: &[ParsedField]) -> Vec<TokenStream2> {
fields
.iter()
.map(|f| {
let ident = &f.ident;
let state_ty = f.state_type();
if f.kind == FieldKind::Collect {
quote! { let mut #ident: #state_ty = std::collections::HashMap::new(); }
} else {
quote! { let mut #ident: #state_ty = None; }
}
})
.collect()
}
fn generate_match_arms(fields: &[&ParsedField]) -> Vec<TokenStream2> {
fields
.iter()
.map(|f| {
let ident = &f.ident;
let key_name = &f.key_name;
let merge_expr = f.merge_expr();
quote! {
#key_name => {
#ident = Some(#merge_expr);
}
}
})
.collect()
}
fn generate_unknown_handling(
container_attrs: &ContainerAttrs,
collect_field: Option<&ParsedField>,
) -> TokenStream2 {
match collect_field {
Some(field) => {
let ident = &field.ident;
quote! {
_ => {
#ident.insert(key.to_string(), value.to_string());
}
}
}
None if container_attrs.allow_unknown => {
quote! { _ => {} }
}
None => {
quote! {
unknown => {
return Err(::pkgsrc::kv::Error::UnknownVariable {
variable: unknown.to_string(),
span: ::pkgsrc::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 field_names: Vec<_> = fields.iter().map(|f| &f.ident).collect();
let to_fields: Vec<_> = fields
.iter()
.map(|f| {
let ident = &f.ident;
quote! { #ident: self.#ident.clone() }
})
.collect();
quote! {
#[cfg(feature = "serde")]
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 {
#(#field_defs,)*
}
let helper = Helper {
#(#to_fields,)*
};
helper.serialize(serializer)
}
}
#[cfg(feature = "serde")]
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 {
#(#field_names: helper.#field_names,)*
})
}
}
}
}
#[derive(Default)]
struct ContainerAttrs {
allow_unknown: 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 {
Err(meta.error(
"unknown container attribute; expected `allow_unknown`",
))
}
})?;
}
Ok(result)
}
}
#[derive(Default)]
struct FieldAttrs {
variable: Option<String>,
multiline: bool,
collect: 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 {
Err(meta.error(
"unknown field attribute; expected `variable`, `multiline`, or `collect`",
))
}
})?;
}
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,
}
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.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(),
});
}
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(),
})
}
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) -> 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();
let mut word_start = 0;
let value_bytes = value.as_bytes();
let mut in_word = false;
for (i, &b) in value_bytes.iter().enumerate() {
let is_ws = b == b' ' || b == b'\t';
if is_ws && in_word {
let word = &value[word_start..i];
let word_offset = value_offset + word_start;
let word_span = ::pkgsrc::kv::Span { offset: word_offset, len: word.len() };
items.push(<#inner as FromKv>::from_kv(word, word_span)?);
in_word = false;
} else if !is_ws && !in_word {
word_start = i;
in_word = true;
}
}
if in_word {
let word = &value[word_start..];
let word_offset = value_offset + word_start;
let word_span = ::pkgsrc::kv::Span { offset: word_offset, len: word.len() };
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 => {
quote! { unreachable!() }
}
}
}
fn extract_expr(&self) -> 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(|| ::pkgsrc::kv::Error::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())
}