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: &regex::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: &regex::Regex) -> bool {
761            pattern.is_match(self)
762        }
763    }
764}