use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use crate::{
get_crate_name,
model_types::{wrap_read_with_padding_handling, CollectionLimit, TypedFnArgList},
syn_helpers::TypeExts,
ParselyReadData, ParselyReadFieldData,
};
pub fn generate_parsely_read_impl(data: ParselyReadData) -> TokenStream {
let struct_name = data.ident;
if data.data.is_struct() {
generate_parsely_read_impl_struct(
struct_name,
data.data.take_struct().unwrap(),
data.alignment,
data.required_context,
)
} else {
todo!()
}
}
fn generate_plain_read(ty: &syn::Type, context_values: &[syn::Expr]) -> TokenStream {
quote! {
#ty::read::<T>(buf, (#(#context_values,)*))
}
}
fn generate_collection_read(
limit: CollectionLimit,
ty: &syn::Type,
context_values: &[syn::Expr],
) -> TokenStream {
let plain_read = generate_plain_read(ty, context_values);
match limit {
CollectionLimit::Count(count) => {
quote! {
(|| {
let item_count = #count;
let mut items: Vec<#ty> = Vec::with_capacity(item_count as usize);
for idx in 0..item_count {
let item = #plain_read.with_context(|| format!("Index {idx}"))?;
items.push(item);
}
ParselyResult::Ok(items)
})()
}
}
CollectionLimit::While(pred) => {
quote! {
(|| {
let mut values: Vec<ParselyResult<#ty>> = Vec::new();
let mut idx = 0;
while (#pred) {
values.push(#plain_read.with_context( || format!("Read {idx}")));
idx += 1
}
values.into_iter().collect::<ParselyResult<Vec<#ty>>>()
})()
}
}
}
}
fn wrap_in_optional(when_expr: &syn::Expr, inner: TokenStream) -> TokenStream {
quote! {
if #when_expr {
Some(#inner)
} else {
None
}
}
}
fn generate_field_read(field_data: &ParselyReadFieldData) -> TokenStream {
let field_name = field_data
.ident
.as_ref()
.expect("Only named fields supported");
let field_name_str = field_name.to_string();
let read_type = field_data.buffer_type();
let context_values = field_data.context_values();
let mut output = TokenStream::new();
if let Some(ref assign_expr) = field_data.assign_from {
output.extend(quote! {
ParselyResult::<_>::Ok(#assign_expr)
})
} else if let Some(ref map_expr) = field_data.common.map {
map_expr.to_read_map_tokens(field_name, &mut output);
} else if field_data.ty.is_collection() {
let limit = if let Some(ref count) = field_data.count {
CollectionLimit::Count(count.clone())
} else if let Some(ref while_pred) = field_data.while_pred {
CollectionLimit::While(while_pred.clone())
} else {
panic!("Collection field '{field_name}' must have either 'count' or 'while' attribute");
};
output.extend(generate_collection_read(limit, read_type, &context_values));
} else {
output.extend(generate_plain_read(read_type, &context_values));
}
if let Some(ref assertion) = field_data.common.assertion {
assertion.to_read_assertion_tokens(&field_name_str, &mut output);
}
let error_context = format!("Reading field '{field_name}'");
output.extend(quote! { .with_context(|| #error_context)?});
output = if field_data.ty.is_option() && field_data.common.map.is_none() {
let when_expr = field_data
.when
.as_ref()
.expect("Optional field '{field_name}' must have a 'when' attribute");
wrap_in_optional(when_expr, output)
} else {
output
};
output = if let Some(alignment) = field_data.common.alignment {
wrap_read_with_padding_handling(field_name, alignment, output)
} else {
output
};
quote! {
let #field_name = #output;
}
}
fn generate_parsely_read_impl_struct(
struct_name: syn::Ident,
fields: darling::ast::Fields<ParselyReadFieldData>,
struct_alignment: Option<usize>,
required_context: Option<TypedFnArgList>,
) -> TokenStream {
let crate_name = get_crate_name();
let (context_assignments, context_types) = if let Some(ref required_context) = required_context
{
(required_context.assignments(), required_context.types())
} else {
(Vec::new(), Vec::new())
};
let field_reads = fields
.iter()
.map(generate_field_read)
.collect::<Vec<TokenStream>>();
let field_names = fields
.iter()
.map(|f| f.ident.as_ref().unwrap())
.collect::<Vec<&syn::Ident>>();
let body = if let Some(alignment) = struct_alignment {
quote! {
let __bytes_remaining_start = buf.remaining_bytes();
#(#field_reads)*
while (__bytes_remaining_start - buf.remaining_bytes()) % #alignment != 0 {
buf.get_u8().context("padding")?;
}
}
} else {
quote! {
#(#field_reads)*
}
};
let ctx_var = if context_types.is_empty() {
format_ident!("_ctx")
} else {
format_ident!("ctx")
};
quote! {
impl<B: BitBuf> ::#crate_name::ParselyRead<B> for #struct_name {
type Ctx = (#(#context_types,)*);
fn read<T: ::#crate_name::ByteOrder>(buf: &mut B, #ctx_var: (#(#context_types,)*)) -> ::#crate_name::ParselyResult<Self> {
#(#context_assignments)*
#body
Ok(Self { #(#field_names,)* })
}
}
}
}