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