Skip to main content

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}