Skip to main content
The bomboni_request_derive crate provides procedural macros for parsing and validating request types, particularly for converting between protobuf messages and domain models.

Parse Derive Macro

The Parse derive macro generates code for converting between different data representations with validation and error handling.
use bomboni_request_derive::Parse;

#[derive(Parse)]
#[parse(source = "proto::User", write)]
struct User {
    name: String,
    email: String,
    age: i32,
}

Struct-Level Attributes

source

Specifies the source type to parse from (required).
#[parse(source = "proto::UserMessage")]

write

Generates From trait implementation for converting back to source type.
#[parse(source = "proto::User", write)]

serialize_as / deserialize_as / serde_as

Generates serde implementations for the source type.
#[parse(source = "proto::User", serde_as)]
// Equivalent to:
#[parse(source = "proto::User", serialize_as, deserialize_as)]

request

Marks the message as a request for enhanced error handling.
#[parse(source = "CreateUserRequest", request)]
// Or with custom name:
#[parse(source = "CreateUserRequest", request { name = "users.create" })]

tagged_union

Creates tagged union from a oneof field.
#[parse(
    source = "ValueContainer",
    tagged_union { oneof = ValueKind, field = kind }
)]
enum Value {
    String(String),
    Number(i64),
}

Crate Path Customization

#[parse(
    source = "proto::User",
    bomboni_crate = "crate::bomboni",
    bomboni_proto_crate = "crate::proto",
    bomboni_request_crate = "crate::request",
    serde_crate = "crate::serde"
)]

Field-Level Attributes

source

Specifies the source field name or path.
#[parse(source = "user_name")]
name: String,

// Nested field access with optional extraction
#[parse(source = "address?.city")]
city: Option<String>,

// Multiple levels
#[parse(source = "user?.profile?.bio")]
bio: Option<String>,

source_field

Indicates the source field name is the same as the target field name.
#[parse(source_field, derive = my_transform)]
field_name: CustomType,

skip

Skips parsing this field entirely.
#[parse(skip)]
calculated_field: String,

keep

Keeps the source and target fields identical without parsing.
#[parse(keep)]
metadata: HashMap<String, String>,

keep_primitive

Parses only the container, keeping primitive types unchanged.
#[parse(keep_primitive)]
nested: Option<PrimitiveMessage>,

unspecified

Allows unspecified enum values and empty strings.
#[parse(wrapper, unspecified)]
optional_name: String,

extract

Defines extraction steps for transforming field values.
// Unwrap Option
#[parse(extract = [Unwrap])]
required_value: String,

// Unwrap with default
#[parse(extract = [UnwrapOr(42)])]
count: i32,

// Unwrap or use type default
#[parse(extract = [UnwrapOrDefault])]
items: Vec<String>,

// Unbox value
#[parse(extract = [Unbox])]
value: i32,

// Filter empty strings
#[parse(extract = [StringFilterEmpty])]
optional_text: Option<String>,

// Filter unspecified enum values
#[parse(enumeration, extract = [EnumerationFilterUnspecified])]
status: Option<Status>,

// Multiple steps
#[parse(extract = [Unwrap, Unbox, StringFilterEmpty])]
complex_field: Option<String>,

// Extract nested field
#[parse(extract = [Field("inner")])]
inner_value: InnerType,

wrapper

Parses protobuf wrapper types.
#[parse(wrapper)]
optional_count: Option<i32>,  // From Int32Value

#[parse(wrapper)]
optional_name: Option<String>,  // From StringValue

#[parse(wrapper)]
required_value: f64,  // From DoubleValue
Supported wrapper mappings:
  • StringValueString
  • BoolValuebool
  • FloatValuef32
  • DoubleValuef64
  • Int32Valuei8, i16, i32
  • UInt32Valueu8, u16, u32
  • Int64Valuei64, isize
  • UInt64Valueu64, usize

oneof

Parses from a protobuf oneof field.
#[parse(oneof)]
value: ParsedValue,

#[parse(oneof, extract = [Unwrap])]
required_value: ParsedValue,

enumeration

Parses enum from i32 value.
#[parse(enumeration)]
status: Status,

#[parse(enumeration)]
values: Vec<Status>,

#[parse(enumeration)]
status_map: HashMap<String, Status>,

regex

Validates string against a regular expression.
#[parse(regex = r"^[a-zA-Z0-9]+$")]
username: String,

#[parse(regex = r"^[^@]+@[^@]+\.[^@]+$")]
email: String,

timestamp

Parses google.protobuf.Timestamp into UtcDateTime.
#[parse(timestamp)]
created_at: UtcDateTime,

try_from

Converts field using TryFrom or TryInto.
#[parse(try_from = "UserId::from_str")]
user_id: UserId,

#[parse(try_from = i32)]
value: i64,

convert

Uses custom conversion functions.
// Single function for both directions
#[parse(convert = my_module)]
field: CustomType,

// Separate parse and write functions
#[parse(convert { parse = parse_fn, write = write_fn })]
field: CustomType,

mod my_module {
    pub fn parse(value: SourceType) -> RequestResult<TargetType> {
        // ...
    }
    
    pub fn write(value: TargetType) -> SourceType {
        // ...
    }
}

derive

Uses derived parsing implementation for custom transformations.
// Module with parse and write functions
#[parse(derive = my_module)]
field: CustomType,

// Explicit functions
#[parse(derive { parse = parse_fn, write = write_fn })]
field: CustomType,

// Borrow source or target
#[parse(derive { parse = parse_fn, write = write_fn, source_borrow })]
field: CustomType,

// Full struct derivation
#[parse(derive { parse = derive_parse, write = derive_write })]
derived_field: DerivedType,

fn derive_parse(source: &SourceStruct) -> RequestResult<DerivedType> {
    // Custom parsing logic
}

fn derive_write(target: &ParsedStruct, source: &mut SourceStruct) {
    // Custom writing logic
}

resource

Parses resource metadata fields.
#[parse(resource)]
resource: ParsedResource,

// Customize field mapping
#[parse(resource {
    fields {
        name = true,
        create_time = true,
        update_time = true,
        delete_time = false,
        deleted = true,
        etag = true,
    }
})]
resource: ParsedResource,

list_query

Parses list query parameters.
#[parse(list_query)]
query: ListQuery,

// Customize field mapping
#[parse(list_query {
    page_size = true,
    page_token = true,
    filter = true,
    order_by = true,
})]
query: ListQuery,

search_query

Parses search query parameters.
#[parse(search_query)]
query: SearchQuery,

// Customize field mapping
#[parse(search_query {
    query = true,
    page_size = true,
    page_token = true,
    filter = true,
    order_by = true,
})]
query: SearchQuery,

field_mask

Parses field only if field mask allows it.
// Default mask field name is "update_mask"
#[parse(source = "book?.title", field_mask)]
title: Option<String>,

// Custom mask field
#[parse(source = "book?.author", field_mask(custom_mask))]
author: Option<String>,

// Explicit field and mask
#[parse(
    source = "book?.publisher",
    field_mask { field = book, mask = update_mask }
)]
publisher: Option<String>,

Complex Examples

Basic Struct Parsing

#[derive(Parse)]
#[parse(source = "proto::User", write)]
struct User {
    #[parse(source = "user_id")]
    id: String,
    
    #[parse(source = "display_name")]
    name: String,
    
    #[parse(source = "email_address", regex = r"^[^@]+@[^@]+\.[^@]+$")]
    email: String,
    
    #[parse(enumeration)]
    status: UserStatus,
    
    #[parse(timestamp)]
    created_at: UtcDateTime,
}

Optional Fields and Extraction

#[derive(Parse)]
#[parse(source = "UpdateBookRequest", write)]
struct UpdateBook {
    #[parse(source = "book?.name", convert = book_id_convert)]
    id: BookId,
    
    #[parse(source = "book?.title", field_mask { field = book, mask = update_mask })]
    title: Option<String>,
    
    #[parse(source = "book?.description", field_mask)]
    description: Option<String>,
    
    #[parse(source = "book?.published_date", field_mask, timestamp)]
    published_date: Option<UtcDateTime>,
}

Oneof and Tagged Unions

// Source protobuf
// message Value {
//   oneof kind {
//     string text = 1;
//     int32 number = 2;
//     bool flag = 3;
//   }
// }

#[derive(Parse)]
#[parse(source = "Value", write)]
enum ParsedValue {
    Text(String),
    Number(i32),
    Flag(bool),
}

// Tagged union wrapper
// message Container {
//   Value value = 1;
// }

#[derive(Parse)]
#[parse(
    source = "Container",
    tagged_union { oneof = ValueKind, field = kind },
    write
)]
enum Value {
    Text(String),
    Number(i32),
    Flag(bool),
}

Collections

#[derive(Parse)]
#[parse(source = "proto::Book", write)]
struct Book {
    #[parse(regex = r"^[a-z0-9-]+$")]
    tags: Vec<String>,
    
    authors: Vec<Author>,
    
    #[parse(enumeration)]
    categories: Vec<Category>,
    
    metadata: HashMap<String, String>,
    
    #[parse(enumeration)]
    properties: BTreeMap<String, PropertyType>,
}

Wrapper Types

#[derive(Parse)]
#[parse(source = "proto::Product", write)]
struct Product {
    #[parse(wrapper)]
    name: String,
    
    #[parse(wrapper)]
    description: Option<String>,
    
    #[parse(wrapper)]
    price: f64,
    
    #[parse(wrapper, unspecified)]
    discount: Option<f32>,
}

Query Parsing

#[derive(Parse)]
#[parse(source = "ListBooksRequest", write)]
struct ListBooks {
    #[parse(list_query)]
    query: ListQuery,
    
    #[parse(source = "parent", convert = parent_id_convert)]
    parent_id: LibraryId,
}

#[derive(Parse)]
#[parse(source = "SearchBooksRequest", write)]
struct SearchBooks {
    #[parse(search_query)]
    query: SearchQuery,
    
    #[parse(source = "parent", convert = parent_id_convert)]
    parent_id: LibraryId,
}

Derived Fields

#[derive(Parse)]
#[parse(source = "proto::Order", write)]
struct Order {
    id: String,
    items: Vec<OrderItem>,
    
    // Derive total from items
    #[parse(derive { parse = calculate_total, write = write_total })]
    total: f64,
    
    // Derive status from multiple fields
    #[parse(derive { parse = derive_status, write = write_status })]
    status: OrderStatus,
}

fn calculate_total(source: &proto::Order) -> RequestResult<f64> {
    Ok(source.items.iter().map(|i| i.price).sum())
}

fn write_total(target: &Order, source: &mut proto::Order) {
    // Total is computed, no write needed
}

fn derive_status(source: &proto::Order) -> RequestResult<OrderStatus> {
    // Complex logic to determine status
    Ok(OrderStatus::from_fields(&source))
}

fn write_status(status: OrderStatus) -> OrderStatusProto {
    status.to_proto()
}

Helper Macros

parse_resource_name

Parses resource names into typed segments.
use bomboni_request_derive::parse_resource_name;

let parse = parse_resource_name!({
    "users": u32,
    "projects": u64,
});

let name = "users/42/projects/1337";
let (user_id, project_id) = parse(name).unwrap();
assert_eq!(user_id, 42);
assert_eq!(project_id, 1337);

// Optional ending segments
let parse = parse_resource_name!({
    "libraries": String,
    "books"?: String,
});

let result1 = parse("libraries/lib1").unwrap();
assert_eq!(result1, ("lib1".into(), None));

let result2 = parse("libraries/lib1/books/book1").unwrap();
assert_eq!(result2, ("lib1".into(), Some("book1".into())));

derived_map

Creates derived map types for complex field transformations.
use bomboni_request_derive::derived_map;

derived_map! {
    pub struct MyFieldMap {
        field1: Type1,
        field2: Type2,
    }
}

Error Handling

The Parse macro generates code that uses RequestResult<T> which wraps errors with path information for debugging.
match ParsedUser::parse(proto_user) {
    Ok(user) => { /* ... */ },
    Err(RequestError::Path(error)) => {
        println!("Error at {}: {}", error.path_to_string(), error.error);
        // Example: "Error at user.email: Invalid string format"
    },
    Err(e) => { /* ... */ },
}
Common error types:
  • CommonError::RequiredFieldMissing - Required field is empty or None
  • CommonError::InvalidStringFormat - String doesn’t match regex
  • CommonError::InvalidEnumValue - Invalid enum discriminant
  • CommonError::InvalidNumericValue - Numeric parsing failed
  • PathError - Error with field path context

Build docs developers (and LLMs) love