icydb/base/validator/
len.rs

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