diff --git a/Notes.md b/Notes.md index dd53fd2..9e789b3 100644 --- a/Notes.md +++ b/Notes.md @@ -506,3 +506,78 @@ able to provide different/optimized read/write impls for different types (e.g. one for BitBuf and another when we know it's a Bits specifically or something) --> look into a refactoring of the read/write traits to accomplish this + +--> Once I cleaned up the design of BitBuf/BitBufMut/Bits/BitsMut, I found I +didn't end up really needing this. So although it was kinda a neat trick, it +also added a lot of complexity (and is really verbose when you wanted to use a +different buffer type, which was annoying) so I ended up ripping this out. + +### Order of operations for different attributes + +As of this writing, for a ParselyRead impl the following attributes are supported: + +* Assertion (assert on a read value) +* Map (map a read value) +* Alignment (consuming data after a read to reach the given alignment) +* Context (pass values when calling a field's read method) +* Limit (denote how many values a collection field contains) +* Assignment (assign a field to a value instead of reading it) +* When (denote when an optional field should be read) + +Some of these attributes are pretty orthogonal to the others (alignment, +context) but others could potentially "interact" in different ways depending on +some desired order. For example: if 'map' and 'assertion' are present, should +the assertion be performed on the read value before or after the map function +was applied? There is no right answer to that question, of course: a caller +could want it either way. So the question is how to implement support for +that. Probably the order of the attributes should be used to determine the +order things are applied: + +This example should: + +1. Read the value +1. Run the assertion on the 'raw' value +1. Run the mapping function + +```rust +#[derive(ParselyRead)] +struct Foo { + #[parsely_read(assertion = "|v: &u8| v % 2 == 0", map = "|v: u8| v * 2")] + field_one: u8 +} +``` + +This example should: + +1. Read the value +1. Run the mapping function +1. Run the assertion on the mapped value + +```rust +#[derive(ParselyRead)] +struct Foo { + #[parsely_read(map = "|v: u8| v * 2", assertion = "|v: &u8| v % 2 == 0")] + field_one: u8 +} +``` + +Order-sensitive interactions of attributes: +Assertion/Map - assertion run on raw value or mapped value +Map/Limit - map function run on individual items or entire collection? Not sure +both are really needed. + +Given the limited number of interactions, I think for now I'll do an implicit, +hard-coded order-of-operations: + +1. When - In a way this has the highest precedence because it determines if + _any_ of the read code for this field is run at all +1. Assignment +1. Limit - For collection fields this determines how many times the 'core' read + is run. +1. (Context - This is really orthogonal so don't think it has much effect + either way) +1. Assertion - this is the first thing done after a value has been read from + the buffer. It's always done on the 'raw' (un-mapped) value. +1. Map +1. (Alignment - this is 'outside' the scope of a field's specific read, so is + also pretty orthogonal) diff --git a/impl/src/code_gen/gen_read.rs b/impl/src/code_gen/gen_read.rs deleted file mode 100644 index 814bb1a..0000000 --- a/impl/src/code_gen/gen_read.rs +++ /dev/null @@ -1,220 +0,0 @@ -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::(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) => { - // Since this is multiple statements we wrap it in a closure - quote! { - (|| { - let mut values: Vec> = Vec::new(); - let mut idx = 0; - while (#pred) { - values.push(#plain_read.with_context( || format!("Read {idx}"))); - idx += 1 - } - values.into_iter().collect::>>() - })() - } - } - } -} - -fn wrap_in_optional(when_expr: &syn::Expr, inner: TokenStream) -> TokenStream { - quote! { - if #when_expr { - Some(#inner) - } else { - None - } - } -} - -/// Given the data associated with a field, generate the code for properly reading it from a -/// buffer. -/// -/// The attributes set in the [`ParselyReadFieldData`] all shape the logic necessary in order to -/// properly parse this field. Roughly, the processing is as follows: -/// -/// 1. Check if an 'assign_from' attribute is set. If so, we don't read from the buffer at all and -/// instead just assign the field to the result of the given expression. -/// 2. Check if a 'map' attribute is set. If so, we'll read a value as a different type and then -/// pass it t othe map function to arrive at the final type and assign it to the field. -/// 3. Check if the field is a collection. If so, some kind of accompanying 'limit' attribute is -/// required: either a 'count' attribute or a `while_pred` attribute that defines how many -/// elements should be read. -/// 4. If none of the above are the case, do a 'plain' read where we just read the type directly -/// from the buffer. -/// 5. If an 'assertion' attribute is present then generate code to assert on the read value using -/// the given assertion function or closure. -/// 6. After the code to perform the read has been generated, we check if the field is an option -/// type. If so, a 'when' attribute is required. This is an expression that determines when the -/// read should actually be done. -/// 7. Finally, if an 'alignment' attribute is present, code is added to detect and consume any -/// padding after the read. -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(); - // Context values that we need to pass to this field's ParselyRead::read method - 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)?}); - - // TODO: what cases should we allow to bypass a 'when' clause for an Option? - 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, - struct_alignment: Option, - required_context: Option, -) -> TokenStream { - let crate_name = get_crate_name(); - // Extract out the assignment expressions we'll do to assign the values of the context tuple - // to the configured variable names, as well as the types of the context tuple. - 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::>(); - - let field_names = fields - .iter() - .map(|f| f.ident.as_ref().unwrap()) - .collect::>(); - - 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 ::#crate_name::ParselyRead for #struct_name { - type Ctx = (#(#context_types,)*); - fn read(buf: &mut B, #ctx_var: (#(#context_types,)*)) -> ::#crate_name::ParselyResult { - #(#context_assignments)* - - #body - - Ok(Self { #(#field_names,)* }) - } - } - } -} diff --git a/impl/src/code_gen/gen_write.rs b/impl/src/code_gen/gen_write.rs deleted file mode 100644 index eff0cc2..0000000 --- a/impl/src/code_gen/gen_write.rs +++ /dev/null @@ -1,167 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; - -use crate::{ - get_crate_name, - model_types::{wrap_write_with_padding_handling, TypedFnArgList}, - syn_helpers::TypeExts, - ParselyWriteData, ParselyWriteFieldData, -}; - -pub fn generate_parsely_write_impl(data: ParselyWriteData) -> TokenStream { - let struct_name = data.ident; - if data.data.is_struct() { - generate_parsely_write_impl_struct( - struct_name, - data.data.take_struct().unwrap(), - data.required_context, - data.alignment, - data.sync_args, - ) - } else { - todo!() - } -} - -fn generate_parsely_write_impl_struct( - struct_name: syn::Ident, - fields: darling::ast::Fields, - required_context: Option, - struct_alignment: Option, - sync_args: Option, -) -> 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()) - }; - // TODO: clean this up like the read code - let field_writes = fields - .iter() - .map(|f| { - let field_name = f.ident.as_ref().expect("Field has a name"); - let field_name_string = field_name.to_string(); - let write_type = f.buffer_type(); - // Context values that we need to pass to this field's ParselyWrite::write method - let context_values = f.context_values(); - - let mut field_write_output = TokenStream::new(); - - if let Some(ref assertion) = f.common.assertion { - assertion.to_write_assertion_tokens(&field_name_string, &mut field_write_output); - } - - // TODO: these write calls should be qualified. Something like <#write_type as - // ParselyWrite>::write - if let Some(ref map_expr) = f.common.map { - map_expr.to_write_map_tokens(field_name, &mut field_write_output); - } else if f.ty.is_option() { - field_write_output.extend(quote! { - if let Some(ref v) = self.#field_name { - #write_type::write::(v, buf, (#(#context_values,)*)).with_context(|| format!("Writing field '{}'", #field_name_string))?; - } - }); - } else if f.ty.is_collection() { - field_write_output.extend(quote! { - self.#field_name.iter().enumerate().map(|(idx, v)| { - #write_type::write::(v, buf, (#(#context_values,)*)).with_context(|| format!("Index {idx}")) - }).collect::>>().with_context(|| format!("Writing field '{}'", #field_name_string))?; - }); - } else { - field_write_output.extend(quote! { - #write_type::write::(&self.#field_name, buf, (#(#context_values,)*)).with_context(|| format!("Writing field '{}'", #field_name_string))?; - }); - } - - field_write_output = if let Some(alignment) = f.common.alignment { - wrap_write_with_padding_handling(field_name, alignment, field_write_output) - } else { - field_write_output - }; - - field_write_output - }) - .collect::>(); - - // sync_args_types lays out the types inside this struct's sync method args tuple. Unless a - // 'sync_args' attribute was used, by default it doesn't need any. - let (sync_args_variables, sync_args_types) = if let Some(ref sync_args) = sync_args { - (sync_args.names(), sync_args.types()) - } else { - (Vec::new(), Vec::new()) - }; - - let sync_field_calls = fields - .iter() - // 'sync_with' attirbutes mean this field's 'sync' method needs to be called with some data - // Iterate over all fields and either: - // a) execute the expression provided in the sync_expr attribute - // b) call the sync function on that type with any provided sync_with arguments - .map(|f| { - let field_name = f.ident.as_ref().expect("Field has a name"); - let field_name_string = field_name.to_string(); - if let Some(ref sync_expr) = f.sync_expr { - quote! { - self.#field_name = (#sync_expr).into_parsely_result().with_context(|| format!("Syncing field '{}'", #field_name_string))?; - } - } else if f.sync_with.is_empty() && f.ty.is_wrapped() { - // We'll allow this combination to skip a call to sync: for types like Option or - // Vec, synchronization is only going to make sense if a custom function was - // provided. - quote! {} - } else { - let sync_with = f.sync_with_expressions(); - quote! { - self.#field_name.sync((#(#sync_with,)*)).with_context(|| format!("Syncing field '{}'", #field_name_string))?; - } - } - }) - .collect::>(); - - let body = if let Some(alignment) = struct_alignment { - quote! { - - let __bytes_remaining_start = buf.remaining_mut_bytes(); - - #(#field_writes)* - - while (__bytes_remaining_start - buf.remaining_mut_bytes()) % #alignment != 0 { - let _ = buf.put_u8(0).context("padding")?; - } - - } - } else { - quote! { - #(#field_writes)* - } - }; - - quote! { - impl ::#crate_name::ParselyWrite for #struct_name { - type Ctx = (#(#context_types,)*); - fn write( - &self, - buf: &mut B, - ctx: Self::Ctx, - ) -> ParselyResult<()> { - #(#context_assignments)* - - #body - - Ok(()) - } - } - - impl StateSync for #struct_name { - type SyncCtx = (#(#sync_args_types,)*); - fn sync(&mut self, (#(#sync_args_variables,)*): (#(#sync_args_types,)*)) -> ParselyResult<()> { - #(#sync_field_calls)* - - Ok(()) - } - - } - } -} diff --git a/impl/src/code_gen/helpers.rs b/impl/src/code_gen/helpers.rs new file mode 100644 index 0000000..3d02fcb --- /dev/null +++ b/impl/src/code_gen/helpers.rs @@ -0,0 +1,12 @@ +use proc_macro2::TokenStream; +use quote::quote; + +pub(crate) fn wrap_in_optional(condition: &syn::Expr, inner: TokenStream) -> TokenStream { + quote! { + if #condition { + Some(#inner) + } else { + None + } + } +} diff --git a/impl/src/code_gen/mod.rs b/impl/src/code_gen/mod.rs index a0f0993..c2e73f2 100644 --- a/impl/src/code_gen/mod.rs +++ b/impl/src/code_gen/mod.rs @@ -1,2 +1,5 @@ -pub(crate) mod gen_read; -pub(crate) mod gen_write; +// pub(crate) mod gen_write; +pub(crate) mod helpers; +pub(crate) mod parsely_common_field_data; +pub(crate) mod read; +pub(crate) mod write; diff --git a/impl/src/code_gen/parsely_common_field_data.rs b/impl/src/code_gen/parsely_common_field_data.rs new file mode 100644 index 0000000..4586918 --- /dev/null +++ b/impl/src/code_gen/parsely_common_field_data.rs @@ -0,0 +1,49 @@ +use crate::{ + syn_helpers::{MemberExts, TypeExts}, + Assertion, Context, MapExpr, +}; + +/// Items that are needed for both reading and writing a field to/from a buffer. +#[derive(Debug)] +pub(crate) struct ParselyCommonFieldData { + pub(crate) ident: syn::Member, + /// The field's type + pub(crate) ty: syn::Type, + + pub(crate) assertion: Option, + /// Values that need to be passed as context to this fields read or write method + pub(crate) context: Option, + + /// An optional mapping that will be applied to the read value + pub(crate) map: Option, + /// An optional indicator that this field is or needs to be aligned to the given byte alignment + /// via padding. + pub(crate) alignment: Option, +} + +impl ParselyCommonFieldData { + /// Get the 'buffer type' of this field (the type that will be used when reading from or + /// writing to the buffer): for wrapper types (like [`Option`] or [`Vec`]), this will be the + /// inner type. + pub(crate) fn buffer_type(&self) -> &syn::Type { + if self.ty.is_option() || self.ty.is_collection() { + self.ty + .inner_type() + .expect("Option or collection has an inner type") + } else { + &self.ty + } + } + + /// Get the context values that need to be passed to the read or write call for this field + pub(crate) fn context_values(&self) -> Vec { + if let Some(ref field_context) = self.context { + field_context.expressions(&format!( + "Read context for field '{}'", + self.ident.as_friendly_string(), + )) + } else { + vec![] + } + } +} diff --git a/impl/src/code_gen/read/helpers.rs b/impl/src/code_gen/read/helpers.rs new file mode 100644 index 0000000..a4db94e --- /dev/null +++ b/impl/src/code_gen/read/helpers.rs @@ -0,0 +1,68 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use crate::{model_types::CollectionLimit, syn_helpers::MemberExts}; + +pub(crate) fn generate_plain_read(ty: &syn::Type, context_values: &[syn::Expr]) -> TokenStream { + quote! { + #ty::read::(buf, (#(#context_values,)*)) + } +} + +pub(crate) 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) => { + // Since this is multiple statements we wrap it in a closure + quote! { + (|| { + let mut values: Vec> = Vec::new(); + let mut idx = 0; + while (#pred) { + values.push(#plain_read.with_context( || format!("Read {idx}"))); + idx += 1 + } + values.into_iter().collect::>>() + })() + } + } + } +} + +pub(crate) fn wrap_read_with_padding_handling( + element_ident: &syn::Member, + alignment: usize, + inner: TokenStream, +) -> TokenStream { + let bytes_read_before_ident = format_ident!( + "__bytes_read_before_{}_read", + element_ident.as_friendly_string() + ); + quote! { + let #bytes_read_before_ident = buf.remaining_bytes(); + + #inner + + while (#bytes_read_before_ident - buf.remaining_bytes()) % #alignment != 0 { + buf.get_u8().context("consuming padding")?; + } + } +} diff --git a/impl/src/code_gen/read/mod.rs b/impl/src/code_gen/read/mod.rs new file mode 100644 index 0000000..b3f1cb6 --- /dev/null +++ b/impl/src/code_gen/read/mod.rs @@ -0,0 +1,5 @@ +pub mod helpers; +pub mod parsely_read_enum_data; +pub mod parsely_read_field_data; +pub mod parsely_read_struct_data; +pub mod parsely_read_variant_data; diff --git a/impl/src/code_gen/read/parsely_read_enum_data.rs b/impl/src/code_gen/read/parsely_read_enum_data.rs new file mode 100644 index 0000000..f5baf97 --- /dev/null +++ b/impl/src/code_gen/read/parsely_read_enum_data.rs @@ -0,0 +1,109 @@ +use ::anyhow::anyhow; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use crate::{anyhow, get_crate_name, syn_helpers::MemberExts, ParselyReadReceiver, TypedFnArgList}; + +use super::{ + helpers::wrap_read_with_padding_handling, parsely_read_field_data::ParselyReadFieldData, + parsely_read_variant_data::ParselyReadVariantData, +}; + +/// A struct which represents all information needed for generating a `ParselyRead` implementation +/// for a given struct. +#[derive(Debug)] +pub(crate) struct ParselyReadEnumData { + pub(crate) ident: syn::Ident, + pub(crate) required_context: TypedFnArgList, + pub(crate) alignment: Option, + pub(crate) key_type: syn::Type, + pub(crate) variants: Vec, +} + +impl TryFrom for ParselyReadEnumData { + type Error = anyhow::Error; + + fn try_from(value: ParselyReadReceiver) -> Result { + let key_type = value + .key_type + .ok_or(anyhow!("'key_type' attribute is required on enums"))?; + let variants = value + .data + .take_enum() + .ok_or(anyhow!("Not an enum"))? + .into_iter() + .map(|v| { + let data_fields = v + .fields + .into_iter() + .enumerate() + .map(|(field_index, field)| { + let ident = syn::Member::from_ident_or_index( + field.ident.as_ref(), + field_index as u32, + ); + ParselyReadFieldData::from_receiver(ident, field) + }) + .collect::>(); + ParselyReadVariantData { + enum_name: value.ident.clone(), + ident: v.ident, + id: v.id, + discriminant: v.discriminant, + fields: data_fields, + } + }) + .collect::>(); + + Ok(ParselyReadEnumData { + ident: value.ident, + key_type, + required_context: value.required_context, + alignment: value.alignment, + variants, + }) + } +} + +impl ToTokens for ParselyReadEnumData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let crate_name = get_crate_name(); + let enum_name = &self.ident; + let enum_name_string = enum_name.to_string(); + let (context_variables, context_types) = + (self.required_context.names(), self.required_context.types()); + + let match_type = &self.key_type; + + let match_arms = &self.variants; + let body = quote! { + let match_value = <#match_type as ::#crate_name::ParselyRead<_>>::read::(buf, ()).with_context(|| format!("Tag for enum '{}'", #enum_name_string))?; + match match_value { + #(#match_arms)* + _ => ParselyResult::<_>::Err(anyhow!("No arms matched value")), + } + }; + + let body = if let Some(alignment) = self.alignment { + wrap_read_with_padding_handling( + &syn::Member::Named(self.ident.clone()), + alignment, + body, + ) + } else { + body + }; + + // TODO: should the enum id be able to be read from the buffer? we could have it support + // being an expr that returns a result or not, like other things. so it could be + // "buf.get_u8()" + tokens.extend(quote! { + impl ::#crate_name::ParselyRead for #enum_name { + type Ctx = (#(#context_types,)*); + fn read(buf: &mut B, (#(#context_variables,)*): (#(#context_types,)*)) -> ::#crate_name::ParselyResult { + #body + } + } + }); + } +} diff --git a/impl/src/code_gen/read/parsely_read_field_data.rs b/impl/src/code_gen/read/parsely_read_field_data.rs new file mode 100644 index 0000000..3ed95d1 --- /dev/null +++ b/impl/src/code_gen/read/parsely_read_field_data.rs @@ -0,0 +1,147 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use crate::{ + code_gen::{helpers::wrap_in_optional, parsely_common_field_data::ParselyCommonFieldData}, + model_types::CollectionLimit, + syn_helpers::MemberExts, + ParselyReadFieldReceiver, TypeExts, +}; + +use super::helpers::{ + generate_collection_read, generate_plain_read, wrap_read_with_padding_handling, +}; + +/// A struct which represents all information needed for generating logic to read a field from a +/// buffer. +#[derive(Debug)] +pub struct ParselyReadFieldData { + /// Data common between read and write for fields + pub(crate) common: ParselyCommonFieldData, + /// Required when there's a collection field + pub(crate) collection_limit: Option, + /// Instead of reading the value of this field from the buffer, assign it from the given + /// [`syn::Ident`] + pub(crate) assign_from: Option, + /// 'when' is required when there's an optional field + pub(crate) when: Option, +} + +impl ParselyReadFieldData { + pub(crate) fn from_receiver( + field_ident: syn::Member, + receiver: ParselyReadFieldReceiver, + ) -> Self { + let collection_limit = if receiver.ty.is_collection() { + if let Some(count) = receiver.count { + Some(CollectionLimit::Count(count)) + } else if let Some(while_pred) = receiver.while_pred { + Some(CollectionLimit::While(while_pred)) + } else { + panic!("Collection type must have 'count' or 'while' attribute"); + } + } else { + None + }; + let when = if receiver.ty.is_option() { + Some( + receiver + .when + .expect("Option field must have 'when' attribute"), + ) + } else { + None + }; + let common = ParselyCommonFieldData { + ident: field_ident, + ty: receiver.ty, + assertion: receiver.common.assertion, + context: receiver.common.context, + map: receiver.common.map, + alignment: receiver.common.alignment, + }; + Self { + common, + collection_limit, + assign_from: receiver.assign_from, + when, + } + } +} + +impl ToTokens for ParselyReadFieldData { + /// Given the data associated with a field, generate the code for properly reading it from a + /// buffer. + /// + /// The attributes set in the [`ParselyReadFieldData`] all shape the logic necessary in order to + /// properly parse this field. Roughly, the processing is as follows: + /// + /// 1. Check if an 'assign_from' attribute is set. If so, we don't read from the buffer at all and + /// instead just assign the field to the result of the given expression. + /// 2. Check if a 'map' attribute is set. If so, we'll read a value as a different type and then + /// pass it t othe map function to arrive at the final type and assign it to the field. + /// 3. Check if the field is a collection. If so, some kind of accompanying 'limit' attribute is + /// required: either a 'count' attribute or a `while_pred` attribute that defines how many + /// elements should be read. + /// 4. If none of the above are the case, do a 'plain' read where we just read the type directly + /// from the buffer. + /// 5. If an 'assertion' attribute is present then generate code to assert on the read value using + /// the given assertion function or closure. + /// 6. After the code to perform the read has been generated, we check if the field is an option + /// type. If so, a 'when' attribute is required. This is an expression that determines when the + /// read should actually be done. + /// 7. Finally, if an 'alignment' attribute is present, code is added to detect and consume any + /// padding after the read. + fn to_tokens(&self, tokens: &mut TokenStream) { + let mut output = TokenStream::new(); + if let Some(ref assign_expr) = self.assign_from { + output.extend(quote! { + ParselyResult::<_>::Ok(#assign_expr) + }); + } else if let Some(ref map_expr) = self.common.map { + map_expr.to_read_map_tokens(&self.common.ident, &mut output); + } else if self.common.ty.is_collection() { + // We've ensure collection_limit is set in this case elswhere. + let limit = self.collection_limit.as_ref().unwrap(); + let read_type = self.common.buffer_type(); + output.extend(generate_collection_read( + limit, + read_type, + &self.common.context_values(), + )); + } else { + output.extend(generate_plain_read( + self.common.buffer_type(), + &self.common.context_values(), + )); + } + + if let Some(ref assertion) = self.common.assertion { + assertion + .to_read_assertion_tokens(&self.common.ident.as_friendly_string(), &mut output); + } + let error_context = format!("Reading field '{}'", self.common.ident.as_friendly_string()); + output.extend(quote! { + .with_context(|| #error_context)? + }); + + output = if self.common.ty.is_option() && self.common.map.is_none() { + // We've ensured 'when' is set in this case elsehwere + let when_expr = self.when.as_ref().unwrap(); + wrap_in_optional(when_expr, output) + } else { + output + }; + + output = if let Some(alignment) = self.common.alignment { + wrap_read_with_padding_handling(&self.common.ident, alignment, output) + } else { + output + }; + + let field_variable_name = self.common.ident.as_variable_name(); + tokens.extend(quote! { + let #field_variable_name = #output; + }) + } +} diff --git a/impl/src/code_gen/read/parsely_read_struct_data.rs b/impl/src/code_gen/read/parsely_read_struct_data.rs new file mode 100644 index 0000000..1ffb55b --- /dev/null +++ b/impl/src/code_gen/read/parsely_read_struct_data.rs @@ -0,0 +1,104 @@ +use anyhow::anyhow; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use crate::syn_helpers::MemberExts; +use crate::{get_crate_name, ParselyReadReceiver, TypedFnArgList}; + +use super::{ + helpers::wrap_read_with_padding_handling, parsely_read_field_data::ParselyReadFieldData, +}; + +/// A struct which represents all information needed for generating a `ParselyRead` implementation +/// for a given struct. +pub(crate) struct ParselyReadStructData { + pub(crate) ident: syn::Ident, + pub(crate) style: darling::ast::Style, + pub(crate) required_context: TypedFnArgList, + pub(crate) alignment: Option, + pub(crate) fields: Vec, +} + +impl TryFrom for ParselyReadStructData { + type Error = anyhow::Error; + + fn try_from(value: ParselyReadReceiver) -> Result { + let (style, struct_receiver_fields) = value + .data + .take_struct() + .ok_or(anyhow!("Not a struct"))? + .split(); + let data_fields = struct_receiver_fields + .into_iter() + .enumerate() + .map(|(field_index, field)| { + let ident = + syn::Member::from_ident_or_index(field.ident.as_ref(), field_index as u32); + ParselyReadFieldData::from_receiver(ident, field) + }) + .collect::>(); + Ok(ParselyReadStructData { + ident: value.ident, + style, + required_context: value.required_context, + alignment: value.alignment, + fields: data_fields, + }) + } +} + +impl ToTokens for ParselyReadStructData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let crate_name = get_crate_name(); + let struct_name = &self.ident; + // Extract out the assignment expressions we'll do to assign the values of the context tuple + // to the configured variable names, as well as the types of the context tuple. + let (context_variables, context_types) = + (self.required_context.names(), self.required_context.types()); + + let fields = &self.fields; + let field_reads = quote! { + #(#fields)* + }; + + let body = if let Some(alignment) = self.alignment { + wrap_read_with_padding_handling( + &syn::Member::Named(self.ident.clone()), + alignment, + field_reads, + ) + } else { + field_reads + }; + + let field_names = fields + .iter() + .map(|f| f.common.ident.as_variable_name().to_owned()) + .collect::>(); + + // TODO: reduce the duplicated code here + if self.style.is_struct() { + tokens.extend(quote! { + impl ::#crate_name::ParselyRead for #struct_name { + type Ctx = (#(#context_types,)*); + fn read(buf: &mut B, (#(#context_variables,)*): (#(#context_types,)*)) -> ::#crate_name::ParselyResult { + #body + + Ok(Self { #(#field_names,)* }) + } + } + }) + } else { + tokens.extend(quote! { + impl ::#crate_name::ParselyRead for #struct_name { + type Ctx = (#(#context_types,)*); + fn read(buf: &mut B, (#(#context_variables,)*): (#(#context_types,)*)) -> ::#crate_name::ParselyResult { + #body + + Ok(Self(#(#field_names,)* )) + } + } + }) + } + } +} diff --git a/impl/src/code_gen/read/parsely_read_variant_data.rs b/impl/src/code_gen/read/parsely_read_variant_data.rs new file mode 100644 index 0000000..0cd4609 --- /dev/null +++ b/impl/src/code_gen/read/parsely_read_variant_data.rs @@ -0,0 +1,74 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use super::parsely_read_field_data::ParselyReadFieldData; +use crate::syn_helpers::MemberExts; + +#[derive(Debug)] +pub(crate) struct ParselyReadVariantData { + pub(crate) enum_name: syn::Ident, + pub(crate) ident: syn::Ident, + pub(crate) id: syn::Expr, + pub(crate) discriminant: Option, + pub(crate) fields: Vec, +} + +impl ParselyReadVariantData { + /// Returns true if this variant contains named fields, false otherwise + fn named_fields(&self) -> bool { + self.fields + .iter() + .any(|f| matches!(f.common.ident, syn::Member::Named(_))) + } +} + +impl ToTokens for ParselyReadVariantData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let arm_expr = &self.id; + let arm_body = if let Some(ref discriminant) = self.discriminant { + quote! { + #discriminant + } + } else { + let fields = &self.fields; + quote! { + #(#fields)* + + + } + }; + let field_names = self + .fields + .iter() + .map(|f| f.common.ident.as_variable_name().to_owned()) + .collect::>(); + let enum_name = &self.enum_name; + let variant_name = &self.ident; + // TODO: don't think we're handling discriminant correctly here + if self.fields.is_empty() { + tokens.extend(quote! { + #arm_expr => { + #arm_body + + Ok(#enum_name::#variant_name) + } + }) + } else if self.named_fields() { + tokens.extend(quote! { + #arm_expr => { + #arm_body + + Ok(#enum_name::#variant_name { #(#field_names,)* }) + } + }) + } else { + tokens.extend(quote! { + #arm_expr => { + #arm_body + + Ok(#enum_name::#variant_name(#(#field_names,)* )) + } + }) + } + } +} diff --git a/impl/src/code_gen/write/helpers.rs b/impl/src/code_gen/write/helpers.rs new file mode 100644 index 0000000..5d894d3 --- /dev/null +++ b/impl/src/code_gen/write/helpers.rs @@ -0,0 +1,31 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use crate::syn_helpers::MemberExts; + +pub(crate) fn wrap_write_with_padding_handling( + element_ident: &syn::Member, + alignment: usize, + inner: TokenStream, +) -> TokenStream { + let bytes_written_before_ident = format_ident!( + "__bytes_written_before_{}_write", + element_ident.as_friendly_string() + ); + + quote! { + let #bytes_written_before_ident = buf.remaining_mut_bytes(); + + #inner + + while (#bytes_written_before_ident - buf.remaining_mut_bytes()) % #alignment != 0 { + buf.put_u8(0).context("adding padding")?; + } + } +} + +#[derive(Debug)] +pub(crate) enum ParentType { + Struct, + Enum, +} diff --git a/impl/src/code_gen/write/mod.rs b/impl/src/code_gen/write/mod.rs new file mode 100644 index 0000000..47c1101 --- /dev/null +++ b/impl/src/code_gen/write/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod helpers; +pub(crate) mod parsely_write_enum_data; +pub(crate) mod parsely_write_field_data; +pub(crate) mod parsely_write_struct_data; +pub(crate) mod parsely_write_variant_data; diff --git a/impl/src/code_gen/write/parsely_write_enum_data.rs b/impl/src/code_gen/write/parsely_write_enum_data.rs new file mode 100644 index 0000000..32de476 --- /dev/null +++ b/impl/src/code_gen/write/parsely_write_enum_data.rs @@ -0,0 +1,116 @@ +use anyhow::anyhow; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use crate::{ + get_crate_name, model_types::TypedFnArgList, syn_helpers::MemberExts, ParselyWriteReceiver, +}; + +use super::{ + helpers::{wrap_write_with_padding_handling, ParentType}, + parsely_write_field_data::ParselyWriteFieldData, + parsely_write_variant_data::ParselyWriteVariantData, +}; + +pub(crate) struct ParselyWriteEnumData { + pub(crate) ident: syn::Ident, + pub(crate) required_context: TypedFnArgList, + pub(crate) alignment: Option, + pub(crate) sync_args: TypedFnArgList, + pub(crate) variants: Vec, +} + +impl TryFrom for ParselyWriteEnumData { + type Error = anyhow::Error; + + fn try_from(value: ParselyWriteReceiver) -> Result { + let key_type = value + .key_type + .ok_or(anyhow!("'key_type' attribute is required on enums"))?; + let variants = value + .data + .take_enum() + .ok_or(anyhow!("Not an enum"))? + .into_iter() + .map(|v| { + let data_fields = v + .fields + .into_iter() + .enumerate() + .map(|(field_index, field)| { + let ident = syn::Member::from_ident_or_index( + field.ident.as_ref(), + field_index as u32, + ); + ParselyWriteFieldData::from_receiver(ident, ParentType::Enum, field) + }) + .collect::>(); + ParselyWriteVariantData { + enum_name: value.ident.clone(), + ident: v.ident, + discriminant: v.discriminant, + id: v.id, + key_type: key_type.clone(), + fields: data_fields, + } + }) + .collect::>(); + Ok(ParselyWriteEnumData { + ident: value.ident, + required_context: value.required_context, + alignment: value.alignment, + sync_args: value.sync_args, + variants, + }) + } +} + +impl ToTokens for ParselyWriteEnumData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let crate_name = get_crate_name(); + let enum_name = &self.ident; + let (context_variables, context_types) = + (self.required_context.names(), self.required_context.types()); + let match_arms = &self.variants; + + let body = quote! { + match self { + #(#match_arms)* + _ => ParselyResult::<()>::Err(anyhow!("No arms matched self"))?, + } + }; + + let body = if let Some(alignment) = self.alignment { + wrap_write_with_padding_handling( + &syn::Member::Named(self.ident.clone()), + alignment, + body, + ) + } else { + body + }; + + let (sync_args_variables, sync_args_types) = + (self.sync_args.names(), self.sync_args.types()); + + // TODO: need to think about what the sync impl for an enum should look like and finish + // that + tokens.extend(quote! { + impl ::#crate_name::ParselyWrite for #enum_name { + type Ctx = (#(#context_types,)*); + fn write(&self, buf: &mut B, (#(#context_variables,)*): Self::Ctx,) -> ParselyResult<()> { + #body + + Ok(()) + } + } + + impl ::#crate_name::StateSync for #enum_name { + type SyncCtx = (#(#sync_args_types,)*); + fn sync(&mut self, (#(#sync_args_variables,)*): (#(#sync_args_types,)*)) -> ParselyResult<()> { + Ok(()) + } + } + }); + } +} diff --git a/impl/src/code_gen/write/parsely_write_field_data.rs b/impl/src/code_gen/write/parsely_write_field_data.rs new file mode 100644 index 0000000..c7befa4 --- /dev/null +++ b/impl/src/code_gen/write/parsely_write_field_data.rs @@ -0,0 +1,127 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use crate::{ + code_gen::parsely_common_field_data::ParselyCommonFieldData, + model_types::{Context, ExprOrFunc}, + syn_helpers::{MemberExts, TypeExts}, + ParselyWriteFieldReceiver, +}; + +use super::helpers::{wrap_write_with_padding_handling, ParentType}; + +#[derive(Debug)] +pub(crate) struct ParselyWriteFieldData { + pub(crate) common: ParselyCommonFieldData, + /// The way we refer to a field when writing differs between structs and enums, so track what + /// kind this field belongs to + pub(crate) parent_type: ParentType, + /// An expression or function call that will be used to update this field in the generated + /// `StateSync` implementation for its parent type. + pub(crate) sync_expr: Option, + /// An list of expressions that should be passed as context to this field's sync method. The + /// sync method provides an opportunity to synchronize "linked" fields, where one field's value + /// depends on the value of another. + pub(crate) sync_with: Context, +} + +impl ParselyWriteFieldData { + pub(crate) fn from_receiver( + field_ident: syn::Member, + parent_type: ParentType, + receiver: ParselyWriteFieldReceiver, + ) -> Self { + let common = ParselyCommonFieldData { + ident: field_ident, + ty: receiver.ty, + assertion: receiver.common.assertion, + context: receiver.common.context, + map: receiver.common.map, + alignment: receiver.common.alignment, + }; + Self { + common, + parent_type, + sync_expr: receiver.sync_expr, + sync_with: receiver.sync_with, + } + } + + /// Get the context values that need to be passed to the sync call for this field + pub(crate) fn sync_with_expressions(&self) -> Vec { + let field_name = self.common.ident.as_friendly_string(); + self.sync_with + .expressions(&format!("Sync context for field '{field_name}'")) + } + + /// Get this field's `sync` call expression + pub(crate) fn to_sync_call_tokens(&self) -> TokenStream { + let field_ident = &self.common.ident; + let field_name_string = field_ident.as_friendly_string(); + if let Some(ref sync_expr) = self.sync_expr { + quote! { + self.#field_ident = (#sync_expr).into_parsely_result().with_context(|| format!("Syncing field '{}'", #field_name_string))?; + } + } else if self.sync_with.is_empty() && self.common.ty.is_wrapped() { + // We'll allow this combination to skip a call to sync: for types like Option or + // Vec, synchronization is only going to make sense if a custom function was + // provided. + quote! {} + } else { + let sync_with = self.sync_with_expressions(); + quote! { + self.#field_ident.sync((#(#sync_with,)*)).with_context(|| format!("Syncing field '{}'", #field_name_string))?; + } + } + } +} + +impl ToTokens for ParselyWriteFieldData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let field_ident = &self.common.ident; + let field_name_string = field_ident.as_friendly_string(); + let write_type = self.common.buffer_type(); + // Context values that we need to pass to this field's ParselyWrite::write method + let context_values = self.common.context_values(); + + let mut output = TokenStream::new(); + let field_var = if matches!(self.parent_type, ParentType::Struct) { + quote! { self.#field_ident } + } else { + let field_name = field_ident.as_variable_name(); + quote! { #field_name } + }; + + if let Some(ref assertion) = self.common.assertion { + assertion.to_write_assertion_tokens(&self.common.ident, &mut output); + } + + if let Some(ref map_expr) = self.common.map { + map_expr.to_write_map_tokens(&self.common.ident, &mut output); + } else if self.common.ty.is_option() { + output.extend(quote! { + if let Some(ref v) = #field_var { + #write_type::write::(v, buf, (#(#context_values,)*)).with_context(|| format!("Writing field '{}'", #field_name_string))?; + } + }); + } else if self.common.ty.is_collection() { + output.extend(quote! { + #field_var.iter().enumerate().map(|(idx, v)| { + #write_type::write::(v, buf, (#(#context_values,)*)).with_context(|| format!("Index {idx}")) + }).collect::>>().with_context(|| format!("Writing field '{}'", #field_name_string))?; + }); + } else { + output.extend(quote! { + #write_type::write::(&#field_var, buf, (#(#context_values,)*)).with_context(|| format!("Writing field '{}'", #field_name_string))?; + }); + } + + output = if let Some(alignment) = self.common.alignment { + wrap_write_with_padding_handling(field_ident, alignment, output) + } else { + output + }; + + tokens.extend(output); + } +} diff --git a/impl/src/code_gen/write/parsely_write_struct_data.rs b/impl/src/code_gen/write/parsely_write_struct_data.rs new file mode 100644 index 0000000..b7dde74 --- /dev/null +++ b/impl/src/code_gen/write/parsely_write_struct_data.rs @@ -0,0 +1,103 @@ +use anyhow::anyhow; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use crate::{ + get_crate_name, model_types::TypedFnArgList, syn_helpers::MemberExts, ParselyWriteReceiver, +}; + +use super::{ + helpers::{wrap_write_with_padding_handling, ParentType}, + parsely_write_field_data::ParselyWriteFieldData, +}; + +pub(crate) struct ParselyWriteStructData { + pub(crate) ident: syn::Ident, + pub(crate) required_context: TypedFnArgList, + pub(crate) alignment: Option, + pub(crate) sync_args: TypedFnArgList, + pub(crate) fields: Vec, +} + +impl TryFrom for ParselyWriteStructData { + type Error = anyhow::Error; + + fn try_from(value: ParselyWriteReceiver) -> Result { + let struct_receiver_fields = value.data.take_struct().ok_or(anyhow!("Not a struct"))?; + let data_fields = struct_receiver_fields + .into_iter() + .enumerate() + .map(|(field_index, field)| { + let ident = + syn::Member::from_ident_or_index(field.ident.as_ref(), field_index as u32); + ParselyWriteFieldData::from_receiver(ident, ParentType::Struct, field) + }) + .collect::>(); + + Ok(ParselyWriteStructData { + ident: value.ident, + required_context: value.required_context, + alignment: value.alignment, + sync_args: value.sync_args, + fields: data_fields, + }) + } +} + +impl ToTokens for ParselyWriteStructData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let crate_name = get_crate_name(); + let struct_name = &self.ident; + let (context_variables, context_types) = + (self.required_context.names(), self.required_context.types()); + + let fields = &self.fields; + let field_writes = quote! { + #(#fields)* + }; + + let sync_field_calls = fields + .iter() + .map(|f| f.to_sync_call_tokens()) + .collect::>(); + + let (sync_args_variables, sync_args_types) = + (self.sync_args.names(), self.sync_args.types()); + + let body = if let Some(alignment) = self.alignment { + wrap_write_with_padding_handling( + &syn::Member::Named(self.ident.clone()), + alignment, + field_writes, + ) + } else { + field_writes + }; + + tokens.extend(quote! { + impl ::#crate_name::ParselyWrite for #struct_name { + type Ctx = (#(#context_types,)*); + fn write( + &self, + buf: &mut B, + (#(#context_variables,)*): Self::Ctx, + ) -> ParselyResult<()> { + + #body + + Ok(()) + } + } + + impl ::#crate_name::StateSync for #struct_name { + type SyncCtx = (#(#sync_args_types,)*); + fn sync(&mut self, (#(#sync_args_variables,)*): (#(#sync_args_types,)*)) -> ParselyResult<()> { + #(#sync_field_calls)* + + Ok(()) + } + + } + }); + } +} diff --git a/impl/src/code_gen/write/parsely_write_variant_data.rs b/impl/src/code_gen/write/parsely_write_variant_data.rs new file mode 100644 index 0000000..c608e66 --- /dev/null +++ b/impl/src/code_gen/write/parsely_write_variant_data.rs @@ -0,0 +1,78 @@ +use crate::{get_crate_name, syn_helpers::MemberExts}; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use super::parsely_write_field_data::ParselyWriteFieldData; + +pub(crate) struct ParselyWriteVariantData { + pub(crate) enum_name: syn::Ident, + pub(crate) ident: syn::Ident, + pub(crate) id: syn::Expr, + pub(crate) discriminant: Option, + pub(crate) key_type: syn::Type, + pub(crate) fields: Vec, +} + +impl ParselyWriteVariantData { + /// Returns true if this variant contains named fields, false otherwise + fn named_fields(&self) -> bool { + self.fields + .iter() + .any(|f| matches!(f.common.ident, syn::Member::Named(_))) + } +} + +impl ToTokens for ParselyWriteVariantData { + fn to_tokens(&self, tokens: &mut TokenStream) { + let crate_name = get_crate_name(); + let enum_name = &self.enum_name; + let variant_name = &self.ident; + + let tag_expr = &self.id; + let tag_type = &self.key_type; + let tag_write = quote! { + let tag_value: #tag_type = #tag_expr; + ::#crate_name::ParselyWrite::write::(&tag_value, buf, ())?; + }; + + let body = if let Some(ref discriminant) = self.discriminant { + quote! { + #enum_name::#variant_name => { + #tag_write + let discriminant_value = #discriminant; + + discriminant_value.write::(buf, ()).context("Writing discriminant value of variant #variant_name") + } + } + } else if !self.fields.is_empty() { + let fields = &self.fields; + let field_variable_names = fields + .iter() + .map(|f| f.common.ident.as_variable_name()) + .collect::>(); + if self.named_fields() { + quote! { + #enum_name::#variant_name { #(ref #field_variable_names,)* } => { + #tag_write + #(#fields)* + } + } + } else { + quote! { + #enum_name::#variant_name(#(ref #field_variable_names,)*) => { + #tag_write + #(#fields)* + } + } + } + } else { + quote! { + #enum_name::#variant_name => { + #tag_write + } + } + }; + + tokens.extend(body); + } +} diff --git a/impl/src/lib.rs b/impl/src/lib.rs index 79f9188..1a725d0 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -3,7 +3,7 @@ pub mod error; mod model_types; pub mod parsely_read; pub mod parsely_write; -mod syn_helpers; +pub(crate) mod syn_helpers; pub use bits_io::{ buf::bit_buf::BitBuf, @@ -25,31 +25,63 @@ pub mod anyhow { pub use anyhow::*; } -use code_gen::{gen_read::generate_parsely_read_impl, gen_write::generate_parsely_write_impl}; -use darling::{ast, FromDeriveInput, FromField, FromMeta}; +use code_gen::{ + read::{ + parsely_read_enum_data::ParselyReadEnumData, + parsely_read_struct_data::ParselyReadStructData, + }, + write::{ + parsely_write_enum_data::ParselyWriteEnumData, + parsely_write_struct_data::ParselyWriteStructData, + }, +}; +use darling::{ast, FromDeriveInput, FromField, FromMeta, FromVariant}; use model_types::{Assertion, Context, ExprOrFunc, MapExpr, TypedFnArgList}; use proc_macro2::TokenStream; +use quote::quote; use syn::DeriveInput; use syn_helpers::TypeExts; #[doc(hidden)] pub fn derive_parsely_read(item: TokenStream) -> std::result::Result { let ast: DeriveInput = syn::parse2(item)?; - let data = ParselyReadData::from_derive_input(&ast)?; - - Ok(generate_parsely_read_impl(data)) + let data = ParselyReadReceiver::from_derive_input(&ast)?; + + // println!("{data:#?}"); + + if data.data.is_struct() { + let struct_data = ParselyReadStructData::try_from(data).unwrap(); + Ok(quote! { + #struct_data + }) + } else { + let enum_data = ParselyReadEnumData::try_from(data).unwrap(); + Ok(quote! { + #enum_data + }) + } } #[doc(hidden)] pub fn derive_parsely_write(item: TokenStream) -> std::result::Result { let ast: DeriveInput = syn::parse2(item)?; - let data = ParselyWriteData::from_derive_input(&ast)?; - - Ok(generate_parsely_write_impl(data)) + let data = ParselyWriteReceiver::from_derive_input(&ast)?; + + if data.data.is_struct() { + let struct_data = ParselyWriteStructData::try_from(data).unwrap(); + Ok(quote! { + #struct_data + }) + } else { + let enum_data = ParselyWriteEnumData::try_from(data).unwrap(); + Ok(quote! { + #enum_data + }) + } } #[derive(Debug, FromField, FromMeta)] -pub struct ParselyCommonFieldData { +pub struct ParselyCommonFieldReceiver { // Note: 'magic' fields (ident, ty, etc.) don't work with 'flatten' so can't be held here. // See https://github.com/TedDriggs/darling/issues/330 @@ -69,13 +101,13 @@ pub struct ParselyCommonFieldData { #[derive(Debug, FromField)] #[darling(attributes(parsely, parsely_read))] -pub struct ParselyReadFieldData { +pub struct ParselyReadFieldReceiver { ident: Option, ty: syn::Type, #[darling(flatten)] - common: ParselyCommonFieldData, + common: ParselyCommonFieldReceiver, /// 'count' is required when the field is a collection count: Option, /// 'while' is an alternate option to 'count' to use with a collection field @@ -91,44 +123,24 @@ pub struct ParselyReadFieldData { when: Option, } -impl ParselyReadFieldData { - /// Get the 'buffer type' of this field (the type that will be used when reading from or - /// writing to the buffer): for wrapper types (like [`Option`] or [`Vec`]), this will be the - /// inner type. - pub(crate) fn buffer_type(&self) -> &syn::Type { - if self.ty.is_option() || self.ty.is_collection() { - self.ty - .inner_type() - .expect("Option or collection has an inner type") - } else { - &self.ty - } - } - - /// Get the context values that need to be passed to the read or write call for this field - pub(crate) fn context_values(&self) -> Vec { - let field_name = self - .ident - .as_ref() - .expect("Field must have a name") - .to_string(); - if let Some(ref field_context) = self.common.context { - field_context.expressions(&format!("Read context for field '{field_name}'")) - } else { - vec![] - } - } +#[derive(Debug, FromVariant)] +#[darling(attributes(parsely, parsely_read))] +pub struct ParselyReadVariantReceiver { + ident: syn::Ident, + discriminant: Option, + id: syn::Expr, + fields: ast::Fields, } #[derive(Debug, FromField)] #[darling(attributes(parsely, parsely_write))] -pub struct ParselyWriteFieldData { +pub struct ParselyWriteFieldReceiver { ident: Option, ty: syn::Type, #[darling(flatten)] - common: ParselyCommonFieldData, + common: ParselyCommonFieldReceiver, /// An expression or function call that will be used to update this field in the generated /// `StateSync` implementation for its parent type. @@ -141,63 +153,39 @@ pub struct ParselyWriteFieldData { sync_with: Context, } -impl ParselyWriteFieldData { - /// Get the 'buffer type' of this field (the type that will be used when reading from or - /// writing to the buffer): for wrapper types (like [`Option`] or [`Vec`]), this will be the - /// inner type. - pub(crate) fn buffer_type(&self) -> &syn::Type { - if self.ty.is_option() || self.ty.is_collection() { - self.ty - .inner_type() - .expect("Option or collection has an inner type") - } else { - &self.ty - } - } - - /// Get the context values that need to be passed to the read or write call for this field - pub(crate) fn context_values(&self) -> Vec { - let field_name = self - .ident - .as_ref() - .expect("Field must have a name") - .to_string(); - if let Some(ref field_context) = self.common.context { - field_context.expressions(&format!("Write context for field '{field_name}'")) - } else { - vec![] - } - } - - /// Get the context values that need to be passed to the read or write call for this field - pub(crate) fn sync_with_expressions(&self) -> Vec { - let field_name = self - .ident - .as_ref() - .expect("Field must have a name") - .to_string(); - self.sync_with - .expressions(&format!("Sync context for field '{field_name}'")) - } +#[derive(Debug, FromVariant)] +#[darling(attributes(parsely, parsely_write))] +pub struct ParselyWriteVariantReceiver { + ident: syn::Ident, + discriminant: Option, + id: syn::Expr, + fields: ast::Fields, } #[derive(Debug, FromDeriveInput)] #[darling(attributes(parsely, parsely_read), supports(struct_any, enum_any))] -pub struct ParselyReadData { +pub struct ParselyReadReceiver { ident: syn::Ident, - required_context: Option, + #[darling(default)] + required_context: TypedFnArgList, alignment: Option, - data: ast::Data<(), ParselyReadFieldData>, + // Enums require a type to denote the tag type that determines which variant will be read + key_type: Option, + data: ast::Data, } #[derive(Debug, FromDeriveInput)] #[darling(attributes(parsely, parsely_write), supports(struct_any, enum_any))] -pub struct ParselyWriteData { +pub struct ParselyWriteReceiver { ident: syn::Ident, - required_context: Option, - sync_args: Option, + #[darling(default)] + required_context: TypedFnArgList, + #[darling(default)] + sync_args: TypedFnArgList, alignment: Option, - data: ast::Data<(), ParselyWriteFieldData>, + // Enums require a type to denote the tag type to be written to denote the variant + key_type: Option, + data: ast::Data, } pub(crate) fn get_crate_name() -> syn::Ident { diff --git a/impl/src/model_types.rs b/impl/src/model_types.rs index 952b6d8..a1c2810 100644 --- a/impl/src/model_types.rs +++ b/impl/src/model_types.rs @@ -3,14 +3,15 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; use syn::parse::Parse; -use crate::get_crate_name; +use crate::{get_crate_name, syn_helpers::MemberExts}; +#[derive(Debug)] pub(crate) enum CollectionLimit { Count(syn::Expr), While(syn::Expr), } -#[derive(Debug)] +#[derive(Debug, Default)] pub(crate) struct TypedFnArgList(pub(crate) Vec); impl TypedFnArgList { @@ -22,21 +23,6 @@ impl TypedFnArgList { pub(crate) fn names(&self) -> Vec<&syn::Ident> { self.0.iter().map(|t| t.name()).collect() } - - // TODO: this is context-specific, but now this type is more generic. move it? - pub(crate) fn assignments(&self) -> Vec { - self.0 - .iter() - .enumerate() - .map(|(idx, fn_arg)| { - let idx: syn::Index = idx.into(); - syn::parse2::(quote! { - let #fn_arg = ctx.#idx; - }) - .unwrap() - }) - .collect::>() - } } impl FromMeta for TypedFnArgList { @@ -253,9 +239,9 @@ impl FromMeta for MapExpr { } impl MapExpr { - pub(crate) fn to_read_map_tokens(&self, field_name: &syn::Ident, tokens: &mut TokenStream) { + pub(crate) fn to_read_map_tokens(&self, field_name: &syn::Member, tokens: &mut TokenStream) { let crate_name = get_crate_name(); - let field_name_string = field_name.to_string(); + let field_name_string = field_name.as_friendly_string(); let map_expr = &self.0; // TODO: is there a case where context might be required for reading the 'buffer_type' // value? @@ -269,13 +255,13 @@ impl MapExpr { }) } - pub(crate) fn to_write_map_tokens(&self, field_name: &syn::Ident, tokens: &mut TokenStream) { + pub(crate) fn to_write_map_tokens(&self, field_ident: &syn::Member, tokens: &mut TokenStream) { let crate_name = get_crate_name(); - let field_name_string = field_name.to_string(); + let field_name_string = field_ident.as_friendly_string(); let map_expr = &self.0; tokens.extend(quote! { { - let mapped_value = (#map_expr)(&self.#field_name); + let mapped_value = (#map_expr)(&self.#field_ident); // Coerce the result of the mapping function into a ParselyResult where we know // T is writable to the buffer. We need to use this syntax because otherwise the // compiler gets caught up on trying to infer the buffer type. @@ -319,62 +305,21 @@ impl Assertion { }); } - pub(crate) fn to_write_assertion_tokens(&self, field_name: &str, tokens: &mut TokenStream) { + pub(crate) fn to_write_assertion_tokens( + &self, + field_ident: &syn::Member, + tokens: &mut TokenStream, + ) { let assertion = &self.0; let assertion_string = quote! { #assertion }.to_string(); - let assertion_func_ident = format_ident!("__{}_assertion_func", field_name); - let field_name_ident = format_ident!("{field_name}"); + let assertion_func_ident = + format_ident!("__{}_assertion_func", field_ident.as_variable_name()); + let field_name_str = field_ident.as_friendly_string(); tokens.extend(quote! { let #assertion_func_ident = #assertion; - if !#assertion_func_ident(&self.#field_name_ident) { - bail!("Assertion failed: value of field '{}' ('{:?}') didn't pass assertion: '{}'", #field_name, self.#field_name_ident, #assertion_string) + if !#assertion_func_ident(&self.#field_ident) { + bail!("Assertion failed: value of field '{}' ('{:?}') didn't pass assertion: '{}'", #field_name_str, self.#field_ident, #assertion_string) } }) } } - -pub(crate) fn wrap_read_with_padding_handling( - element_ident: &syn::Ident, - alignment: usize, - inner: TokenStream, -) -> TokenStream { - let bytes_read_before_ident = format_ident!("__bytes_read_before_{element_ident}_read"); - let bytes_read_after_ident = format_ident!("__bytes_read_after_{element_ident}_read"); - let amount_read_ident = format_ident!("__bytes_read_for_{element_ident}"); - - quote! { - let #bytes_read_before_ident = buf.remaining_bytes(); - - #inner - - let #bytes_read_after_ident = buf.remaining_bytes(); - let mut #amount_read_ident = #bytes_read_before_ident - #bytes_read_after_ident; - while #amount_read_ident % #alignment != 0 { - let _ = buf.get_u8().context("padding")?; - #amount_read_ident += 1; - } - } -} - -pub(crate) fn wrap_write_with_padding_handling( - element_ident: &syn::Ident, - alignment: usize, - inner: TokenStream, -) -> TokenStream { - let bytes_written_before_ident = format_ident!("__bytes_written_before_{element_ident}_write"); - let bytes_written_after_ident = format_ident!("__bytes_written_after_{element_ident}_write"); - let amount_written_ident = format_ident!("__bytes_written_for_{element_ident}"); - - quote! { - let #bytes_written_before_ident = buf.remaining_bytes(); - - #inner - - let #bytes_written_after_ident = buf.remaining_bytes(); - let mut #amount_written_ident = #bytes_written_after_ident - #bytes_written_before_ident; - while #amount_written_ident % #alignment != 0 { - buf.put_u8(0).context("padding")?; - #amount_written_ident += 1; - } - } -} diff --git a/impl/src/syn_helpers.rs b/impl/src/syn_helpers.rs index b403ad7..0181b67 100644 --- a/impl/src/syn_helpers.rs +++ b/impl/src/syn_helpers.rs @@ -1,3 +1,5 @@ +use quote::format_ident; + pub(crate) trait TypeExts { fn is_option(&self) -> bool; fn is_collection(&self) -> bool; @@ -59,3 +61,40 @@ impl TypeExts for syn::Type { Some(inner_type) } } + +pub(crate) trait MemberExts { + fn from_ident_or_index(ident: Option<&syn::Ident>, index: u32) -> Self; + /// Return the value of this `syn::Member` as a user-friendly String. This version is intended + /// to be used for things like error messages. + fn as_friendly_string(&self) -> String; + /// Return the value of this `syn::Member` in the form of a `syn::Ident` that can be used as a + /// local variable. + fn as_variable_name(&self) -> syn::Ident; +} + +impl MemberExts for syn::Member { + fn from_ident_or_index(ident: Option<&syn::Ident>, index: u32) -> Self { + if let Some(ident) = ident { + syn::Member::Named(ident.clone()) + } else { + syn::Member::Unnamed(syn::Index { + index, + span: proc_macro2::Span::call_site(), + }) + } + } + + fn as_friendly_string(&self) -> String { + match self { + syn::Member::Named(ref ident) => ident.to_string(), + syn::Member::Unnamed(ref index) => format!("Field {}", index.index), + } + } + + fn as_variable_name(&self) -> syn::Ident { + match self { + syn::Member::Named(ref ident) => ident.clone(), + syn::Member::Unnamed(ref index) => format_ident!("field_{}", index.index), + } + } +} diff --git a/tests/expand/alignment.expanded.rs b/tests/expand/alignment.expanded.rs index 948c66b..c4a6d42 100644 --- a/tests/expand/alignment.expanded.rs +++ b/tests/expand/alignment.expanded.rs @@ -7,20 +7,20 @@ impl ::parsely_rs::ParselyRead for Foo { type Ctx = (); fn read( buf: &mut B, - _ctx: (), + (): (), ) -> ::parsely_rs::ParselyResult { - let __bytes_remaining_start = buf.remaining_bytes(); + let __bytes_read_before_Foo_read = buf.remaining_bytes(); let one = u8::read::(buf, ()).with_context(|| "Reading field 'one'")?; - while (__bytes_remaining_start - buf.remaining_bytes()) % 4usize != 0 { - buf.get_u8().context("padding")?; + while (__bytes_read_before_Foo_read - buf.remaining_bytes()) % 4usize != 0 { + buf.get_u8().context("consuming padding")?; } Ok(Self { one }) } } impl ::parsely_rs::ParselyWrite for Foo { type Ctx = (); - fn write(&self, buf: &mut B, ctx: Self::Ctx) -> ParselyResult<()> { - let __bytes_remaining_start = buf.remaining_mut_bytes(); + fn write(&self, buf: &mut B, (): Self::Ctx) -> ParselyResult<()> { + let __bytes_written_before_Foo_write = buf.remaining_mut_bytes(); u8::write::(&self.one, buf, ()) .with_context(|| ::alloc::__export::must_use({ let res = ::alloc::fmt::format( @@ -28,13 +28,15 @@ impl ::parsely_rs::ParselyWrite for Foo { ); res }))?; - while (__bytes_remaining_start - buf.remaining_mut_bytes()) % 4usize != 0 { - let _ = buf.put_u8(0).context("padding")?; + while (__bytes_written_before_Foo_write - buf.remaining_mut_bytes()) % 4usize + != 0 + { + buf.put_u8(0).context("adding padding")?; } Ok(()) } } -impl StateSync for Foo { +impl ::parsely_rs::StateSync for Foo { type SyncCtx = (); fn sync(&mut self, (): ()) -> ParselyResult<()> { self.one diff --git a/tests/expand/assertion.expanded.rs b/tests/expand/assertion.expanded.rs index de4eb2c..84a88cc 100644 --- a/tests/expand/assertion.expanded.rs +++ b/tests/expand/assertion.expanded.rs @@ -7,7 +7,7 @@ impl ::parsely_rs::ParselyRead for Foo { type Ctx = (); fn read( buf: &mut B, - _ctx: (), + (): (), ) -> ::parsely_rs::ParselyResult { let value = u8::read::(buf, ()) .and_then(|read_value| { @@ -35,7 +35,7 @@ impl ::parsely_rs::ParselyRead for Foo { } impl ::parsely_rs::ParselyWrite for Foo { type Ctx = (); - fn write(&self, buf: &mut B, ctx: Self::Ctx) -> ParselyResult<()> { + fn write(&self, buf: &mut B, (): Self::Ctx) -> ParselyResult<()> { let __value_assertion_func = |v: &u8| *v % 2 == 0; if !__value_assertion_func(&self.value) { return ::anyhow::__private::Err( @@ -62,7 +62,7 @@ impl ::parsely_rs::ParselyWrite for Foo { Ok(()) } } -impl StateSync for Foo { +impl ::parsely_rs::StateSync for Foo { type SyncCtx = (); fn sync(&mut self, (): ()) -> ParselyResult<()> { self.value diff --git a/tests/expand/enum_basic.expanded.rs b/tests/expand/enum_basic.expanded.rs new file mode 100644 index 0000000..8b44df3 --- /dev/null +++ b/tests/expand/enum_basic.expanded.rs @@ -0,0 +1,132 @@ +use parsely_rs::*; +#[parsely(key_type = "u8")] +enum Foo { + #[parsely(id = 1)] + One, + #[parsely(id = 2)] + Two(u8), + #[parsely(id = 3)] + Three { bar: u8, baz: u16 }, +} +#[automatically_derived] +impl ::core::fmt::Debug for Foo { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + Foo::One => ::core::fmt::Formatter::write_str(f, "One"), + Foo::Two(__self_0) => { + ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Two", &__self_0) + } + Foo::Three { bar: __self_0, baz: __self_1 } => { + ::core::fmt::Formatter::debug_struct_field2_finish( + f, + "Three", + "bar", + __self_0, + "baz", + &__self_1, + ) + } + } + } +} +impl ::parsely_rs::ParselyRead for Foo { + type Ctx = (); + fn read( + buf: &mut B, + (): (), + ) -> ::parsely_rs::ParselyResult { + let match_value = >::read::(buf, ()) + .with_context(|| ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("Tag for enum \'{0}\'", "Foo"), + ); + res + }))?; + match match_value { + 1 => Ok(Foo::One), + 2 => { + let field_0 = u8::read::(buf, ()) + .with_context(|| "Reading field 'Field 0'")?; + Ok(Foo::Two(field_0)) + } + 3 => { + let bar = u8::read::(buf, ()).with_context(|| "Reading field 'bar'")?; + let baz = u16::read::(buf, ()) + .with_context(|| "Reading field 'baz'")?; + Ok(Foo::Three { bar, baz }) + } + _ => { + ParselyResult::< + _, + >::Err( + ::anyhow::__private::must_use({ + let error = ::anyhow::__private::format_err( + format_args!("No arms matched value"), + ); + error + }), + ) + } + } + } +} +impl ::parsely_rs::ParselyWrite for Foo { + type Ctx = (); + fn write(&self, buf: &mut B, (): Self::Ctx) -> ParselyResult<()> { + match self { + Foo::One => { + let tag_value: u8 = 1; + ::parsely_rs::ParselyWrite::write::(&tag_value, buf, ())?; + } + Foo::Two(ref field_0) => { + let tag_value: u8 = 2; + ::parsely_rs::ParselyWrite::write::(&tag_value, buf, ())?; + u8::write::(&field_0, buf, ()) + .with_context(|| ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("Writing field \'{0}\'", "Field 0"), + ); + res + }))?; + } + Foo::Three { ref bar, ref baz } => { + let tag_value: u8 = 3; + ::parsely_rs::ParselyWrite::write::(&tag_value, buf, ())?; + u8::write::(&bar, buf, ()) + .with_context(|| ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("Writing field \'{0}\'", "bar"), + ); + res + }))?; + u16::write::(&baz, buf, ()) + .with_context(|| ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("Writing field \'{0}\'", "baz"), + ); + res + }))?; + } + _ => { + ParselyResult::< + (), + >::Err( + ::anyhow::__private::must_use({ + let error = ::anyhow::__private::format_err( + format_args!("No arms matched self"), + ); + error + }), + )? + } + } + Ok(()) + } +} +impl ::parsely_rs::StateSync for Foo { + type SyncCtx = (); + fn sync(&mut self, (): ()) -> ParselyResult<()> { + Ok(()) + } +} diff --git a/tests/expand/enum_basic.rs b/tests/expand/enum_basic.rs new file mode 100644 index 0000000..23050f1 --- /dev/null +++ b/tests/expand/enum_basic.rs @@ -0,0 +1,12 @@ +use parsely_rs::*; + +#[derive(Debug, ParselyRead, ParselyWrite)] +#[parsely(key_type = "u8")] +enum Foo { + #[parsely(id = 1)] + One, + #[parsely(id = 2)] + Two(u8), + #[parsely(id = 3)] + Three { bar: u8, baz: u16 }, +} diff --git a/tests/expand/map.expanded.rs b/tests/expand/map.expanded.rs index d672265..a22db23 100644 --- a/tests/expand/map.expanded.rs +++ b/tests/expand/map.expanded.rs @@ -8,7 +8,7 @@ impl ::parsely_rs::ParselyRead for Foo { type Ctx = (); fn read( buf: &mut B, - _ctx: (), + (): (), ) -> ::parsely_rs::ParselyResult { let value = { let original_value = ::parsely_rs::ParselyRead::read::(buf, ()) @@ -33,7 +33,7 @@ impl ::parsely_rs::ParselyRead for Foo { } impl ::parsely_rs::ParselyWrite for Foo { type Ctx = (); - fn write(&self, buf: &mut B, ctx: Self::Ctx) -> ParselyResult<()> { + fn write(&self, buf: &mut B, (): Self::Ctx) -> ParselyResult<()> { { let mapped_value = (|v: &str| { v.parse::() })(&self.value); let result = <_ as IntoWritableParselyResult< @@ -57,7 +57,7 @@ impl ::parsely_rs::ParselyWrite for Foo { Ok(()) } } -impl StateSync for Foo { +impl ::parsely_rs::StateSync for Foo { type SyncCtx = (); fn sync(&mut self, (): ()) -> ParselyResult<()> { self.value diff --git a/tests/expand/read_context.expanded.rs b/tests/expand/read_context.expanded.rs new file mode 100644 index 0000000..da968ca --- /dev/null +++ b/tests/expand/read_context.expanded.rs @@ -0,0 +1,17 @@ +use parsely_rs::*; +#[parsely_read(required_context("some_context_value: u8"))] +struct ReadContext { + #[parsely_read(assign_from = "some_context_value")] + one: u8, +} +impl ::parsely_rs::ParselyRead for ReadContext { + type Ctx = (u8,); + fn read( + buf: &mut B, + (some_context_value,): (u8,), + ) -> ::parsely_rs::ParselyResult { + let one = ParselyResult::<_>::Ok(some_context_value) + .with_context(|| "Reading field 'one'")?; + Ok(Self { one }) + } +} diff --git a/tests/expand/read_context.rs b/tests/expand/read_context.rs new file mode 100644 index 0000000..1216793 --- /dev/null +++ b/tests/expand/read_context.rs @@ -0,0 +1,8 @@ +use parsely_rs::*; + +#[derive(ParselyRead)] +#[parsely_read(required_context("some_context_value: u8"))] +struct ReadContext { + #[parsely_read(assign_from = "some_context_value")] + one: u8, +} diff --git a/tests/ui/pass/enum_basic.rs b/tests/ui/pass/enum_basic.rs new file mode 100644 index 0000000..f5edfc6 --- /dev/null +++ b/tests/ui/pass/enum_basic.rs @@ -0,0 +1,45 @@ +use parsely_rs::*; + +#[derive(Debug, ParselyRead, ParselyWrite)] +#[parsely(key_type = "u8")] +enum Foo { + #[parsely(id = 1)] + One, + #[parsely(id = 2)] + Two(u8), + #[parsely(id = 3)] + Three { bar: u8, baz: u16 }, +} + +fn main() { + #[rustfmt::skip] + let mut bits = Bits::from_static_bytes( + &[ + // First instance: Foo::One, no data + 1, + // Second instance: Foo::Two, value of 1 + 2, 1, + // Third instance: Foo::Three, { bar: 1, baz: 42 } + 3, 1, 0, 42, + ] + ); + let bits_clone = bits.clone(); + + let one = Foo::read::(&mut bits, ()).expect("one"); + assert!(matches!(one, Foo::One)); + let two = Foo::read::(&mut bits, ()).expect("two"); + assert!(matches!(two, Foo::Two(1))); + let three = Foo::read::(&mut bits, ()).expect("three"); + assert!(matches!(three, Foo::Three { bar: 1, baz: 42 })); + + let mut bits_mut = BitsMut::new(); + one.write::(&mut bits_mut, ()) + .expect("successful write one"); + two.write::(&mut bits_mut, ()) + .expect("successful write two"); + three + .write::(&mut bits_mut, ()) + .expect("successful write three"); + + assert_eq!(bits_clone, bits_mut.freeze()); +} diff --git a/tests/ui/pass/newtype_struct.rs b/tests/ui/pass/newtype_struct.rs new file mode 100644 index 0000000..443f3d1 --- /dev/null +++ b/tests/ui/pass/newtype_struct.rs @@ -0,0 +1,17 @@ +use parsely_rs::*; + +#[derive(ParselyRead, ParselyWrite)] +struct Foo(u8); + +fn main() { + let mut bits = Bits::from_static_bytes(&[42]); + + let foo = Foo::read::(&mut bits, ()).expect("successful parse"); + assert_eq!(foo.0, 42); + + let mut bits_mut = BitsMut::new(); + + foo.write::(&mut bits_mut, ()) + .expect("successful write"); + assert_eq!(bits_mut.chunk_bytes()[0], 42); +} diff --git a/tests/ui/pass/read_context.rs b/tests/ui/pass/read_context.rs new file mode 100644 index 0000000..225a7fe --- /dev/null +++ b/tests/ui/pass/read_context.rs @@ -0,0 +1,15 @@ +use parsely_rs::*; + +#[derive(ParselyRead)] +#[parsely_read(required_context("some_context_value: u8"))] +struct ReadContext { + #[parsely_read(assign_from = "some_context_value")] + one: u8, +} + +fn main() { + let mut buf = Bits::from_static_bytes(&[]); + + let value = ReadContext::read::(&mut buf, (42,)).expect("successful parse"); + assert_eq!(value.one, 42); +} diff --git a/tests/ui/pass/tuple_struct.rs b/tests/ui/pass/tuple_struct.rs new file mode 100644 index 0000000..2657149 --- /dev/null +++ b/tests/ui/pass/tuple_struct.rs @@ -0,0 +1,19 @@ +use parsely_rs::*; + +#[derive(ParselyRead, ParselyWrite)] +struct Foo(u8, u8); + +fn main() { + let mut bits = Bits::from_static_bytes(&[42, 43]); + + let foo = Foo::read::(&mut bits, ()).expect("successful parse"); + assert_eq!(foo.0, 42); + assert_eq!(foo.1, 43); + + let mut bits_mut = BitsMut::new(); + foo.write::(&mut bits_mut, ()) + .expect("successful write"); + let bytes = bits_mut.chunk_bytes(); + assert_eq!(bytes[0], 42); + assert_eq!(bytes[1], 43); +}