assert_struct/lib.rs
1//! # assert-struct: Ergonomic Structural Assertions
2//!
3//! `assert-struct` is a procedural macro that enables clean, readable assertions for complex
4//! data structures without verbose field-by-field comparisons. When assertions fail, it provides
5//! clear, actionable error messages showing exactly what went wrong, including field paths and
6//! expected vs actual values.
7//!
8//! This comprehensive guide teaches you how to use `assert-struct` effectively in your tests.
9//! After reading this documentation, you'll be familiar with all capabilities and able to
10//! leverage the full power of structural assertions.
11//!
12//! # Table of Contents
13//!
14//! - [Quick Start](#quick-start)
15//! - [Core Concepts](#core-concepts)
16//! - [Basic Assertions](#basic-assertions)
17//! - [Partial Matching](#partial-matching)
18//! - [Nested Structures](#nested-structures)
19//! - [Pattern Types](#pattern-types)
20//! - [Comparison Operators](#comparison-operators)
21//! - [Equality Operators](#equality-operators)
22//! - [Range Patterns](#range-patterns)
23//! - [Regex Patterns](#regex-patterns)
24//! - [Method Call Patterns](#method-call-patterns)
25//! - [Data Types](#data-types)
26//! - [Collections (Vec/Slice)](#collections-vecslice)
27//! - [Tuples](#tuples)
28//! - [Enums (Option/Result/Custom)](#enums-optionresultcustom)
29//! - [Smart Pointers](#smart-pointers)
30//! - [Error Messages](#error-messages)
31//! - [Advanced Usage](#advanced-usage)
32//!
33//! # Quick Start
34//!
35//! Add to your `Cargo.toml`:
36//!
37//! ```toml
38//! [dev-dependencies]
39//! assert-struct = "0.1"
40//! ```
41//!
42//! Basic example:
43//!
44//! ```rust
45//! use assert_struct::assert_struct;
46//!
47//! #[derive(Debug)]
48//! struct User {
49//! name: String,
50//! age: u32,
51//! email: String,
52//! }
53//!
54//! let user = User {
55//! name: "Alice".to_string(),
56//! age: 30,
57//! email: "alice@example.com".to_string(),
58//! };
59//!
60//! // Only check the fields you care about
61//! assert_struct!(user, User {
62//! name: "Alice",
63//! age: 30,
64//! .. // Ignore email
65//! });
66//! ```
67//!
68//! # Core Concepts
69//!
70//! ## Basic Assertions
71//!
72//! The simplest use case is asserting all fields of a struct:
73//!
74//! ```rust
75//! # use assert_struct::assert_struct;
76//! # #[derive(Debug)]
77//! # struct Point { x: i32, y: i32 }
78//! let point = Point { x: 10, y: 20 };
79//!
80//! assert_struct!(point, Point {
81//! x: 10,
82//! y: 20,
83//! });
84//! ```
85//!
86//! String fields work naturally with string literals:
87//!
88//! ```rust
89//! # use assert_struct::assert_struct;
90//! # #[derive(Debug)]
91//! # struct Message { text: String, urgent: bool }
92//! let msg = Message {
93//! text: "Hello world".to_string(),
94//! urgent: false,
95//! };
96//!
97//! assert_struct!(msg, Message {
98//! text: "Hello world", // No .to_string() needed!
99//! urgent: false,
100//! });
101//! ```
102//!
103//! ## Partial Matching
104//!
105//! Use `..` to ignore fields you don't want to check:
106//!
107//! ```rust
108//! # use assert_struct::assert_struct;
109//! # #[derive(Debug)]
110//! # struct User { id: u64, name: String, email: String, created_at: String }
111//! # let user = User {
112//! # id: 1,
113//! # name: "Alice".to_string(),
114//! # email: "alice@example.com".to_string(),
115//! # created_at: "2024-01-01".to_string(),
116//! # };
117//! // Only verify name and email, ignore id and created_at
118//! assert_struct!(user, User {
119//! name: "Alice",
120//! email: "alice@example.com",
121//! ..
122//! });
123//! ```
124//!
125//! ## Nested Structures
126//!
127//! Assert on deeply nested data without repetitive field access:
128//!
129//! ```rust
130//! # use assert_struct::assert_struct;
131//! # #[derive(Debug)]
132//! # struct Order { customer: Customer, total: f64 }
133//! # #[derive(Debug)]
134//! # struct Customer { name: String, address: Address }
135//! # #[derive(Debug)]
136//! # struct Address { city: String, country: String }
137//! # let order = Order {
138//! # customer: Customer {
139//! # name: "Bob".to_string(),
140//! # address: Address { city: "Paris".to_string(), country: "France".to_string() }
141//! # },
142//! # total: 99.99
143//! # };
144//! assert_struct!(order, Order {
145//! customer: Customer {
146//! name: "Bob",
147//! address: Address {
148//! city: "Paris",
149//! country: "France",
150//! },
151//! },
152//! total: 99.99,
153//! });
154//!
155//! // Or with partial matching
156//! assert_struct!(order, Order {
157//! customer: Customer {
158//! name: "Bob",
159//! address: Address { city: "Paris", .. },
160//! ..
161//! },
162//! ..
163//! });
164//!
165//! // Direct nested field access (no need to nest structs)
166//! assert_struct!(order, Order {
167//! customer.name: "Bob",
168//! customer.address.city: "Paris",
169//! customer.address.country: "France",
170//! total: > 50.0,
171//! ..
172//! });
173//! ```
174//!
175//! # Pattern Types
176//!
177//! ## Comparison Operators
178//!
179//! Use comparison operators for numeric assertions:
180//!
181//! ```rust
182//! # use assert_struct::assert_struct;
183//! # #[derive(Debug)]
184//! # struct Metrics { cpu: f64, memory: u64, requests: u32 }
185//! # let metrics = Metrics { cpu: 75.5, memory: 1024, requests: 150 };
186//! assert_struct!(metrics, Metrics {
187//! cpu: < 80.0, // Less than 80%
188//! memory: <= 2048, // At most 2GB
189//! requests: > 100, // More than 100
190//! });
191//! ```
192//!
193//! All comparison operators work: `<`, `<=`, `>`, `>=`
194//!
195//! ## Equality Operators
196//!
197//! Use explicit equality for clarity:
198//!
199//! ```rust
200//! # use assert_struct::assert_struct;
201//! # #[derive(Debug)]
202//! # struct Status { code: i32, active: bool }
203//! # let status = Status { code: 200, active: true };
204//! assert_struct!(status, Status {
205//! code: == 200, // Explicit equality
206//! active: != false, // Not equal to false
207//! });
208//! ```
209//!
210//! ## Range Patterns
211//!
212//! Use ranges for boundary checks:
213//!
214//! ```rust
215//! # use assert_struct::assert_struct;
216//! # #[derive(Debug)]
217//! # struct Person { age: u32, score: f64 }
218//! # let person = Person { age: 25, score: 87.5 };
219//! assert_struct!(person, Person {
220//! age: 18..=65, // Working age range
221//! score: 0.0..100.0, // Valid score range
222//! });
223//! ```
224//!
225//! ## Regex Patterns
226//!
227//! Match string patterns with regular expressions (requires `regex` feature, enabled by default):
228//!
229//! ```rust
230//! # #[cfg(feature = "regex")]
231//! # {
232//! # use assert_struct::assert_struct;
233//! # #[derive(Debug)]
234//! # struct Account { username: String, email: String }
235//! # let account = Account {
236//! # username: "alice_doe".to_string(),
237//! # email: "alice@company.com".to_string(),
238//! # };
239//! assert_struct!(account, Account {
240//! username: =~ r"^[a-z_]+$", // Lowercase and underscores
241//! email: =~ r"@company\.com$", // Company email domain
242//! });
243//! # }
244//! ```
245//!
246//! ## Method Call Patterns
247//!
248//! Call methods on fields and assert on their results:
249//!
250//! ```rust
251//! # use assert_struct::assert_struct;
252//! # use std::collections::HashMap;
253//! # #[derive(Debug)]
254//! # struct Data {
255//! # content: String,
256//! # items: Vec<i32>,
257//! # metadata: Option<String>,
258//! # cache: HashMap<String, i32>,
259//! # }
260//! # let mut map = HashMap::new();
261//! # map.insert("key1".to_string(), 42);
262//! # let data = Data {
263//! # content: "hello world".to_string(),
264//! # items: vec![1, 2, 3, 4, 5],
265//! # metadata: Some("cached".to_string()),
266//! # cache: map,
267//! # };
268//! assert_struct!(data, Data {
269//! content.len(): 11, // String length
270//! items.len(): >= 5, // Vector size check
271//! metadata.is_some(): true, // Option state
272//! cache.contains_key("key1"): true, // HashMap lookup
273//! ..
274//! });
275//! ```
276//!
277//! Method calls work with arguments too:
278//!
279//! ```rust
280//! # use assert_struct::assert_struct;
281//! # #[derive(Debug)]
282//! # struct Text { content: String, other: String }
283//! # let text = Text { content: "hello world".to_string(), other: "test".to_string() };
284//! assert_struct!(text, Text {
285//! content.starts_with("hello"): true,
286//! ..
287//! });
288//!
289//! assert_struct!(text, Text {
290//! content.contains("world"): true,
291//! ..
292//! });
293//! ```
294//!
295//! # Data Types
296//!
297//! ## Collections (Vec/Slice)
298//!
299//! Element-wise pattern matching for vectors:
300//!
301//! ```rust
302//! # use assert_struct::assert_struct;
303//! # #[derive(Debug)]
304//! # struct Data { values: Vec<i32>, names: Vec<String> }
305//! # let data = Data {
306//! # values: vec![5, 15, 25],
307//! # names: vec!["alice".to_string(), "bob".to_string()],
308//! # };
309//! // Exact matching
310//! assert_struct!(data, Data {
311//! values: [5, 15, 25],
312//! names: ["alice", "bob"], // String literals work in slices too!
313//! });
314//!
315//! // Pattern matching for each element
316//! assert_struct!(data, Data {
317//! values: [> 0, < 20, >= 25], // Different pattern per element
318//! names: ["alice", "bob"],
319//! });
320//! ```
321//!
322//! Partial slice matching:
323//!
324//! ```rust
325//! # use assert_struct::assert_struct;
326//! # #[derive(Debug)]
327//! # struct Data { items: Vec<i32> }
328//! # let data = Data { items: vec![1, 2, 3, 4, 5] };
329//! assert_struct!(data, Data {
330//! items: [1, 2, ..], // First two elements, ignore rest
331//! });
332//!
333//! assert_struct!(data, Data {
334//! items: [.., 4, 5], // Last two elements
335//! });
336//!
337//! assert_struct!(data, Data {
338//! items: [1, .., 5], // First and last elements
339//! });
340//! ```
341//!
342//! ## Tuples
343//!
344//! Full support for multi-field tuples:
345//!
346//! ```rust
347//! # use assert_struct::assert_struct;
348//! # #[derive(Debug)]
349//! # struct Data { point: (i32, i32), metadata: (String, u32, bool) }
350//! # let data = Data {
351//! # point: (15, 25),
352//! # metadata: ("info".to_string(), 100, true),
353//! # };
354//! // Basic tuple matching
355//! assert_struct!(data, Data {
356//! point: (15, 25),
357//! metadata: ("info", 100, true), // String literals work in tuples!
358//! });
359//!
360//! // Advanced patterns
361//! assert_struct!(data, Data {
362//! point: (> 10, < 30), // Comparison operators
363//! metadata: ("info", >= 50, true),
364//! });
365//! ```
366//!
367//! Tuple method calls:
368//!
369//! ```rust
370//! # use assert_struct::assert_struct;
371//! # #[derive(Debug)]
372//! # struct Data { coords: (String, Vec<i32>) }
373//! # let data = Data {
374//! # coords: ("location".to_string(), vec![1, 2, 3]),
375//! # };
376//! assert_struct!(data, Data {
377//! coords: (0.len(): 8, 1.len(): 3), // Method calls on tuple elements
378//! });
379//! ```
380//!
381//! ## Enums (Option/Result/Custom)
382//!
383//! ### Option Types
384//!
385//! ```rust
386//! # use assert_struct::assert_struct;
387//! # #[derive(Debug)]
388//! # struct User { name: Option<String>, age: Option<u32> }
389//! # let user = User { name: Some("Alice".to_string()), age: Some(30) };
390//! assert_struct!(user, User {
391//! name: Some("Alice"),
392//! age: Some(30),
393//! });
394//!
395//! // Advanced patterns inside Option
396//! assert_struct!(user, User {
397//! name: Some("Alice"),
398//! age: Some(>= 18), // Adult check inside Some
399//! });
400//! ```
401//!
402//! ### Result Types
403//!
404//! ```rust
405//! # use assert_struct::assert_struct;
406//! # #[derive(Debug)]
407//! # struct Response { result: Result<String, String> }
408//! # let response = Response { result: Ok("success".to_string()) };
409//! assert_struct!(response, Response {
410//! result: Ok("success"),
411//! });
412//!
413//! // Pattern matching inside Result
414//! # let response = Response { result: Ok("user123".to_string()) };
415//! assert_struct!(response, Response {
416//! result: Ok(=~ r"^user\d+$"), // Regex inside Ok
417//! });
418//! ```
419//!
420//! ### Custom Enums
421//!
422//! ```rust
423//! # use assert_struct::assert_struct;
424//! # #[derive(Debug, PartialEq)]
425//! # enum Status { Active, Pending { since: String } }
426//! # #[derive(Debug)]
427//! # struct Account { status: Status }
428//! # let account = Account { status: Status::Pending { since: "2024-01-01".to_string() } };
429//! // Unit variants
430//! let active_account = Account { status: Status::Active };
431//! assert_struct!(active_account, Account {
432//! status: Status::Active,
433//! });
434//!
435//! // Struct variants with partial matching
436//! assert_struct!(account, Account {
437//! status: Status::Pending { since: "2024-01-01" },
438//! });
439//! ```
440//!
441//! ## Smart Pointers
442//!
443//! Dereference smart pointers directly in patterns:
444//!
445//! ```rust
446//! # use assert_struct::assert_struct;
447//! # use std::rc::Rc;
448//! # use std::sync::Arc;
449//! # #[derive(Debug)]
450//! # struct Cache {
451//! # data: Arc<String>,
452//! # count: Box<i32>,
453//! # shared: Rc<bool>,
454//! # }
455//! # let cache = Cache {
456//! # data: Arc::new("cached".to_string()),
457//! # count: Box::new(42),
458//! # shared: Rc::new(true),
459//! # };
460//! assert_struct!(cache, Cache {
461//! *data: "cached", // Dereference Arc<String>
462//! *count: > 40, // Dereference Box<i32> with comparison
463//! *shared: true, // Dereference Rc<bool>
464//! });
465//! ```
466//!
467//! Multiple dereferencing for nested pointers:
468//!
469//! ```rust
470//! # use assert_struct::assert_struct;
471//! # #[derive(Debug)]
472//! # struct Nested { value: Box<Box<i32>> }
473//! # let nested = Nested { value: Box::new(Box::new(42)) };
474//! assert_struct!(nested, Nested {
475//! **value: 42, // Double dereference
476//! });
477//! ```
478//!
479//! ## Wildcard Patterns
480//!
481//! Use wildcard patterns (`_`) to avoid importing types while still asserting on their structure:
482//!
483//! ```rust
484//! # use assert_struct::assert_struct;
485//! # mod api {
486//! # #[derive(Debug)]
487//! # pub struct Response {
488//! # pub user: User,
489//! # pub metadata: Metadata,
490//! # }
491//! # #[derive(Debug)]
492//! # pub struct User {
493//! # pub id: u32,
494//! # pub name: String,
495//! # }
496//! # #[derive(Debug)]
497//! # pub struct Metadata {
498//! # pub timestamp: u64,
499//! # pub version: String,
500//! # }
501//! # }
502//! # let response = api::Response {
503//! # user: api::User { id: 123, name: "Alice".to_string() },
504//! # metadata: api::Metadata { timestamp: 1234567890, version: "1.0".to_string() }
505//! # };
506//! // No need to import User or Metadata types!
507//! assert_struct!(response, _ {
508//! user: _ {
509//! id: 123,
510//! name: "Alice",
511//! ..
512//! },
513//! metadata: _ {
514//! version: "1.0",
515//! .. // Ignore other metadata fields
516//! },
517//! ..
518//! });
519//! ```
520//!
521//! This is particularly useful when testing API responses where you don't want to import all the nested types:
522//!
523//! ```rust
524//! # use assert_struct::assert_struct;
525//! # #[derive(Debug)]
526//! # struct JsonResponse { data: Data }
527//! # #[derive(Debug)]
528//! # struct Data { items: Vec<Item>, total: u32 }
529//! # #[derive(Debug)]
530//! # struct Item { id: u32, value: String }
531//! # let json_response = JsonResponse {
532//! # data: Data {
533//! # items: vec![Item { id: 1, value: "test".to_string() }],
534//! # total: 1
535//! # }
536//! # };
537//! // Test deeply nested structures without imports
538//! assert_struct!(json_response, _ {
539//! data: _ {
540//! items: [_ { id: 1, value: "test", .. }],
541//! total: 1,
542//! ..
543//! },
544//! ..
545//! });
546//! ```
547//!
548//! # Error Messages
549//!
550//! When assertions fail, `assert-struct` provides detailed, actionable error messages:
551//!
552//! ## Basic Mismatch
553//!
554//! ```rust,should_panic
555//! # use assert_struct::assert_struct;
556//! # #[derive(Debug)]
557//! # struct User { name: String, age: u32 }
558//! # let user = User { name: "Alice".to_string(), age: 25 };
559//! assert_struct!(user, User {
560//! name: "Bob", // This will fail
561//! age: 25,
562//! });
563//! // Error output:
564//! // assert_struct! failed:
565//! //
566//! // value mismatch:
567//! // --> `user.name` (src/lib.rs:456)
568//! // actual: "Alice"
569//! // expected: "Bob"
570//! ```
571//!
572//! ## Comparison Failure
573//!
574//! ```rust,should_panic
575//! # use assert_struct::assert_struct;
576//! # #[derive(Debug)]
577//! # struct Stats { score: u32 }
578//! # let stats = Stats { score: 50 };
579//! assert_struct!(stats, Stats {
580//! score: > 100, // This will fail
581//! });
582//! // Error output:
583//! // assert_struct! failed:
584//! //
585//! // comparison mismatch:
586//! // --> `stats.score` (src/lib.rs:469)
587//! // actual: 50
588//! // expected: > 100
589//! ```
590//!
591//! ## Nested Field Errors
592//!
593//! Error messages show the exact path to the failing field, even in deeply nested structures.
594//! Method calls are also shown in the field path for clear debugging.
595//!
596//! # Advanced Usage
597//!
598//! ## Pattern Composition
599//!
600//! Combine multiple patterns for comprehensive assertions:
601//!
602//! ```rust
603//! # use assert_struct::assert_struct;
604//! # #[derive(Debug)]
605//! # struct Complex {
606//! # data: Option<Vec<i32>>,
607//! # metadata: (String, u32),
608//! # }
609//! # let complex = Complex {
610//! # data: Some(vec![1, 2, 3]),
611//! # metadata: ("info".to_string(), 42),
612//! # };
613//! assert_struct!(complex, Complex {
614//! data: Some([> 0, > 1, > 2]), // Option + Vec + comparisons
615//! metadata: ("info", > 40), // Tuple + string + comparison
616//! ..
617//! });
618//!
619//! // Verify data length separately
620//! assert_eq!(complex.data.as_ref().unwrap().len(), 3);
621//! ```
622//!
623//! ## Real-World Testing Patterns
624//!
625//! See the [examples directory](../../examples/) for comprehensive real-world examples including:
626//! - API response validation
627//! - Database record testing
628//! - Configuration validation
629//! - Event system testing
630//!
631//! For complete specification details, see the [`assert_struct!`] macro documentation.
632
633// Re-export the procedural macro
634pub use assert_struct_macros::assert_struct;
635
636// Error handling module
637#[doc(hidden)]
638pub mod error;
639
640// Hidden module for macro support functions
641#[doc(hidden)]
642pub mod __macro_support {
643 pub use crate::error::{ErrorContext, ErrorType, PatternNode, format_errors_with_root};
644
645 /// Helper function to enable type inference for closure parameters in assert_struct patterns
646 #[inline]
647 pub fn check_closure_condition<T, F>(value: T, predicate: F) -> bool
648 where
649 F: FnOnce(T) -> bool,
650 {
651 predicate(value)
652 }
653}
654
655/// A trait for pattern matching, similar to `PartialEq` but for flexible matching.
656///
657/// The `Like` trait enables custom pattern matching logic beyond simple equality.
658/// It's primarily used with the `=~` operator in `assert_struct!` macro to support
659/// regex patterns, custom matching logic, and other pattern-based comparisons.
660///
661/// # Examples
662///
663/// ## Basic String Pattern Matching
664///
665/// ```
666/// # #[cfg(feature = "regex")]
667/// # {
668/// use assert_struct::Like;
669///
670/// // Using Like trait directly
671/// let text = "hello@example.com";
672/// assert!(text.like(&r".*@example\.com"));
673/// # }
674/// ```
675///
676/// ## Custom Implementation
677///
678/// ```
679/// use assert_struct::Like;
680///
681/// struct EmailAddress(String);
682///
683/// struct DomainPattern {
684/// domain: String,
685/// }
686///
687/// impl Like<DomainPattern> for EmailAddress {
688/// fn like(&self, pattern: &DomainPattern) -> bool {
689/// self.0.ends_with(&format!("@{}", pattern.domain))
690/// }
691/// }
692///
693/// let email = EmailAddress("user@example.com".to_string());
694/// let pattern = DomainPattern { domain: "example.com".to_string() };
695/// assert!(email.like(&pattern));
696/// ```
697pub trait Like<Rhs = Self> {
698 /// Returns `true` if `self` matches the pattern `other`.
699 ///
700 /// # Examples
701 ///
702 /// ```
703 /// # #[cfg(feature = "regex")]
704 /// # {
705 /// use assert_struct::Like;
706 ///
707 /// let s = "test123";
708 /// assert!(s.like(&r"\w+\d+"));
709 /// # }
710 /// ```
711 fn like(&self, other: &Rhs) -> bool;
712}
713
714// String/&str implementations for regex pattern matching
715#[cfg(feature = "regex")]
716mod like_impls {
717 use super::Like;
718
719 /// Implementation of Like for String with &str patterns (interpreted as regex)
720 impl Like<&str> for String {
721 fn like(&self, pattern: &&str) -> bool {
722 regex::Regex::new(pattern)
723 .map(|re| re.is_match(self))
724 .unwrap_or(false)
725 }
726 }
727
728 /// Implementation of Like for String with String patterns (interpreted as regex)
729 impl Like<String> for String {
730 fn like(&self, pattern: &String) -> bool {
731 self.like(&pattern.as_str())
732 }
733 }
734
735 /// Implementation of Like for &str with &str patterns (interpreted as regex)
736 impl Like<&str> for &str {
737 fn like(&self, pattern: &&str) -> bool {
738 regex::Regex::new(pattern)
739 .map(|re| re.is_match(self))
740 .unwrap_or(false)
741 }
742 }
743
744 /// Implementation of Like for &str with String patterns (interpreted as regex)
745 impl Like<String> for &str {
746 fn like(&self, pattern: &String) -> bool {
747 self.like(&pattern.as_str())
748 }
749 }
750
751 /// Implementation of Like for String with pre-compiled Regex
752 impl Like<regex::Regex> for String {
753 fn like(&self, pattern: ®ex::Regex) -> bool {
754 pattern.is_match(self)
755 }
756 }
757
758 /// Implementation of Like for &str with pre-compiled Regex
759 impl Like<regex::Regex> for &str {
760 fn like(&self, pattern: ®ex::Regex) -> bool {
761 pattern.is_match(self)
762 }
763 }
764}