Overview
The Parse derive macro provides powerful code generation for converting between different data representations. It’s particularly useful for transforming API request types (e.g., Protocol Buffers) into validated domain models.
The Parse derive macro automatically generates both parsing (source → target) and writing (target → source) code when write is specified.
Basic Usage
Simple Field Mapping
use bomboni_request_derive :: Parse ;
use bomboni_request :: parse :: RequestParse ;
#[derive( Debug , Clone , PartialEq , Default )]
struct UserProto {
user_name : String ,
user_age : i32 ,
user_email : Option < String >,
}
#[derive( Debug , Clone , PartialEq , Parse )]
#[parse(source = UserProto , write)]
struct User {
#[parse(source = "user_name" )]
name : String ,
#[parse(source = "user_age" )]
age : i32 ,
#[parse(source = "user_email?" )]
email : Option < String >,
}
let proto = UserProto {
user_name : "Alice" . to_string (),
user_age : 30 ,
user_email : Some ( "[email protected] " . to_string ()),
};
let user = User :: parse ( proto ) . unwrap ();
assert_eq! ( user . name, "Alice" );
assert_eq! ( user . age, 30 );
assert_eq! ( user . email, Some ( "[email protected] " . to_string ()));
Field Mapping
Source Attribute
Map fields from different names in the source type:
#[derive( Parse )]
#[parse(source = ProtoUser , write)]
struct User {
#[parse(source = "user_name" )] // From: proto.user_name
name : String ,
#[parse(source = "user_email?" )] // From: proto.user_email (optional unwrap)
email : Option < String >,
}
Nested Field Access
Access nested fields with dot notation:
#[derive( Parse )]
#[parse(source = Request , write)]
struct ParsedRequest {
#[parse(source = "user.name" )] // From: request.user.name
user_name : String ,
#[parse(source = "user?.email?" )] // Optional unwrapping at each level
user_email : Option < String >,
}
Unwrap and transform values during parsing:
Unwrap
UnwrapOr
UnwrapOrDefault
Unbox
Field
#[derive( Parse )]
#[parse(source = Proto , write)]
struct Model {
#[parse(extract = [ Unwrap ])] // Unwrap Option<T> → T
required_field : String ,
}
Chain extractions together:
#[parse(extract = [ Unwrap , Unbox , Unwrap , Unbox ])]
inner_value : i32 ,
// Processes: Option<Box<Option<Box<i32>>>> → i32
#[derive( Parse )]
#[parse(source = Proto , write)]
struct Model {
#[parse(extract = [ StringFilterEmpty ])] // Convert "" → None
possibly_empty : Option < String >,
#[parse(regex = "^[a-z]+$" )] // Validate with regex
validated_string : String ,
}
Type Conversions
try_from Conversions
Convert between compatible types:
#[derive( Parse )]
#[parse(source = Proto , write)]
struct Model {
#[parse(try_from = i32 )] // Convert i32 → i64
large_number : i64 ,
#[parse(try_from = i32 , extract = [ Unwrap ])]
required_converted : i64 , // Option<i32> → i64
}
Enumeration Parsing
Convert i32 enum values to Rust enums:
#[derive( Debug , PartialEq , Clone , Copy , Default )]
#[repr( i32 )]
enum Status {
#[default]
Unspecified = 0 ,
Active = 1 ,
Inactive = 2 ,
}
impl TryFrom < i32 > for Status {
type Error = ();
fn try_from ( value : i32 ) -> Result < Self , Self :: Error > {
match value {
0 => Ok ( Self :: Unspecified ),
1 => Ok ( Self :: Active ),
2 => Ok ( Self :: Inactive ),
_ => Err (()),
}
}
}
#[derive( Parse )]
#[parse(source = Proto , write)]
struct Model {
#[parse(enumeration)] // Parse i32 → Status
status : Status ,
#[parse(enumeration, extract = [ EnumerationFilterUnspecified ])]
optional_status : Option < Status >, // Filter out 0 (Unspecified)
}
Wrapper Types
Handle Protocol Buffer wrapper types:
use bomboni_proto :: google :: protobuf :: { Int32Value , StringValue };
#[derive( Parse )]
#[parse(source = Proto , write)]
struct Model {
#[parse(wrapper)] // Int32Value → i32
age : i32 ,
#[parse(wrapper)] // StringValue → String
name : String ,
#[parse(wrapper, unspecified)] // Treat empty wrapper as None
optional_value : Option < String >,
}
Derive Functions
Provide custom parse and write functions:
#[derive( Parse )]
#[parse(source = Proto , write)]
struct Model {
#[parse(derive { parse = parse_position, write = write_position })]
position : String ,
}
fn parse_position ( proto : & Proto ) -> RequestResult < String > {
Ok ( format! ( "{}, {}" , proto . x, proto . y))
}
fn write_position ( model : & Model , proto : & mut Proto ) {
let parts : Vec < & str > = model . position . split ( ", " ) . collect ();
proto . x = parts [ 0 ] . parse () . unwrap ();
proto . y = parts [ 1 ] . parse () . unwrap ();
}
Field-Specific Derivation
#[parse(source = "name" , derive { parse = parse_id, write = write_id })]
id : u64 ,
fn parse_id ( name : String ) -> RequestResult < u64 > {
if name . is_empty () {
return Err ( CommonError :: RequiredFieldMissing . into ());
}
Ok ( name . parse () . unwrap ())
}
fn write_id ( id : u64 ) -> String {
id . to_string ()
}
Collections
Vectors
#[derive( Parse )]
#[parse(source = Proto , write)]
struct Model {
values : Vec < i32 >,
#[parse(regex = "^[a-z]$" )] // Validate each element
strings : Vec < String >,
items : Vec < ParsedItem >, // Parse nested items
}
Maps
use std :: collections :: { BTreeMap , HashMap };
#[derive( Parse )]
#[parse(source = Proto , write)]
struct Model {
values_map : BTreeMap < String , i32 >,
items_map : HashMap < i32 , ParsedItem >,
#[parse(enumeration)] // Parse enum values
enum_map : BTreeMap < i32 , Status >,
}
Oneof Handling
Enum Oneofs
#[derive( Debug , Clone )]
enum ProtoKind {
StringValue ( String ),
IntValue ( i32 ),
NestedValue ( Nested ),
}
impl ProtoKind {
pub fn get_variant_name ( & self ) -> & ' static str {
match self {
Self :: StringValue ( _ ) => "string_value" ,
Self :: IntValue ( _ ) => "int_value" ,
Self :: NestedValue ( _ ) => "nested_value" ,
}
}
}
#[derive( Parse )]
#[parse(source = ProtoKind , write)]
enum ParsedKind {
StringValue ( String ),
IntValue ( i32 ),
NestedValue ( ParsedNested ),
}
Tagged Union (Proto3 style)
#[derive( Debug , Clone )]
struct ProtoValue {
kind : Option < ProtoKind >,
}
#[derive( Parse )]
#[parse(
source = ProtoValue ,
tagged_union { oneof = ProtoKind , field = kind },
write
)]
enum ParsedValue {
StringValue ( String ),
IntValue ( i32 ),
}
Oneof in Structs
#[derive( Parse )]
#[parse(source = Proto , write)]
struct Model {
#[parse(oneof)] // Direct enum field
value : ParsedKind ,
#[parse(oneof, extract = [ Unwrap ])] // Unwrap Option<ProtoKind>
required_value : ParsedKind ,
}
List and Search Query Integration
List Query Parsing
use bomboni_request :: query :: list :: ListQuery ;
#[derive( Default )]
struct ListUsersRequest {
page_size : Option < u32 >,
page_token : Option < String >,
filter : Option < String >,
order_by : Option < String >,
}
#[derive( Parse )]
#[parse(source = ListUsersRequest , write)]
struct ParsedListUsersRequest {
#[parse(list_query)]
query : ListQuery ,
}
// Use with builder
let request = ListUsersRequest {
page_size : Some ( 50 ),
filter : Some ( r#"displayName = "John""# . into ()),
order_by : Some ( "age desc" . into ()),
page_token : None ,
};
let parsed = ParsedListUsersRequest :: parse_list_query ( request , & list_builder ) . unwrap ();
assert_eq! ( parsed . query . page_size, 50 );
Search Query Parsing
use bomboni_request :: query :: search :: SearchQuery ;
#[derive( Default )]
struct SearchUsersRequest {
query : String ,
page_size : Option < u32 >,
page_token : Option < String >,
filter : Option < String >,
order_by : Option < String >,
}
#[derive( Parse )]
#[parse(source = SearchUsersRequest , write)]
struct ParsedSearchUsersRequest {
#[parse(search_query)]
query : SearchQuery ,
}
let request = SearchUsersRequest {
query : "john doe" . to_string (),
page_size : Some ( 25 ),
filter : Some ( r#"age >= 18"# . into ()),
order_by : Some ( "age desc" . into ()),
page_token : None ,
};
let parsed = ParsedSearchUsersRequest :: parse_search_query ( request , & search_builder ) . unwrap ();
assert_eq! ( parsed . query . query, "john doe" );
Complete Example
use bomboni_request_derive :: Parse ;
use bomboni_request :: parse :: RequestParse ;
use bomboni_proto :: google :: protobuf :: StringValue ;
#[derive( Debug , Clone , PartialEq , Default )]
struct UserProto {
user_id : String ,
user_name : String ,
user_email : Option < String >,
user_age : i32 ,
user_status : i32 ,
user_tags : Vec < String >,
metadata : Option < StringValue >,
}
#[derive( Debug , Clone , Copy , PartialEq , Default )]
#[repr( i32 )]
enum UserStatus {
#[default]
Unspecified = 0 ,
Active = 1 ,
Inactive = 2 ,
}
impl TryFrom < i32 > for UserStatus {
type Error = ();
fn try_from ( value : i32 ) -> Result < Self , Self :: Error > {
match value {
0 => Ok ( Self :: Unspecified ),
1 => Ok ( Self :: Active ),
2 => Ok ( Self :: Inactive ),
_ => Err (()),
}
}
}
#[derive( Debug , Clone , PartialEq , Parse )]
#[parse(source = UserProto , write)]
struct User {
#[parse(source = "user_id" )]
id : String ,
#[parse(source = "user_name" )]
name : String ,
#[parse(source = "user_email?" )]
email : Option < String >,
#[parse(source = "user_age" )]
age : i32 ,
#[parse(source = "user_status" , enumeration)]
status : UserStatus ,
#[parse(source = "user_tags" , regex = "^[a-z0-9-]+$" )]
tags : Vec < String >,
#[parse(wrapper, unspecified)]
metadata : Option < String >,
}
let proto = UserProto {
user_id : "123" . into (),
user_name : "Alice" . into (),
user_email : Some ( "[email protected] " . into ()),
user_age : 30 ,
user_status : 1 ,
user_tags : vec! [ "admin" . into (), "verified" . into ()],
metadata : Some ( StringValue { value : "extra" . into () }),
};
let user = User :: parse ( proto ) . unwrap ();
assert_eq! ( user . id, "123" );
assert_eq! ( user . name, "Alice" );
assert_eq! ( user . status, UserStatus :: Active );
assert_eq! ( user . tags . len (), 2 );
Error Handling
Parse errors include path information for debugging:
use bomboni_request :: error :: { RequestError , PathError , CommonError };
match User :: parse ( invalid_proto ) {
Ok ( user ) => println! ( "Parsed: {:?}" , user ),
Err ( RequestError :: Path ( PathError { error , path , .. })) => {
eprintln! ( "Error at {}: {:?}" ,
path . iter ()
. map ( | s | s . to_string ())
. collect :: < Vec < _ >>()
. join ( "." ),
error
);
}
Err ( e ) => eprintln! ( "Parse error: {:?}" , e ),
}
Best Practices
Use Source Attributes
Always use #[parse(source = "field")] to make field mappings explicit
Validate Early
Use regex validation and enumeration filtering to catch invalid data during parsing
Handle Optional Fields
Use extract modifiers (Unwrap, UnwrapOr) to clearly express required vs optional fields
Provide Write Implementations
Include write in the parse attribute to generate bidirectional conversion
The get_variant_name() method must be implemented for oneof enums. This is used by the macro to match variant names.
Pagination Use Parse with list and search queries
Filtering Validate parsed data with filters