Crate assert_struct

Crate assert_struct 

Source
Expand description

§assert-struct: Ergonomic Structural Assertions

assert-struct is a procedural macro that enables clean, readable assertions for complex data structures without verbose field-by-field comparisons. When assertions fail, it provides clear, actionable error messages showing exactly what went wrong, including field paths and expected vs actual values.

This comprehensive guide teaches you how to use assert-struct effectively in your tests. After reading this documentation, you’ll be familiar with all capabilities and able to leverage the full power of structural assertions.

§Table of Contents

§Quick Start

Add to your Cargo.toml:

[dev-dependencies]
assert-struct = "0.2"

Basic example:

use assert_struct::assert_struct;

#[derive(Debug)]
struct User {
    name: String,
    age: u32,
    email: String,
}

let user = User {
    name: "Alice".to_string(),
    age: 30,
    email: "alice@example.com".to_string(),
};

// Only check the fields you care about
assert_struct!(user, User {
    name: "Alice",
    age: 30,
    ..  // Ignore email
});

§Core Concepts

§Basic Assertions

The simplest use case is asserting all fields of a struct:

let point = Point { x: 10, y: 20 };

assert_struct!(point, Point {
    x: 10,
    y: 20,
});

String fields work naturally with string literals:

let msg = Message {
    text: "Hello world".to_string(),
    urgent: false,
};

assert_struct!(msg, Message {
    text: "Hello world",  // No .to_string() needed!
    urgent: false,
});

§Partial Matching

Use .. to ignore fields you don’t want to check:

// Only verify name and email, ignore id and created_at
assert_struct!(user, User {
    name: "Alice",
    email: "alice@example.com",
    ..
});

§Nested Structures

Assert on deeply nested data without repetitive field access:

assert_struct!(order, Order {
    customer: Customer {
        name: "Bob",
        address: Address {
            city: "Paris",
            country: "France",
        },
    },
    total: 99.99,
});

// Or with partial matching
assert_struct!(order, Order {
    customer: Customer {
        name: "Bob",
        address: Address { city: "Paris", .. },
        ..
    },
    ..
});

// Direct nested field access (no need to nest structs)
assert_struct!(order, Order {
    customer.name: "Bob",
    customer.address.city: "Paris",
    customer.address.country: "France",
    total: > 50.0,
    ..
});

§Pattern Types

§Comparison Operators

Use comparison operators for numeric assertions:

assert_struct!(metrics, Metrics {
    cpu: < 80.0,          // Less than 80%
    memory: <= 2048,      // At most 2GB
    requests: > 100,      // More than 100
});

All comparison operators work: <, <=, >, >=

§Equality Operators

Use explicit equality for clarity:

assert_struct!(status, Status {
    code: == 200,         // Explicit equality
    active: != false,     // Not equal to false
});

§Range Patterns

Use ranges for boundary checks:

assert_struct!(person, Person {
    age: 18..=65,         // Working age range
    score: 0.0..100.0,    // Valid score range
});

§Regex Patterns

Match string patterns with regular expressions (requires regex feature, enabled by default):

assert_struct!(account, Account {
    username: =~ r"^[a-z_]+$",        // Lowercase and underscores
    email: =~ r"@company\.com$",      // Company email domain
});

§Method Call Patterns

Call methods on fields and assert on their results:

assert_struct!(data, Data {
    content.len(): 11,                    // String length
    items.len(): >= 5,                    // Vector size check
    metadata.is_some(): true,             // Option state
    cache.contains_key("key1"): true,     // HashMap lookup
    ..
});

Method calls work with arguments too:

assert_struct!(text, Text {
    content.starts_with("hello"): true,
    ..
});

assert_struct!(text, Text {
    content.contains("world"): true,
    ..
});

§Data Types

§Collections (Vec/Slice)

Element-wise pattern matching for vectors:

// Exact matching
assert_struct!(data, Data {
    values: [5, 15, 25],
    names: ["alice", "bob"],  // String literals work in slices too!
});

// Pattern matching for each element
assert_struct!(data, Data {
    values: [> 0, < 20, >= 25],    // Different pattern per element
    names: ["alice", "bob"],
});

Partial slice matching:

assert_struct!(data, Data {
    items: [1, 2, ..],      // First two elements, ignore rest
});

assert_struct!(data, Data {
    items: [.., 4, 5],      // Last two elements
});

assert_struct!(data, Data {
    items: [1, .., 5],      // First and last elements
});

§Maps (HashMap/BTreeMap)

Pattern matching for map-like structures using duck typing (works with any type that has len() and get() methods):

// Exact matching (checks map length)
assert_struct!(config, Config {
    settings: #{ "theme": "dark", "language": "en" },
    flags: #{ "debug": true, "verbose": false },
});

// Partial matching (ignores map length)
assert_struct!(config, Config {
    settings: #{ "theme": "dark", .. },     // Only check theme
    flags: #{ "debug": true, .. },          // Only check debug flag
});

Advanced map patterns with comparisons and nested patterns:

// Pattern matching with comparison operators
assert_struct!(analytics, Analytics {
    metrics: #{
        "views": > 500,           // More than 500 views
        "clicks": >= 10,          // At least 10 clicks
        "conversions": > 0,       // Some conversions
        ..
    },
    metadata: #{
        "version": =~ r"^v\d+\.\d+\.\d+$",  // Semantic version pattern
        "environment": != "development",     // Not in development
        ..
    },
});

Empty and wildcard map matching:

assert_struct!(data, Data {
    cache: #{},             // Exactly empty map (len() == 0)
    config: #{ .. },        // Any map content (wildcard)
});

§Tuples

Full support for multi-field tuples:

// Basic tuple matching
assert_struct!(data, Data {
    point: (15, 25),
    metadata: ("info", 100, true),  // String literals work in tuples!
});

// Advanced patterns
assert_struct!(data, Data {
    point: (> 10, < 30),           // Comparison operators
    metadata: ("info", >= 50, true),
});

Tuple method calls:

assert_struct!(data, Data {
    coords: (0.len(): 8, 1.len(): 3),  // Method calls on tuple elements
});

§Enums (Option/Result/Custom)

§Option Types

assert_struct!(user, User {
    name: Some("Alice"),
    age: Some(30),
});

// Advanced patterns inside Option
assert_struct!(user, User {
    name: Some("Alice"),
    age: Some(>= 18),      // Adult check inside Some
});

§Result Types

assert_struct!(response, Response {
    result: Ok("success"),
});

// Pattern matching inside Result
assert_struct!(response, Response {
    result: Ok(=~ r"^user\d+$"),  // Regex inside Ok
});

§Custom Enums

// Unit variants
let active_account = Account { status: Status::Active };
assert_struct!(active_account, Account {
    status: Status::Active,
});

// Struct variants with partial matching
assert_struct!(account, Account {
    status: Status::Pending { since: "2024-01-01" },
});

§Smart Pointers

Dereference smart pointers directly in patterns:

assert_struct!(cache, Cache {
    *data: "cached",       // Dereference Arc<String>
    *count: > 40,          // Dereference Box<i32> with comparison
    *shared: true,         // Dereference Rc<bool>
});

Multiple dereferencing for nested pointers:

assert_struct!(nested, Nested {
    **value: 42,           // Double dereference
});

§Wildcard Patterns

Use wildcard patterns (_) to avoid importing types while still asserting on their structure:

// No need to import User or Metadata types!
assert_struct!(response, _ {
    user: _ {
        id: 123,
        name: "Alice",
        ..
    },
    metadata: _ {
        version: "1.0",
        ..  // Ignore other metadata fields
    },
    ..
});

This is particularly useful when testing API responses where you don’t want to import all the nested types:

// Test deeply nested structures without imports
assert_struct!(json_response, _ {
    data: _ {
        items: [_ { id: 1, value: "test", .. }],
        total: 1,
        ..
    },
    ..
});

§Error Messages

When assertions fail, assert-struct provides detailed, actionable error messages:

§Basic Mismatch

assert_struct!(user, User {
    name: "Bob",  // This will fail
    age: 25,
});
// Error output:
// assert_struct! failed:
//
// value mismatch:
//   --> `user.name` (src/lib.rs:456)
//   actual: "Alice"
//   expected: "Bob"

§Comparison Failure

assert_struct!(stats, Stats {
    score: > 100,  // This will fail
});
// Error output:
// assert_struct! failed:
//
// comparison mismatch:
//   --> `stats.score` (src/lib.rs:469)
//   actual: 50
//   expected: > 100

§Nested Field Errors

Error messages show the exact path to the failing field, even in deeply nested structures. Method calls are also shown in the field path for clear debugging.

§Advanced Usage

§Pattern Composition

Combine multiple patterns for comprehensive assertions:

assert_struct!(complex, Complex {
    data: Some([> 0, > 1, > 2]),              // Option + Vec + comparisons
    metadata: ("info", > 40),                 // Tuple + string + comparison
    ..
});

// Verify data length separately
assert_eq!(complex.data.as_ref().unwrap().len(), 3);

§Real-World Testing Patterns

See the examples directory for comprehensive real-world examples including:

  • API response validation
  • Database record testing
  • Configuration validation
  • Event system testing

For complete specification details, see the assert_struct! macro documentation.

Macros§

assert_struct
Structural assertion macro for testing complex data structures.

Traits§

Like
A trait for pattern matching, similar to PartialEq but for flexible matching.