icydb_model/base/validator/
len.rs1use crate::{prelude::*, visitor::Validator};
8use std::{
9 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
10 hash::BuildHasher,
11};
12
13#[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#[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#[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#[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#[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#[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}