Skip to main content

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>,
}

Transformation Options

Extract Modifiers

Unwrap and transform values during parsing:
#[derive(Parse)]
#[parse(source = Proto, write)]
struct Model {
    #[parse(extract = [Unwrap])]            // Unwrap Option<T> → T
    required_field: String,
}

Multiple Extractions

Chain extractions together:
#[parse(extract = [Unwrap, Unbox, Unwrap, Unbox])]
inner_value: i32,
// Processes: Option<Box<Option<Box<i32>>>> → i32

String Transformations

#[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>,
}

Custom Transformations

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

1

Use Source Attributes

Always use #[parse(source = "field")] to make field mappings explicit
2

Validate Early

Use regex validation and enumeration filtering to catch invalid data during parsing
3

Handle Optional Fields

Use extract modifiers (Unwrap, UnwrapOr) to clearly express required vs optional fields
4

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

Build docs developers (and LLMs) love