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//! - [Set Patterns](#set-patterns)
28//! - [Maps (HashMap/BTreeMap)](#maps-hashmapbtreemap)
29//! - [Tuples](#tuples)
30//! - [Enums (Option/Result/Custom)](#enums-optionresultcustom)
31//! - [Smart Pointers](#smart-pointers)
32//! - [Error Messages](#error-messages)
33//! - [Advanced Usage](#advanced-usage)
34//! - [Dynamic Value Types](#dynamic-value-types)
35//!
36//! # Quick Start
37//!
38//! Add to your `Cargo.toml`:
39//!
40//! ```toml
41//! [dev-dependencies]
42//! assert-struct = "0.2"
43//! ```
44//!
45//! Basic example:
46//!
47//! ```rust
48//! use assert_struct::assert_struct;
49//!
50//! #[derive(Debug)]
51//! struct User {
52//! name: String,
53//! age: u32,
54//! email: String,
55//! }
56//!
57//! let user = User {
58//! name: "Alice".to_string(),
59//! age: 30,
60//! email: "alice@example.com".to_string(),
61//! };
62//!
63//! // Only check the fields you care about
64//! assert_struct!(user, User {
65//! name: "Alice",
66//! age: 30,
67//! .. // Ignore email
68//! });
69//! ```
70//!
71//! # Core Concepts
72//!
73//! ## Basic Assertions
74//!
75//! The simplest use case is asserting all fields of a struct:
76//!
77//! ```rust
78//! # use assert_struct::assert_struct;
79//! # #[derive(Debug)]
80//! # struct Point { x: i32, y: i32 }
81//! let point = Point { x: 10, y: 20 };
82//!
83//! assert_struct!(point, Point {
84//! x: 10,
85//! y: 20,
86//! });
87//! ```
88//!
89//! String fields work naturally with string literals:
90//!
91//! ```rust
92//! # use assert_struct::assert_struct;
93//! # #[derive(Debug)]
94//! # struct Message { text: String, urgent: bool }
95//! let msg = Message {
96//! text: "Hello world".to_string(),
97//! urgent: false,
98//! };
99//!
100//! assert_struct!(msg, Message {
101//! text: "Hello world", // No .to_string() needed!
102//! urgent: false,
103//! });
104//! ```
105//!
106//! ## Partial Matching
107//!
108//! Use `..` to ignore fields you don't want to check:
109//!
110//! ```rust
111//! # use assert_struct::assert_struct;
112//! # #[derive(Debug)]
113//! # struct User { id: u64, name: String, email: String, created_at: String }
114//! # let user = User {
115//! # id: 1,
116//! # name: "Alice".to_string(),
117//! # email: "alice@example.com".to_string(),
118//! # created_at: "2024-01-01".to_string(),
119//! # };
120//! // Only verify name and email, ignore id and created_at
121//! assert_struct!(user, User {
122//! name: "Alice",
123//! email: "alice@example.com",
124//! ..
125//! });
126//! ```
127//!
128//! ## Nested Structures
129//!
130//! Assert on deeply nested data without repetitive field access:
131//!
132//! ```rust
133//! # use assert_struct::assert_struct;
134//! # #[derive(Debug)]
135//! # struct Order { customer: Customer, total: f64 }
136//! # #[derive(Debug)]
137//! # struct Customer { name: String, address: Address }
138//! # #[derive(Debug)]
139//! # struct Address { city: String, country: String }
140//! # let order = Order {
141//! # customer: Customer {
142//! # name: "Bob".to_string(),
143//! # address: Address { city: "Paris".to_string(), country: "France".to_string() }
144//! # },
145//! # total: 99.99
146//! # };
147//! assert_struct!(order, Order {
148//! customer: Customer {
149//! name: "Bob",
150//! address: Address {
151//! city: "Paris",
152//! country: "France",
153//! },
154//! },
155//! total: 99.99,
156//! });
157//!
158//! // Or with partial matching
159//! assert_struct!(order, Order {
160//! customer: Customer {
161//! name: "Bob",
162//! address: Address { city: "Paris", .. },
163//! ..
164//! },
165//! ..
166//! });
167//!
168//! // Direct nested field access (no need to nest structs)
169//! assert_struct!(order, Order {
170//! customer.name: "Bob",
171//! customer.address.city: "Paris",
172//! customer.address.country: "France",
173//! total: > 50.0,
174//! ..
175//! });
176//! ```
177//!
178//! # Pattern Types
179//!
180//! ## Comparison Operators
181//!
182//! Use comparison operators for numeric assertions:
183//!
184//! ```rust
185//! # use assert_struct::assert_struct;
186//! # #[derive(Debug)]
187//! # struct Metrics { cpu: f64, memory: u64, requests: u32 }
188//! # let metrics = Metrics { cpu: 75.5, memory: 1024, requests: 150 };
189//! assert_struct!(metrics, Metrics {
190//! cpu: < 80.0, // Less than 80%
191//! memory: <= 2048, // At most 2GB
192//! requests: > 100, // More than 100
193//! });
194//! ```
195//!
196//! All comparison operators work: `<`, `<=`, `>`, `>=`
197//!
198//! ## Equality Operators
199//!
200//! Use explicit equality for clarity:
201//!
202//! ```rust
203//! # use assert_struct::assert_struct;
204//! # #[derive(Debug)]
205//! # struct Status { code: i32, active: bool }
206//! # let status = Status { code: 200, active: true };
207//! assert_struct!(status, Status {
208//! code: == 200, // Explicit equality
209//! active: != false, // Not equal to false
210//! });
211//! ```
212//!
213//! ## Range Patterns
214//!
215//! Use ranges for boundary checks:
216//!
217//! ```rust
218//! # use assert_struct::assert_struct;
219//! # #[derive(Debug)]
220//! # struct Person { age: u32, score: f64 }
221//! # let person = Person { age: 25, score: 87.5 };
222//! assert_struct!(person, Person {
223//! age: 18..=65, // Working age range
224//! score: 0.0..100.0, // Valid score range
225//! });
226//! ```
227//!
228//! ## Regex Patterns
229//!
230//! Match string patterns with regular expressions (requires `regex` feature, enabled by default):
231//!
232//! ```rust
233//! # #[cfg(feature = "regex")]
234//! # {
235//! # use assert_struct::assert_struct;
236//! # #[derive(Debug)]
237//! # struct Account { username: String, email: String }
238//! # let account = Account {
239//! # username: "alice_doe".to_string(),
240//! # email: "alice@company.com".to_string(),
241//! # };
242//! assert_struct!(account, Account {
243//! username: =~ r"^[a-z_]+$", // Lowercase and underscores
244//! email: =~ r"@company\.com$", // Company email domain
245//! });
246//! # }
247//! ```
248//!
249//! ## Method Call Patterns
250//!
251//! Call methods on fields and assert on their results:
252//!
253//! ```rust
254//! # use assert_struct::assert_struct;
255//! # use std::collections::HashMap;
256//! # #[derive(Debug)]
257//! # struct Data {
258//! # content: String,
259//! # items: Vec<i32>,
260//! # metadata: Option<String>,
261//! # cache: HashMap<String, i32>,
262//! # }
263//! # let mut map = HashMap::new();
264//! # map.insert("key1".to_string(), 42);
265//! # let data = Data {
266//! # content: "hello world".to_string(),
267//! # items: vec![1, 2, 3, 4, 5],
268//! # metadata: Some("cached".to_string()),
269//! # cache: map,
270//! # };
271//! assert_struct!(data, Data {
272//! content.len(): 11, // String length
273//! items.len(): >= 5, // Vector size check
274//! metadata.is_some(): true, // Option state
275//! cache.contains_key("key1"): true, // HashMap lookup
276//! ..
277//! });
278//! ```
279//!
280//! Method calls work with arguments too:
281//!
282//! ```rust
283//! # use assert_struct::assert_struct;
284//! # #[derive(Debug)]
285//! # struct Text { content: String, other: String }
286//! # let text = Text { content: "hello world".to_string(), other: "test".to_string() };
287//! assert_struct!(text, Text {
288//! content.starts_with("hello"): true,
289//! ..
290//! });
291//!
292//! assert_struct!(text, Text {
293//! content.contains("world"): true,
294//! ..
295//! });
296//! ```
297//!
298//! # Data Types
299//!
300//! ## Collections (Vec/Slice)
301//!
302//! Element-wise pattern matching for vectors:
303//!
304//! ```rust
305//! # use assert_struct::assert_struct;
306//! # #[derive(Debug)]
307//! # struct Data { values: Vec<i32>, names: Vec<String> }
308//! # let data = Data {
309//! # values: vec![5, 15, 25],
310//! # names: vec!["alice".to_string(), "bob".to_string()],
311//! # };
312//! // Exact matching
313//! assert_struct!(data, Data {
314//! values: [5, 15, 25],
315//! names: ["alice", "bob"], // String literals work in slices too!
316//! });
317//!
318//! // Pattern matching for each element
319//! assert_struct!(data, Data {
320//! values: [> 0, < 20, >= 25], // Different pattern per element
321//! names: ["alice", "bob"],
322//! });
323//! ```
324//!
325//! Partial slice matching:
326//!
327//! ```rust
328//! # use assert_struct::assert_struct;
329//! # #[derive(Debug)]
330//! # struct Data { items: Vec<i32> }
331//! # let data = Data { items: vec![1, 2, 3, 4, 5] };
332//! assert_struct!(data, Data {
333//! items: [1, 2, ..], // First two elements, ignore rest
334//! });
335//!
336//! assert_struct!(data, Data {
337//! items: [.., 4, 5], // Last two elements
338//! });
339//!
340//! assert_struct!(data, Data {
341//! items: [1, .., 5], // First and last elements
342//! });
343//! ```
344//!
345//! ## Set Patterns
346//!
347//! Use `#(...)` to assert that a collection contains elements matching the given patterns,
348//! **in any order**. Each pattern must match a distinct element; unlike slice patterns `[...]`,
349//! position does not matter.
350//!
351//! ```rust
352//! # use assert_struct::assert_struct;
353//! # #[derive(Debug)]
354//! # struct Data { items: Vec<i32>, tags: Vec<String> }
355//! # let data = Data {
356//! # items: vec![3, 1, 2],
357//! # tags: vec!["rust".to_string(), "async".to_string()],
358//! # };
359//! // Exact match - every element must be matched, in any order
360//! assert_struct!(data, Data {
361//! items: #(1, 2, 3),
362//! tags: #("rust", "async"), // String literals work too
363//! });
364//!
365//! // Partial match - use .. to allow extra unmatched elements
366//! assert_struct!(data, Data {
367//! items: #(> 0, ..), // At least one element matching > 0
368//! tags: #("rust", ..), // Contains "rust", possibly more
369//! });
370//! ```
371//!
372//! Struct patterns work inside `#(...)` too, which is useful when asserting that certain
373//! items are present in a collection regardless of their order:
374//!
375//! ```rust
376//! # use assert_struct::assert_struct;
377//! # #[derive(Debug)]
378//! # struct Event { kind: String, value: i32 }
379//! # let events = vec![
380//! # Event { kind: "click".to_string(), value: 10 },
381//! # Event { kind: "hover".to_string(), value: 20 },
382//! # Event { kind: "scroll".to_string(), value: 30 },
383//! # ];
384//! // Assert that click and hover events are present, ignoring order and other events
385//! assert_struct!(events, #(
386//! _ { kind: "click", value: > 0 },
387//! _ { kind: "hover" },
388//! ..
389//! ));
390//! ```
391//!
392//! Other pattern types also work inside `#(...)`:
393//!
394//! ```rust
395//! # use assert_struct::assert_struct;
396//! # #[derive(Debug)]
397//! # struct Report { scores: Vec<i32>, results: Vec<Option<i32>> }
398//! # let report = Report {
399//! # scores: vec![95, 42, 77],
400//! # results: vec![Some(5), None, Some(10)],
401//! # };
402//! assert_struct!(report, Report {
403//! scores: #(> 90, < 50, 18..=100), // Comparisons and ranges
404//! results: #(None, Some(> 0), ..), // Enum patterns
405//! });
406//! ```
407//!
408//! Empty and wildcard set patterns:
409//!
410//! ```rust
411//! # use assert_struct::assert_struct;
412//! # #[derive(Debug)]
413//! # struct Data { empty: Vec<i32>, any: Vec<i32> }
414//! # let data = Data { empty: vec![], any: vec![1, 2, 3] };
415//! assert_struct!(data, Data {
416//! empty: #(), // Exactly empty collection
417//! any: #(..), // Any collection (wildcard - any contents or length)
418//! });
419//! ```
420//!
421//!
422//! ## Maps (HashMap/BTreeMap)
423//!
424//! Pattern matching for map-like structures using duck typing (works with any type that has `len()` and `get()` methods):
425//!
426//! ```rust
427//! # use assert_struct::assert_struct;
428//! # use std::collections::HashMap;
429//! # #[derive(Debug)]
430//! # struct Config {
431//! # settings: HashMap<String, String>,
432//! # flags: HashMap<String, bool>,
433//! # }
434//! # let mut settings = HashMap::new();
435//! # settings.insert("theme".to_string(), "dark".to_string());
436//! # settings.insert("language".to_string(), "en".to_string());
437//! # let mut flags = HashMap::new();
438//! # flags.insert("debug".to_string(), true);
439//! # flags.insert("verbose".to_string(), false);
440//! # let config = Config { settings, flags };
441//! // Exact matching (checks map length)
442//! assert_struct!(config, Config {
443//! settings: #{ "theme": "dark", "language": "en" },
444//! flags: #{ "debug": true, "verbose": false },
445//! });
446//!
447//! // Partial matching (ignores map length)
448//! assert_struct!(config, Config {
449//! settings: #{ "theme": "dark", .. }, // Only check theme
450//! flags: #{ "debug": true, .. }, // Only check debug flag
451//! });
452//! ```
453//!
454//! Advanced map patterns with comparisons and nested patterns:
455//!
456//! ```rust
457//! # use assert_struct::assert_struct;
458//! # use std::collections::{HashMap, BTreeMap};
459//! # #[derive(Debug)]
460//! # struct Analytics {
461//! # metrics: HashMap<String, i32>,
462//! # metadata: BTreeMap<String, String>,
463//! # }
464//! # let mut metrics = HashMap::new();
465//! # metrics.insert("views".to_string(), 1000);
466//! # metrics.insert("clicks".to_string(), 50);
467//! # metrics.insert("conversions".to_string(), 5);
468//! # let mut metadata = BTreeMap::new();
469//! # metadata.insert("version".to_string(), "v1.2.3".to_string());
470//! # metadata.insert("environment".to_string(), "production".to_string());
471//! # let analytics = Analytics { metrics, metadata };
472//! // Pattern matching with comparison operators
473//! assert_struct!(analytics, Analytics {
474//! metrics: #{
475//! "views": > 500, // More than 500 views
476//! "clicks": >= 10, // At least 10 clicks
477//! "conversions": > 0, // Some conversions
478//! ..
479//! },
480//! metadata: #{
481//! "version": =~ r"^v\d+\.\d+\.\d+$", // Semantic version pattern
482//! "environment": != "development", // Not in development
483//! ..
484//! },
485//! });
486//! ```
487//!
488//! Empty and wildcard map matching:
489//!
490//! ```rust
491//! # use assert_struct::assert_struct;
492//! # use std::collections::HashMap;
493//! # #[derive(Debug)]
494//! # struct Data {
495//! # cache: HashMap<String, i32>,
496//! # config: HashMap<String, String>,
497//! # }
498//! # let mut config = HashMap::new();
499//! # config.insert("key".to_string(), "value".to_string());
500//! # let data = Data { cache: HashMap::new(), config };
501//! assert_struct!(data, Data {
502//! cache: #{}, // Exactly empty map (len() == 0)
503//! config: #{ .. }, // Any map content (wildcard)
504//! });
505//! ```
506//!
507//! ## Tuples
508//!
509//! Full support for multi-field tuples:
510//!
511//! ```rust
512//! # use assert_struct::assert_struct;
513//! # #[derive(Debug)]
514//! # struct Data { point: (i32, i32), metadata: (String, u32, bool) }
515//! # let data = Data {
516//! # point: (15, 25),
517//! # metadata: ("info".to_string(), 100, true),
518//! # };
519//! // Basic tuple matching
520//! assert_struct!(data, Data {
521//! point: (15, 25),
522//! metadata: ("info", 100, true), // String literals work in tuples!
523//! });
524//!
525//! // Advanced patterns
526//! assert_struct!(data, Data {
527//! point: (> 10, < 30), // Comparison operators
528//! metadata: ("info", >= 50, true),
529//! });
530//! ```
531//!
532//! Tuple method calls:
533//!
534//! ```rust
535//! # use assert_struct::assert_struct;
536//! # #[derive(Debug)]
537//! # struct Data { coords: (String, Vec<i32>) }
538//! # let data = Data {
539//! # coords: ("location".to_string(), vec![1, 2, 3]),
540//! # };
541//! assert_struct!(data, Data {
542//! coords: (0.len(): 8, 1.len(): 3), // Method calls on tuple elements
543//! });
544//! ```
545//!
546//! ## Enums (Option/Result/Custom)
547//!
548//! ### Option Types
549//!
550//! ```rust
551//! # use assert_struct::assert_struct;
552//! # #[derive(Debug)]
553//! # struct User { name: Option<String>, age: Option<u32> }
554//! # let user = User { name: Some("Alice".to_string()), age: Some(30) };
555//! assert_struct!(user, User {
556//! name: Some("Alice"),
557//! age: Some(30),
558//! });
559//!
560//! // Advanced patterns inside Option
561//! assert_struct!(user, User {
562//! name: Some("Alice"),
563//! age: Some(>= 18), // Adult check inside Some
564//! });
565//! ```
566//!
567//! ### Result Types
568//!
569//! ```rust
570//! # use assert_struct::assert_struct;
571//! # #[derive(Debug)]
572//! # struct Response { result: Result<String, String> }
573//! # let response = Response { result: Ok("success".to_string()) };
574//! assert_struct!(response, Response {
575//! result: Ok("success"),
576//! });
577//!
578//! // Pattern matching inside Result
579//! # let response = Response { result: Ok("user123".to_string()) };
580//! assert_struct!(response, Response {
581//! result: Ok(=~ r"^user\d+$"), // Regex inside Ok
582//! });
583//! ```
584//!
585//! ### Custom Enums
586//!
587//! ```rust
588//! # use assert_struct::assert_struct;
589//! # #[derive(Debug, PartialEq)]
590//! # enum Status { Active, Pending { since: String } }
591//! # #[derive(Debug)]
592//! # struct Account { status: Status }
593//! # let account = Account { status: Status::Pending { since: "2024-01-01".to_string() } };
594//! // Unit variants
595//! let active_account = Account { status: Status::Active };
596//! assert_struct!(active_account, Account {
597//! status: Status::Active,
598//! });
599//!
600//! // Struct variants with partial matching
601//! assert_struct!(account, Account {
602//! status: Status::Pending { since: "2024-01-01" },
603//! });
604//! ```
605//!
606//! ## Smart Pointers
607//!
608//! Dereference smart pointers directly in patterns:
609//!
610//! ```rust
611//! # use assert_struct::assert_struct;
612//! # use std::rc::Rc;
613//! # use std::sync::Arc;
614//! # #[derive(Debug)]
615//! # struct Cache {
616//! # data: Arc<String>,
617//! # count: Box<i32>,
618//! # shared: Rc<bool>,
619//! # }
620//! # let cache = Cache {
621//! # data: Arc::new("cached".to_string()),
622//! # count: Box::new(42),
623//! # shared: Rc::new(true),
624//! # };
625//! assert_struct!(cache, Cache {
626//! *data: "cached", // Dereference Arc<String>
627//! *count: > 40, // Dereference Box<i32> with comparison
628//! *shared: true, // Dereference Rc<bool>
629//! });
630//! ```
631//!
632//! Multiple dereferencing for nested pointers:
633//!
634//! ```rust
635//! # use assert_struct::assert_struct;
636//! # #[derive(Debug)]
637//! # struct Nested { value: Box<Box<i32>> }
638//! # let nested = Nested { value: Box::new(Box::new(42)) };
639//! assert_struct!(nested, Nested {
640//! **value: 42, // Double dereference
641//! });
642//! ```
643//!
644//! ## Wildcard Patterns
645//!
646//! Use wildcard patterns (`_`) to avoid importing types while still asserting on their structure:
647//!
648//! ```rust
649//! # use assert_struct::assert_struct;
650//! # mod api {
651//! # #[derive(Debug)]
652//! # pub struct Response {
653//! # pub user: User,
654//! # pub metadata: Metadata,
655//! # }
656//! # #[derive(Debug)]
657//! # pub struct User {
658//! # pub id: u32,
659//! # pub name: String,
660//! # }
661//! # #[derive(Debug)]
662//! # pub struct Metadata {
663//! # pub timestamp: u64,
664//! # pub version: String,
665//! # }
666//! # }
667//! # let response = api::Response {
668//! # user: api::User { id: 123, name: "Alice".to_string() },
669//! # metadata: api::Metadata { timestamp: 1234567890, version: "1.0".to_string() }
670//! # };
671//! // No need to import User or Metadata types!
672//! // Wildcard patterns always do partial matching, so .. is never required.
673//! assert_struct!(response, _ {
674//! user: _ {
675//! id: 123,
676//! name: "Alice",
677//! },
678//! metadata: _ {
679//! version: "1.0",
680//! },
681//! });
682//! ```
683//!
684//! This is particularly useful when testing API responses where you don't want to import all the nested types:
685//!
686//! ```rust
687//! # use assert_struct::assert_struct;
688//! # #[derive(Debug)]
689//! # struct JsonResponse { data: Data }
690//! # #[derive(Debug)]
691//! # struct Data { items: Vec<Item>, total: u32 }
692//! # #[derive(Debug)]
693//! # struct Item { id: u32, value: String }
694//! # let json_response = JsonResponse {
695//! # data: Data {
696//! # items: vec![Item { id: 1, value: "test".to_string() }],
697//! # total: 1
698//! # }
699//! # };
700//! // Test deeply nested structures without imports
701//! assert_struct!(json_response, _ {
702//! data: _ {
703//! items: [_ { id: 1, value: "test" }],
704//! total: 1,
705//! },
706//! });
707//! ```
708//!
709//! # Error Messages
710//!
711//! When assertions fail, `assert-struct` provides detailed, actionable error messages:
712//!
713//! ## Basic Mismatch
714//!
715//! ```rust,should_panic
716//! # use assert_struct::assert_struct;
717//! # #[derive(Debug)]
718//! # struct User { name: String, age: u32 }
719//! # let user = User { name: "Alice".to_string(), age: 25 };
720//! assert_struct!(user, User {
721//! name: "Bob", // This will fail
722//! age: 25,
723//! });
724//! // Error output:
725//! // assert_struct! failed:
726//! //
727//! // value mismatch:
728//! // --> `user.name` (src/lib.rs:456)
729//! // actual: "Alice"
730//! // expected: "Bob"
731//! ```
732//!
733//! ## Comparison Failure
734//!
735//! ```rust,should_panic
736//! # use assert_struct::assert_struct;
737//! # #[derive(Debug)]
738//! # struct Stats { score: u32 }
739//! # let stats = Stats { score: 50 };
740//! assert_struct!(stats, Stats {
741//! score: > 100, // This will fail
742//! });
743//! // Error output:
744//! // assert_struct! failed:
745//! //
746//! // comparison mismatch:
747//! // --> `stats.score` (src/lib.rs:469)
748//! // actual: 50
749//! // expected: > 100
750//! ```
751//!
752//! ## Nested Field Errors
753//!
754//! Error messages show the exact path to the failing field, even in deeply nested structures.
755//! Method calls are also shown in the field path for clear debugging.
756//!
757//! # Advanced Usage
758//!
759//! ## Mixing Structural and Operator Patterns
760//!
761//! Structural patterns and operator patterns are complementary — within a single
762//! `assert_struct!` call, use whichever is most concise for each field. The goal is
763//! the shortest assertion that expresses what you actually care about.
764//!
765//! Structural patterns (`SomeType { field: value, .. }`) are great for navigating
766//! nested types. But for a specific field, writing out the full structural equivalent
767//! can be far more verbose than a simple `==` or `=~` check — especially when the
768//! field's type implements `PartialEq` or `PartialOrd` with a simpler type.
769//!
770//! For example, consider a `Value` enum (similar to `serde_json::Value`) that can
771//! hold different primitive types and implements `PartialEq` with them:
772//!
773//! ```rust
774//! # use assert_struct::assert_struct;
775//! # #[derive(Debug, PartialEq)]
776//! # enum Value { Bool(bool), Int(i64), Text(String) }
777//! # impl PartialEq<bool> for Value {
778//! # fn eq(&self, other: &bool) -> bool {
779//! # match self { Value::Bool(b) => b == other, _ => false }
780//! # }
781//! # }
782//! # impl PartialEq<i64> for Value {
783//! # fn eq(&self, other: &i64) -> bool {
784//! # match self { Value::Int(n) => n == other, _ => false }
785//! # }
786//! # }
787//! # impl PartialEq<&str> for Value {
788//! # fn eq(&self, other: &&str) -> bool {
789//! # match self { Value::Text(s) => s == *other, _ => false }
790//! # }
791//! # }
792//! # impl PartialOrd<i64> for Value {
793//! # fn partial_cmp(&self, other: &i64) -> Option<std::cmp::Ordering> {
794//! # match self { Value::Int(n) => n.partial_cmp(other), _ => None }
795//! # }
796//! # }
797//! # #[derive(Debug)]
798//! # struct Filter { column: usize, value: Value }
799//! # #[derive(Debug)]
800//! # struct Query { table: String, filter: Option<Filter> }
801//! # let query = Query {
802//! # table: "users".to_string(),
803//! # filter: Some(Filter { column: 2, value: Value::Text("alice".to_string()) }),
804//! # };
805//! // Structural matching navigates the outer types; operator pattern handles Value
806//! assert_struct!(query, Query {
807//! table: "users",
808//! filter: Some(_ {
809//! column: 2,
810//! value: == "alice", // much shorter than Value::Text("alice".to_string())
811//! }),
812//! });
813//! ```
814//!
815//! The `== "alice"` works because `Value: PartialEq<&str>`. The same applies to
816//! comparison operators — if `Value: PartialOrd<i64>`, you can write `value: > 0i64`
817//! instead of matching the variant.
818//!
819//! The general rule: if writing the structural pattern for a field requires naming
820//! internal types or constructing wrapper values, check whether an operator pattern
821//! expresses the same intent more directly.
822//!
823//! ## Pattern Composition
824//!
825//! Combine multiple patterns for comprehensive assertions:
826//!
827//! ```rust
828//! # use assert_struct::assert_struct;
829//! # #[derive(Debug)]
830//! # struct Complex {
831//! # data: Option<Vec<i32>>,
832//! # metadata: (String, u32),
833//! # }
834//! # let complex = Complex {
835//! # data: Some(vec![1, 2, 3]),
836//! # metadata: ("info".to_string(), 42),
837//! # };
838//! assert_struct!(complex, Complex {
839//! data: Some([> 0, > 1, > 2]), // Option + Vec + comparisons
840//! metadata: ("info", > 40), // Tuple + string + comparison
841//! ..
842//! });
843//!
844//! // Verify data length separately
845//! assert_eq!(complex.data.as_ref().unwrap().len(), 3);
846//! ```
847//!
848//! ## Real-World Testing Patterns
849//!
850//! See the [examples directory](../../examples/) for comprehensive real-world examples including:
851//! - API response validation
852//! - Database record testing
853//! - Configuration validation
854//! - Event system testing
855//!
856//! For complete specification details, see the [`assert_struct!`] macro documentation.
857
858// Re-export the procedural macro
859pub use assert_struct_macros::assert_struct;
860
861// Error handling module
862#[doc(hidden)]
863pub mod error;
864
865// Hidden module for macro support functions
866#[doc(hidden)]
867pub mod __macro_support {
868 pub use crate::error::{ComparisonOp, ErrorReport, NodeKind, PatternNode, PlainOutputGuard};
869
870 // Re-export regex types for macro expansion when regex feature is enabled
871 #[cfg(feature = "regex")]
872 pub use regex::Regex;
873
874 /// Helper function to enable type inference for closure parameters in assert_struct patterns
875 #[inline]
876 pub fn check_closure_condition<T, F>(value: T, predicate: F) -> bool
877 where
878 F: FnOnce(T) -> bool,
879 {
880 predicate(value)
881 }
882
883 /// Runtime helper for the set pattern `#(...)`.
884 ///
885 /// Checks that `n_elements` satisfies the length constraint, then uses backtracking
886 /// to find a 1-to-1 assignment of patterns to elements. Each predicate returns `true`
887 /// if the element at the given index matches its pattern.
888 ///
889 /// On failure, pushes exactly one error to `report`.
890 pub fn set_match(
891 n_elements: usize,
892 rest: bool,
893 predicates: &[&dyn Fn(usize) -> bool],
894 report: &mut ErrorReport,
895 node: &'static PatternNode,
896 ) {
897 let n_patterns = predicates.len();
898
899 // Length check
900 let length_ok = if rest {
901 n_elements >= n_patterns
902 } else {
903 n_elements == n_patterns
904 };
905
906 if !length_ok {
907 let expected_str = if rest {
908 format!("at least {} element(s)", n_patterns)
909 } else {
910 format!("{} element(s)", n_patterns)
911 };
912 report.push(
913 node,
914 format!("{} element(s)", n_elements),
915 Some(expected_str),
916 );
917 return;
918 }
919
920 // Backtracking assignment
921 let mut matched = vec![false; n_elements];
922 if !set_backtrack(predicates, &mut matched, 0) {
923 report.push(node, format!("{} element(s)", n_elements), None);
924 }
925 }
926
927 fn set_backtrack(
928 predicates: &[&dyn Fn(usize) -> bool],
929 matched: &mut [bool],
930 pattern_idx: usize,
931 ) -> bool {
932 if pattern_idx == predicates.len() {
933 return true;
934 }
935 for i in 0..matched.len() {
936 if !matched[i] && predicates[pattern_idx](i) {
937 matched[i] = true;
938 if set_backtrack(predicates, matched, pattern_idx + 1) {
939 return true;
940 }
941 matched[i] = false;
942 }
943 }
944 false
945 }
946}
947
948/// A trait for pattern matching, similar to `PartialEq` but for flexible matching.
949///
950/// The `Like` trait enables custom pattern matching logic beyond simple equality.
951/// It's primarily used with the `=~` operator in `assert_struct!` macro to support
952/// regex patterns, custom matching logic, and other pattern-based comparisons.
953///
954/// # Examples
955///
956/// ## Basic String Pattern Matching
957///
958/// ```
959/// # #[cfg(feature = "regex")]
960/// # {
961/// use assert_struct::Like;
962///
963/// // Using Like trait directly
964/// let text = "hello@example.com";
965/// assert!(text.like(&r".*@example\.com"));
966/// # }
967/// ```
968///
969/// ## Custom Implementation
970///
971/// ```
972/// use assert_struct::Like;
973///
974/// struct EmailAddress(String);
975///
976/// struct DomainPattern {
977/// domain: String,
978/// }
979///
980/// impl Like<DomainPattern> for EmailAddress {
981/// fn like(&self, pattern: &DomainPattern) -> bool {
982/// self.0.ends_with(&format!("@{}", pattern.domain))
983/// }
984/// }
985///
986/// let email = EmailAddress("user@example.com".to_string());
987/// let pattern = DomainPattern { domain: "example.com".to_string() };
988/// assert!(email.like(&pattern));
989/// ```
990pub trait Like<Rhs = Self> {
991 /// Returns `true` if `self` matches the pattern `other`.
992 ///
993 /// # Examples
994 ///
995 /// ```
996 /// # #[cfg(feature = "regex")]
997 /// # {
998 /// use assert_struct::Like;
999 ///
1000 /// let s = "test123";
1001 /// assert!(s.like(&r"\w+\d+"));
1002 /// # }
1003 /// ```
1004 fn like(&self, other: &Rhs) -> bool;
1005}
1006
1007// String/&str implementations for regex pattern matching
1008#[cfg(feature = "regex")]
1009mod like_impls {
1010 use super::Like;
1011
1012 /// Implementation of Like for String with &str patterns (interpreted as regex)
1013 impl Like<&str> for String {
1014 fn like(&self, pattern: &&str) -> bool {
1015 regex::Regex::new(pattern)
1016 .map(|re| re.is_match(self))
1017 .unwrap_or(false)
1018 }
1019 }
1020
1021 /// Implementation of Like for String with String patterns (interpreted as regex)
1022 impl Like<String> for String {
1023 fn like(&self, pattern: &String) -> bool {
1024 self.like(&pattern.as_str())
1025 }
1026 }
1027
1028 /// Implementation of Like for &str with &str patterns (interpreted as regex)
1029 impl Like<&str> for &str {
1030 fn like(&self, pattern: &&str) -> bool {
1031 regex::Regex::new(pattern)
1032 .map(|re| re.is_match(self))
1033 .unwrap_or(false)
1034 }
1035 }
1036
1037 /// Implementation of Like for &str with String patterns (interpreted as regex)
1038 impl Like<String> for &str {
1039 fn like(&self, pattern: &String) -> bool {
1040 self.like(&pattern.as_str())
1041 }
1042 }
1043
1044 /// Implementation of Like for String with pre-compiled Regex
1045 impl Like<regex::Regex> for String {
1046 fn like(&self, pattern: ®ex::Regex) -> bool {
1047 pattern.is_match(self)
1048 }
1049 }
1050
1051 /// Implementation of Like for &str with pre-compiled Regex
1052 impl Like<regex::Regex> for &str {
1053 fn like(&self, pattern: ®ex::Regex) -> bool {
1054 pattern.is_match(self)
1055 }
1056 }
1057}