gam_models/block_layout/block_count.rs
1//! Shared block-count arity guard.
2//!
3//! Every family / term entry point that consumes a `&[ParameterBlockState]`
4//! opens with the identical check: "this family expects exactly `N` blocks;
5//! reject any other count". The structure is fixed — only the family name,
6//! the expected arity, and the per-module error enum vary. This module holds
7//! the single canonical implementation so the guard is not re-typed by hand
8//! across modules.
9//!
10//! The canonical error message is owned by [`BlockCountMismatch::message`].
11//! Each module routes its own error enum through `From<BlockCountMismatch>`,
12//! so the per-module error identity (and its `Display`/variant) is preserved
13//! while the arity-check logic and the message wording live in one place.
14
15/// A block-count arity mismatch: a family that needs exactly `expected`
16/// parameter blocks was handed `got` of them.
17///
18/// This is the neutral carrier produced by [`validate_block_count`]; each
19/// module converts it into its own error type via `From<BlockCountMismatch>`.
20///
21/// The type itself has descended to `gam-problem` so lower tiers can route
22/// their error enums through `From<BlockCountMismatch>` without depending on
23/// `gam-models`; it is re-exported here unchanged.
24pub use gam_problem::BlockCountMismatch;
25
26/// Reject any `got` block count that does not exactly equal `expected`.
27///
28/// On mismatch, builds a [`BlockCountMismatch`] and converts it into the
29/// caller's error type `E` (chosen via the turbofish or inferred from the
30/// surrounding `?`). On a match, returns `Ok(())`.
31///
32/// This is the single source of truth for the block-arity guard shared
33/// across the family and term modules.
34#[inline]
35pub fn validate_block_count<E>(
36 family: impl Into<String>,
37 expected: usize,
38 got: usize,
39) -> Result<(), E>
40where
41 E: From<BlockCountMismatch>,
42{
43 if got != expected {
44 return Err(BlockCountMismatch {
45 family: family.into(),
46 expected,
47 got,
48 }
49 .into());
50 }
51 Ok(())
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 #[test]
59 fn matching_count_is_ok() {
60 // Exact match returns Ok(()) and produces no error.
61 let result: Result<(), String> = validate_block_count("FooFamily", 2, 2);
62 assert!(result.is_ok());
63
64 let zero: Result<(), String> = validate_block_count("EmptyFamily", 0, 0);
65 assert!(zero.is_ok());
66 }
67
68 #[test]
69 fn wrong_count_is_rejected_with_canonical_message() {
70 // Plural form when expected != 1.
71 let err: String = validate_block_count::<String>("FooFamily", 2, 1).unwrap_err();
72 assert_eq!(err, "FooFamily expects 2 blocks, got 1");
73
74 // Too many blocks is rejected just the same.
75 let too_many: String = validate_block_count::<String>("FooFamily", 2, 3).unwrap_err();
76 assert_eq!(too_many, "FooFamily expects 2 blocks, got 3");
77 }
78
79 #[test]
80 fn singular_block_wording_when_expected_is_one() {
81 // The message switches to the singular "block" when expected == 1.
82 let err: String = validate_block_count::<String>("BarFamily", 1, 0).unwrap_err();
83 assert_eq!(err, "BarFamily expects 1 block, got 0");
84 }
85
86 #[test]
87 fn mismatch_carrier_message_matches_helper() {
88 // The carrier's message is the single source of truth for the wording.
89 let carrier = BlockCountMismatch {
90 family: "BazFamily".to_string(),
91 expected: 3,
92 got: 5,
93 };
94 assert_eq!(carrier.message(), "BazFamily expects 3 blocks, got 5");
95 // And the String conversion forwards to it.
96 let converted: String = carrier.into();
97 assert_eq!(converted, "BazFamily expects 3 blocks, got 5");
98 }
99}