Skip to content

Commit

Permalink
feat(derive): read_with and write_with attribute for fields
Browse files Browse the repository at this point in the history
The `read_with` and `write_with` attributes have been added.
They override the generated implementation for reading/writing individual fields,
similarly to how Serde's `deserialize_with` and `serialize_with` attributes work.

The `with` attribute has also been added as a utility,
which sets both `read_with` and `write_with` simultaneously.

See the README.md for more specific documentation.
Closes koute#12.
  • Loading branch information
tecc committed Jul 23, 2024
1 parent 81c665c commit 99087a0
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 58 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,26 @@ to the default value for its type and the EOF will be ignored.
Specifies a static string of bytes which will be written or has to be present
when reading before a given field.

### `#[speedy(read_with = $path)]`

Specifies a path to a function that can read this field, overriding the generated implementation.
The function's signature should be
`fn<'a, C: speedy::Context, R: speedy::Reader<'a, C>>(reader: &mut R) -> Result<$ty, C::Error>`,
where `$ty` is the type to be read.

### `#[speedy(write_with = $path)]`

Specifies a path to a function that can read this field, overriding the generated implementation.
The function's signature should be
`fn<C: speedy::Context, W: ?Sized + speedy::Writer<C>>(value: $ty, writer: &mut W) -> Result<(), C::Error>`,
where `$ty` is the type to be written.

### `#[speedy(with = $path)]`

Effectively shorthand for `#[speedy(read_with = $path::read, write_with = $path::write)]` (see above).

Cannot be combined with `read_with` nor `write_with`.

## Enum attributes

### `#[speedy(tag_type = $ty)]`
Expand Down
226 changes: 168 additions & 58 deletions speedy-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ mod kw {
syn::custom_keyword!( varint );
syn::custom_keyword!( unsafe_is_primitive );
syn::custom_keyword!( always );
syn::custom_keyword!( read_with );
syn::custom_keyword!( write_with );
syn::custom_keyword!( with );

syn::custom_keyword!( u7 );
syn::custom_keyword!( u8 );
Expand Down Expand Up @@ -756,7 +759,9 @@ struct Field< 'a > {
ty: Opt< Ty >,
skip: bool,
varint: bool,
constant_prefix: Option< syn::LitByteStr >
constant_prefix: Option< syn::LitByteStr >,
read_with: Option< syn::Path >,
write_with: Option< syn::Path >
}

impl< 'a > Field< 'a > {
Expand All @@ -766,7 +771,9 @@ impl< 'a > Field< 'a > {
self.length_type.is_none() &&
self.skip == false &&
self.varint == false &&
self.constant_prefix.is_none()
self.constant_prefix.is_none() &&
self.read_with.is_none() &&
self.write_with.is_none()
}

fn is_simple( &self ) -> bool {
Expand Down Expand Up @@ -858,6 +865,18 @@ enum FieldAttribute {
},
VarInt {
key_span: Span
},
ReadWith {
key_span: Span,
fn_path: syn::Path
},
WriteWith {
key_span: Span,
fn_path: syn::Path
},
With {
key_span: Span,
base_path: syn::Path // #[speedy(read_with = "$base_path::read", write_with = "$base_path::write")]
}
}

Expand Down Expand Up @@ -951,7 +970,31 @@ impl syn::parse::Parse for FieldAttribute {
FieldAttribute::VarInt {
key_span: key_token.span()
}
} else {
} else if lookahead.peek( kw::read_with ) {
let key_token = input.parse::< kw::read_with >()?;
let _: Token![=] = input.parse()?;
let fn_path: syn::Path = input.parse()?;
FieldAttribute::ReadWith {
key_span: key_token.span(),
fn_path
}
} else if lookahead.peek( kw::write_with ) {
let key_token = input.parse::< kw::write_with >()?;
let _: Token![=] = input.parse()?;
let fn_path: syn::Path = input.parse()?;
FieldAttribute::WriteWith {
key_span: key_token.span(),
fn_path
}
} else if lookahead.peek( kw::with ) {
let key_token = input.parse::< kw::with >()?;
let _: Token![=] = input.parse()?;
let base_path: syn::Path = input.parse()?;
FieldAttribute::With {
key_span: key_token.span(),
base_path
}
}else {
return Err( lookahead.error() )
};

Expand Down Expand Up @@ -1206,6 +1249,9 @@ fn get_fields< 'a, I: IntoIterator< Item = &'a syn::Field > + 'a >( fields: I )
let mut skip = false;
let mut varint = false;
let mut constant_prefix = None;
let mut read_with = None;
let mut write_with = None;
let mut used_with = false;
for attr in parse_attributes::< FieldAttribute >( &field.attrs )? {
match attr {
FieldAttribute::DefaultOnEof { key_span } => {
Expand Down Expand Up @@ -1253,6 +1299,47 @@ fn get_fields< 'a, I: IntoIterator< Item = &'a syn::Field > + 'a >( fields: I )
}

varint = true;
},
FieldAttribute::ReadWith { key_span, fn_path } => {
if read_with.is_some() {
let message = if used_with {
"Cannot combine 'read_with' with 'with'"
} else {
"Duplicate 'read_with'"
};
return Err( syn::Error::new( key_span, message ) );
}

read_with = Some( fn_path );
},
FieldAttribute::WriteWith { key_span, fn_path } => {
if write_with.is_some() {
let message = if used_with {
"Cannot combine 'write_with' with 'with'"
} else {
"Duplicate 'write_with'"
};
return Err( syn::Error::new( key_span, message ) );
}
write_with = Some( fn_path );
},
FieldAttribute::With { key_span, base_path } => {
if used_with {
let message = "Duplicate 'with'";
return Err( syn::Error::new( key_span, message ) );
}
if read_with.is_some() {
let message = "Cannot combine 'read_with' with 'with'";
return Err( syn::Error::new( key_span, message ) );
}
if write_with.is_some() {
let message = "Cannot combine 'write_with' with 'with'";
return Err( syn::Error::new( key_span, message ) );
}

read_with = Some( parse_quote!( #base_path :: read ) );
write_with = Some( parse_quote!( #base_path :: write ) );
used_with = true;
}
}
}
Expand Down Expand Up @@ -1391,7 +1478,9 @@ fn get_fields< 'a, I: IntoIterator< Item = &'a syn::Field > + 'a >( fields: I )
ty,
skip,
varint,
constant_prefix
constant_prefix,
read_with,
write_with
})
});

Expand Down Expand Up @@ -1641,30 +1730,35 @@ fn read_field_body( field: &Field ) -> TokenStream {
})
}};

let body = match field.ty.inner() {
Ty::String => read_string(),
Ty::Vec( .. ) => read_vec(),
Ty::CowSlice( .. ) => read_cow_slice(),
Ty::CowStr( .. ) => read_cow_str(),
Ty::HashMap( .. ) |
Ty::BTreeMap( .. ) => read_key_value_collection(),
Ty::HashSet( .. ) |
Ty::BTreeSet( .. ) => read_collection(),
Ty::CowHashMap( .. ) |
Ty::CowBTreeMap( .. ) => read_cow_key_value_collection(),
Ty::CowHashSet( .. ) |
Ty::CowBTreeSet( .. ) => read_cow_collection(),
Ty::RefSliceU8( .. ) => read_ref_slice_u8(),
Ty::RefSlice( _, inner_ty ) => read_ref_slice( inner_ty ),
Ty::RefStr( .. ) => read_ref_str(),
Ty::Array( _, length ) => read_array( *length ),
Ty::Primitive( .. ) if field.varint => read_u64_varint(),
Ty::Primitive( .. ) |
Ty::Ty( .. ) => {
assert!( field.length.is_none() );
quote! { _reader_.read_value() }
let body;
if let Some( ref read_with ) = field.read_with {
body = quote! { #read_with( _reader_ ) };
} else {
body = match field.ty.inner() {
Ty::String => read_string(),
Ty::Vec( .. ) => read_vec(),
Ty::CowSlice( .. ) => read_cow_slice(),
Ty::CowStr( .. ) => read_cow_str(),
Ty::HashMap( .. ) |
Ty::BTreeMap( .. ) => read_key_value_collection(),
Ty::HashSet( .. ) |
Ty::BTreeSet( .. ) => read_collection(),
Ty::CowHashMap( .. ) |
Ty::CowBTreeMap( .. ) => read_cow_key_value_collection(),
Ty::CowHashSet( .. ) |
Ty::CowBTreeSet( .. ) => read_cow_collection(),
Ty::RefSliceU8( .. ) => read_ref_slice_u8(),
Ty::RefSlice( _, inner_ty ) => read_ref_slice( inner_ty ),
Ty::RefStr( .. ) => read_ref_str(),
Ty::Array( _, length ) => read_array( *length ),
Ty::Primitive( .. ) if field.varint => read_u64_varint(),
Ty::Primitive( .. ) |
Ty::Ty( .. ) => {
assert!( field.length.is_none() );
quote! { _reader_.read_value() }
}
}
};
}

let body = match field.ty {
Opt::Plain( _ ) => body,
Expand Down Expand Up @@ -1779,32 +1873,37 @@ fn write_field_body( field: &Field ) -> TokenStream {
}
}};

let body = match field.ty.inner() {
Ty::String |
Ty::CowStr( .. ) |
Ty::RefStr( .. )
let body;
if let Some( ref write_with ) = field.write_with {
body = quote!( #write_with ( #name, _writer_ ) );
} else {
body = match field.ty.inner() {
Ty::String |
Ty::CowStr( .. ) |
Ty::RefStr( .. )
=> write_str(),
Ty::Vec( .. ) |
Ty::CowSlice( .. ) |
Ty::RefSliceU8( .. ) |
Ty::RefSlice( .. )
Ty::Vec( .. ) |
Ty::CowSlice( .. ) |
Ty::RefSliceU8( .. ) |
Ty::RefSlice( .. )
=> write_slice(),
Ty::HashMap( .. ) |
Ty::HashSet( .. ) |
Ty::BTreeMap( .. ) |
Ty::BTreeSet( .. ) |
Ty::CowHashMap( .. ) |
Ty::CowHashSet( .. ) |
Ty::CowBTreeMap( .. ) |
Ty::CowBTreeSet( .. ) => write_collection(),
Ty::Array( .. ) => write_array(),
Ty::Primitive( .. ) if field.varint => write_u64_varint(),
Ty::Primitive( .. ) |
Ty::Ty( .. ) => {
assert!( field.length.is_none() );
quote! { _writer_.write_value( #name )?; }
}
};
Ty::HashMap( .. ) |
Ty::HashSet( .. ) |
Ty::BTreeMap( .. ) |
Ty::BTreeSet( .. ) |
Ty::CowHashMap( .. ) |
Ty::CowHashSet( .. ) |
Ty::CowBTreeMap( .. ) |
Ty::CowBTreeSet( .. ) => write_collection(),
Ty::Array( .. ) => write_array(),
Ty::Primitive( .. ) if field.varint => write_u64_varint(),
Ty::Primitive( .. ) |
Ty::Ty( .. ) => {
assert!( field.length.is_none() );
quote! { _writer_.write_value( #name )?; }
}
};
}

let body = match field.ty {
Opt::Plain( _ ) => body,
Expand Down Expand Up @@ -1979,7 +2078,7 @@ impl< 'a > Enum< 'a > {
}

fn get_minimum_bytes( field: &Field ) -> Option< TokenStream > {
if field.default_on_eof || field.length.is_some() || field.skip {
if field.default_on_eof || field.length.is_some() || field.skip || field.read_with.is_some() || field.write_with.is_some() {
None
} else {
let mut length = match field.ty {
Expand Down Expand Up @@ -2077,14 +2176,25 @@ fn generate_is_primitive( fields: &[Field], is_writable: bool, check_order: bool
}

let ty = &field.raw_ty;
// NOTE(with-attribute):
// For now, `with`-attributes will forcefully make
// the return value of `speedy_is_primitive` false.
if is_writable {
is_primitive.push( quote! {
<#ty as speedy::Writable< C_ >>::speedy_is_primitive()
});
if field.write_with.is_some() {
return quote! { false };
} else {
is_primitive.push( quote! {
<#ty as speedy::Writable< C_ >>::speedy_is_primitive()
});
}
} else {
is_primitive.push( quote! {
<#ty as speedy::Readable< 'a_, C_ >>::speedy_is_primitive()
});
if field.read_with.is_some() {
return quote! { false };
} else {
is_primitive.push( quote! {
<#ty as speedy::Readable< 'a_, C_ >>::speedy_is_primitive()
});
}
}

fields_size.push( quote! {
Expand Down

0 comments on commit 99087a0

Please sign in to comment.