Skip to main content

icydb_model/base/validator/
len.rs

1//! Module: base::validator::len
2//!
3//! Responsibility: base validator definitions.
4//! Does not own: normalization policy, persistence, or schema mutation semantics.
5//! Boundary: reports typed visitor issues for facade schema values.
6
7use crate::{prelude::*, visitor::Validator};
8use std::{
9    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
10    hash::BuildHasher,
11};
12
13///
14/// HasLen
15///
16
17#[expect(clippy::len_without_is_empty)]
18pub trait HasLen {
19    fn len(&self) -> usize;
20}
21
22impl HasLen for Blob {
23    fn len(&self) -> usize {
24        Self::len(self)
25    }
26}
27
28impl HasLen for str {
29    fn len(&self) -> usize {
30        self.chars().count()
31    }
32}
33
34impl HasLen for String {
35    fn len(&self) -> usize {
36        self.chars().count()
37    }
38}
39
40impl<T> HasLen for [T] {
41    fn len(&self) -> usize {
42        <[T]>::len(self)
43    }
44}
45
46impl<T> HasLen for Vec<T> {
47    fn len(&self) -> usize {
48        Self::len(self)
49    }
50}
51
52impl<T, S: BuildHasher> HasLen for HashSet<T, S> {
53    fn len(&self) -> usize {
54        Self::len(self)
55    }
56}
57
58impl<K, V, S: BuildHasher> HasLen for HashMap<K, V, S> {
59    fn len(&self) -> usize {
60        Self::len(self)
61    }
62}
63
64impl<T> HasLen for BTreeSet<T> {
65    fn len(&self) -> usize {
66        Self::len(self)
67    }
68}
69
70impl<K, V> HasLen for BTreeMap<K, V> {
71    fn len(&self) -> usize {
72        Self::len(self)
73    }
74}
75
76//
77// ============================================================================
78// Equal
79// ============================================================================
80//
81
82#[validator]
83pub struct Equal {
84    target: usize,
85}
86
87impl Equal {
88    pub fn new(target: impl TryInto<usize>) -> Self {
89        Self {
90            target: target.try_into().unwrap_or_default(),
91        }
92    }
93}
94
95impl<T: HasLen + ?Sized> Validator<T> for Equal {
96    fn validate(&self, t: &T, ctx: &mut dyn VisitorContext) {
97        let len = t.len();
98
99        if len != self.target {
100            ctx.issue(format!("length ({len}) is not equal to {}", self.target));
101        }
102    }
103}
104
105///
106/// Min
107///
108
109#[validator]
110pub struct Min {
111    target: usize,
112}
113
114impl Min {
115    pub fn new(target: impl TryInto<usize>) -> Self {
116        Self {
117            target: target.try_into().unwrap_or_default(),
118        }
119    }
120}
121
122impl<T: HasLen + ?Sized> Validator<T> for Min {
123    fn validate(&self, t: &T, ctx: &mut dyn VisitorContext) {
124        let len = t.len();
125
126        if len < self.target {
127            ctx.issue(format!(
128                "length ({len}) is lower than minimum of {}",
129                self.target
130            ));
131        }
132    }
133}
134
135///
136/// Max
137///
138
139#[validator]
140pub struct Max {
141    target: usize,
142}
143
144impl Max {
145    pub fn new(target: impl TryInto<usize>) -> Self {
146        Self {
147            target: target.try_into().unwrap_or_default(),
148        }
149    }
150}
151
152impl<T: HasLen + ?Sized> Validator<T> for Max {
153    fn validate(&self, t: &T, ctx: &mut dyn VisitorContext) {
154        let len = t.len();
155
156        if len > self.target {
157            ctx.issue(format!(
158                "length ({len}) is greater than maximum of {}",
159                self.target
160            ));
161        }
162    }
163}
164
165///
166/// Range
167///
168
169#[validator]
170pub struct Range {
171    min: usize,
172    max: usize,
173}
174
175impl Range {
176    pub fn new(min: impl TryInto<usize>, max: impl TryInto<usize>) -> Self {
177        Self {
178            min: min.try_into().unwrap_or_default(),
179            max: max.try_into().unwrap_or_default(),
180        }
181    }
182}
183
184impl<T: HasLen + ?Sized> Validator<T> for Range {
185    fn validate(&self, t: &T, ctx: &mut dyn VisitorContext) {
186        let len = t.len();
187
188        if len < self.min || len > self.max {
189            ctx.issue(format!(
190                "length ({len}) must be between {} and {} (inclusive)",
191                self.min, self.max
192            ));
193        }
194    }
195}
196
197///
198/// TESTS
199///
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    struct TestCtx {
206        issues: crate::visitor::VisitorIssues,
207    }
208
209    impl TestCtx {
210        fn new() -> Self {
211            Self {
212                issues: crate::visitor::VisitorIssues::new(),
213            }
214        }
215    }
216
217    impl crate::visitor::VisitorContext for TestCtx {
218        fn add_issue(&mut self, issue: crate::visitor::Issue) {
219            self.issues.push(String::new(), issue);
220        }
221
222        fn add_issue_at(&mut self, _: crate::visitor::PathSegment, issue: crate::visitor::Issue) {
223            self.add_issue(issue);
224        }
225    }
226
227    #[test]
228    fn equal_reports_mismatch() {
229        let v = Equal::new(3);
230        let mut ctx = TestCtx::new();
231
232        v.validate("abcd", &mut ctx);
233
234        assert_eq!(
235            ctx.issues.get("").expect("root issue should exist")[0].message(),
236            "length (4) is not equal to 3"
237        );
238    }
239
240    #[test]
241    fn range_accepts_in_bounds() {
242        let v = Range::new(2, 4);
243        let mut ctx = TestCtx::new();
244
245        v.validate("abc", &mut ctx);
246
247        assert!(ctx.issues.is_empty());
248    }
249
250    #[test]
251    fn text_length_counts_unicode_scalar_values_not_utf8_bytes() {
252        let one_scalar_four_bytes = "\u{1F525}";
253
254        let equal_one = Equal::new(1);
255        let min_two = Min::new(2);
256        let max_one = Max::new(1);
257        let mut equal_ctx = TestCtx::new();
258        let mut min_ctx = TestCtx::new();
259        let mut max_ctx = TestCtx::new();
260
261        equal_one.validate(one_scalar_four_bytes, &mut equal_ctx);
262        min_two.validate(one_scalar_four_bytes, &mut min_ctx);
263        max_one.validate(one_scalar_four_bytes, &mut max_ctx);
264
265        assert!(equal_ctx.issues.is_empty());
266        assert_eq!(
267            min_ctx.issues.get("").expect("root issue should exist")[0].message(),
268            "length (1) is lower than minimum of 2"
269        );
270        assert!(max_ctx.issues.is_empty());
271    }
272
273    #[test]
274    fn range_reports_out_of_bounds() {
275        let v = Range::new(2, 4);
276        let mut ctx = TestCtx::new();
277
278        v.validate("a", &mut ctx);
279
280        assert_eq!(
281            ctx.issues.get("").expect("root issue should exist")[0].message(),
282            "length (1) must be between 2 and 4 (inclusive)"
283        );
284    }
285}