Line data Source code
1 : // Copyright 2026 The Authors
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // https://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : import 'package:betto_common/collections.dart' show Range;
16 : import 'package:characters/characters.dart';
17 : import 'package:collection/collection.dart';
18 :
19 : import 'lists.dart';
20 :
21 : /// Validators are used to validate data against a schema
22 : ///
23 : /// The [name] provides a handy string to use in error messages.
24 : ///
25 : /// Validators are [call]able classes that usually have that single
26 : /// instance method.
27 : abstract interface class Validator<T> {
28 : String get name;
29 :
30 : bool call(T input);
31 :
32 : Map<String, dynamic> toMap();
33 : }
34 :
35 : /// Validates that the input is one of the specified values
36 : class EnumValidator<T> implements Validator<T> {
37 : /// The allowed values
38 : Iterable<T> values;
39 :
40 : @override
41 : final String name = 'enum';
42 :
43 1 : EnumValidator(Iterable<T> values)
44 2 : : values = UnmodifiableListView([...values]);
45 :
46 1 : @override
47 2 : bool call(T input) => values.contains(input);
48 :
49 1 : @override
50 : bool operator ==(Object other) {
51 1 : if (other is EnumValidator<T>) {
52 3 : return hasTheSameElements(other.values, values);
53 : }
54 : return false;
55 : }
56 :
57 1 : @override
58 4 : int get hashCode => Object.hashAllUnordered([name, ...values]);
59 :
60 1 : @override
61 1 : Map<String, dynamic> toMap() => {
62 1 : 'name': name,
63 5 : 'value': values.map((e) => e.toString()).toList(),
64 : };
65 : }
66 :
67 : /// Validates that the input is equal to the specified value.
68 : ///
69 : /// Uses [DeepCollectionEquality] for the comparison so that nested [List] and
70 : /// [Map] values are compared by structural value rather than by reference.
71 : /// Primitive values (numbers, strings, booleans, `null`) are handled correctly
72 : /// by deep equality as well.
73 : class ConstValidator<T> implements Validator<T> {
74 : /// The single allowed value.
75 : T value;
76 :
77 : @override
78 : final String name = 'const';
79 :
80 : // Deep equality is required for JSON value comparison — Dart's == operator
81 : // compares List and Map by identity, not by structural value, so plain ==
82 : // would produce false negatives for nested objects and arrays.
83 : static const _deep = DeepCollectionEquality();
84 :
85 1 : ConstValidator(this.value);
86 :
87 1 : @override
88 2 : bool call(T input) => _deep.equals(value, input);
89 :
90 1 : @override
91 : bool operator ==(Object other) {
92 1 : if (other is ConstValidator<T>) {
93 3 : return _deep.equals(other.value, value);
94 : }
95 : return false;
96 : }
97 :
98 1 : @override
99 4 : int get hashCode => Object.hash(name, _deep.hash(value));
100 :
101 1 : @override
102 3 : Map<String, dynamic> toMap() => {'name': name, 'value': value};
103 : }
104 :
105 : /// Validates that a value is less than or equal to the specified [max]
106 : ///
107 : /// Note that [max] is inclusive.
108 : class Maximum<T extends num> implements Validator<T> {
109 : /// The (inclusive) maximum allowed value
110 : final num max;
111 :
112 : @override
113 : final String name = 'maximum';
114 :
115 1 : Maximum(this.max);
116 :
117 1 : @override
118 2 : bool call(T input) => maximum(input, max);
119 :
120 1 : static bool maximum(num input, num max) {
121 1 : return input <= max;
122 : }
123 :
124 1 : @override
125 : bool operator ==(Object other) {
126 1 : if (other is Maximum<T>) {
127 3 : return other.max == max;
128 : }
129 : return false;
130 : }
131 :
132 1 : @override
133 3 : int get hashCode => Object.hash(name, max);
134 :
135 1 : @override
136 3 : Map<String, dynamic> toMap() => {'name': name, 'value': max};
137 : }
138 :
139 : /// Validates that a value is less than the specified [max]
140 : ///
141 : /// Note that [max] is exclusive.
142 : class ExclusiveMaximum<T extends num> implements Validator<T> {
143 : /// The (exclusive) maximum allowed value
144 : final num max;
145 :
146 : @override
147 : final String name = 'exclusiveMaximum';
148 :
149 1 : ExclusiveMaximum(this.max);
150 :
151 1 : @override
152 2 : bool call(T input) => exclusiveMaximum(input, max);
153 :
154 1 : bool exclusiveMaximum(num input, num max) {
155 1 : return input < max;
156 : }
157 :
158 1 : @override
159 : bool operator ==(Object other) {
160 1 : if (other is ExclusiveMaximum<T>) {
161 3 : return other.max == max;
162 : }
163 : return false;
164 : }
165 :
166 1 : @override
167 3 : int get hashCode => Object.hash(name, max);
168 :
169 1 : @override
170 3 : Map<String, dynamic> toMap() => {'name': name, 'value': max};
171 : }
172 :
173 : /// Validates that a value is greater than or equal to the specified [min]
174 : class Minimum<T extends num> implements Validator<T> {
175 : /// The (inclusive) minimum allowed value
176 : final T min;
177 :
178 : @override
179 : final String name = 'minimum';
180 :
181 1 : Minimum(this.min);
182 :
183 1 : @override
184 2 : bool call(T input) => minimum(input, min);
185 :
186 1 : bool minimum(num input, num min) {
187 1 : return input >= min;
188 : }
189 :
190 1 : @override
191 : bool operator ==(Object other) {
192 1 : if (other is Minimum<T>) {
193 3 : return other.min == min;
194 : }
195 : return false;
196 : }
197 :
198 1 : @override
199 3 : int get hashCode => Object.hash(name, min);
200 :
201 1 : @override
202 3 : Map<String, dynamic> toMap() => {'name': name, 'value': min};
203 : }
204 :
205 : /// Validates that a value is greater than the specified [min]
206 : class ExclusiveMinimum<T extends num> implements Validator<T> {
207 : /// The (exclusive) minimum allowed value
208 : final T min;
209 :
210 : @override
211 : final String name = 'exclusiveMinimum';
212 :
213 1 : ExclusiveMinimum(this.min);
214 :
215 1 : @override
216 2 : bool call(T input) => exclusiveMinimum(input, min);
217 :
218 1 : bool exclusiveMinimum(num input, num min) {
219 1 : return input > min;
220 : }
221 :
222 1 : @override
223 : bool operator ==(Object other) {
224 1 : if (other is ExclusiveMinimum<T>) {
225 3 : return other.min == min;
226 : }
227 : return false;
228 : }
229 :
230 1 : @override
231 3 : int get hashCode => Object.hash(name, min);
232 :
233 1 : @override
234 3 : Map<String, dynamic> toMap() => {'name': name, 'value': min};
235 : }
236 :
237 : /// Validates that a value is a multiple of the specified [divisor].
238 : ///
239 : /// Uses a floating-point-safe algorithm: divides [input] by [divisor] and
240 : /// checks whether the quotient is within [_epsilon] of a whole number. The
241 : /// naive `input % divisor == 0` check fails for decimal divisors such as
242 : /// `0.1` due to IEEE-754 rounding (e.g. `0.3 % 0.1` is not exactly `0`).
243 : class MultipleOf<T extends num> implements Validator<T> {
244 : final T divisor;
245 :
246 : @override
247 : final String name = 'multipleOf';
248 :
249 : // Tolerance used when checking whether the quotient is a whole number.
250 : // 1e-10 is small enough to avoid false positives for common decimal values
251 : // while remaining robust to typical IEEE-754 rounding errors.
252 : static const double _epsilon = 1e-10;
253 :
254 1 : MultipleOf(this.divisor);
255 :
256 1 : @override
257 2 : bool call(num input) => multipleOf(input, divisor);
258 :
259 : /// Returns `true` if [input] is a multiple of [divisor].
260 : ///
261 : /// A [divisor] of zero is treated as a schema-error guard: the spec requires
262 : /// `multipleOf` values to be strictly greater than zero, so a zero divisor
263 : /// returns `false` rather than throwing.
264 1 : bool multipleOf(num input, num divisor) {
265 1 : if (divisor == 0) return false;
266 : // Compute the quotient and check that its fractional part is negligibly
267 : // small, guarding against IEEE-754 rounding in decimal arithmetic.
268 1 : final quotient = input / divisor;
269 4 : return (quotient - quotient.roundToDouble()).abs() < _epsilon;
270 : }
271 :
272 1 : @override
273 : bool operator ==(Object other) {
274 1 : if (other is MultipleOf<T>) {
275 3 : return other.divisor == divisor;
276 : }
277 : return false;
278 : }
279 :
280 1 : @override
281 3 : int get hashCode => Object.hash(name, divisor);
282 :
283 1 : @override
284 3 : Map<String, dynamic> toMap() => {'name': name, 'value': divisor};
285 : }
286 :
287 : /// Validates that a value is within the specified [range]
288 : class InRange implements Validator<num> {
289 : final Range range;
290 :
291 : @override
292 : final String name = 'inRange';
293 :
294 1 : InRange(this.range);
295 :
296 1 : @override
297 2 : bool call(num input) => range.contains(input);
298 :
299 1 : @override
300 : bool operator ==(Object other) {
301 1 : if (other is InRange) {
302 3 : return other.range == range;
303 : }
304 : return false;
305 : }
306 :
307 1 : @override
308 3 : int get hashCode => Object.hash(name, range);
309 :
310 1 : @override
311 4 : Map<String, dynamic> toMap() => {'name': name, 'value': range.toMap()};
312 : }
313 :
314 : /// Validates that a string is not longer than the specified [maximumLength]
315 : class MaximumLength implements Validator<String> {
316 : final int maximumLength;
317 :
318 : @override
319 : final String name = 'maximumLength';
320 :
321 1 : MaximumLength(this.maximumLength);
322 :
323 1 : @override
324 2 : bool call(String input) => maxLength(input, maximumLength);
325 :
326 1 : static bool maxLength(String input, int maximumLength) {
327 3 : return input.characters.length <= maximumLength;
328 : }
329 :
330 1 : @override
331 : bool operator ==(Object other) {
332 1 : if (other is MaximumLength) {
333 3 : return other.maximumLength == maximumLength;
334 : }
335 : return false;
336 : }
337 :
338 1 : @override
339 3 : int get hashCode => Object.hash(name, maximumLength);
340 :
341 1 : @override
342 3 : Map<String, dynamic> toMap() => {'name': name, 'value': maximumLength};
343 : }
344 :
345 : /// Validates that a string is exactly the specified [length]
346 : class ExactLength implements Validator<String> {
347 : final int length;
348 :
349 : @override
350 : final String name = 'exactLength';
351 :
352 1 : ExactLength(this.length);
353 :
354 1 : @override
355 4 : bool call(String input) => input.characters.length == length;
356 :
357 1 : @override
358 : bool operator ==(Object other) {
359 1 : if (other is ExactLength) {
360 3 : return other.length == length;
361 : }
362 : return false;
363 : }
364 :
365 1 : @override
366 3 : int get hashCode => Object.hash(name, length);
367 :
368 1 : @override
369 3 : Map<String, dynamic> toMap() => {'name': name, 'value': length};
370 : }
371 :
372 : /// Validates that a string is not shorter than the specified [minimumLength]
373 : class MinimumLength implements Validator<String> {
374 : final int minimumLength;
375 :
376 : @override
377 : final String name = 'minimumLength';
378 :
379 1 : MinimumLength(this.minimumLength);
380 :
381 1 : @override
382 2 : bool call(String input) => minLength(input, minimumLength);
383 :
384 1 : bool minLength(String input, int minimumLength) {
385 3 : return input.characters.length >= minimumLength;
386 : }
387 :
388 1 : @override
389 : bool operator ==(Object other) {
390 1 : if (other is MinimumLength) {
391 3 : return other.minimumLength == minimumLength;
392 : }
393 : return false;
394 : }
395 :
396 1 : @override
397 3 : int get hashCode => Object.hash(name, minimumLength);
398 :
399 1 : @override
400 3 : Map<String, dynamic> toMap() => {'name': name, 'value': minimumLength};
401 : }
402 :
403 : class InRangeLength implements Validator<String> {
404 : final Range range;
405 :
406 : @override
407 : final String name = 'inRangeLength';
408 :
409 1 : InRangeLength(this.range);
410 :
411 1 : @override
412 4 : bool call(String input) => range.contains(input.characters.length);
413 :
414 1 : @override
415 : bool operator ==(Object other) {
416 1 : if (other is InRangeLength) {
417 3 : return other.range == range;
418 : }
419 : return false;
420 : }
421 :
422 1 : @override
423 3 : int get hashCode => Object.hash(name, range);
424 :
425 1 : @override
426 4 : Map<String, dynamic> toMap() => {'name': name, 'value': range.toMap()};
427 : }
428 :
429 : /// Validates that a string matches the specified [pattern]
430 : class PatternValidator implements Validator<String> {
431 : final RegExp pattern;
432 :
433 : @override
434 : final String name = 'pattern';
435 :
436 1 : PatternValidator(this.pattern);
437 :
438 3 : PatternValidator.fromString(String pattern) : this(RegExp(pattern));
439 :
440 1 : @override
441 : bool call(String input) {
442 : // Per JSON Schema spec §6.3.3, patterns are not implicitly anchored —
443 : // the pattern only needs to match somewhere within the string.
444 2 : return pattern.hasMatch(input);
445 : }
446 :
447 1 : @override
448 : bool operator ==(Object other) {
449 1 : if (other is PatternValidator) {
450 3 : return other.pattern == pattern;
451 : }
452 : return false;
453 : }
454 :
455 1 : @override
456 3 : int get hashCode => Object.hash(name, pattern);
457 :
458 1 : @override
459 1 : Map<String, dynamic> toMap() => {
460 1 : 'name': name,
461 3 : 'value': pattern.pattern.toString(),
462 : };
463 : }
464 :
465 : /// Validates that a list has at most [max] items
466 : class MaxItems<T> implements Validator<Iterable<T>> {
467 : final int max;
468 :
469 : @override
470 : final String name = 'maxItems';
471 :
472 1 : MaxItems(this.max);
473 :
474 1 : @override
475 2 : bool call(Iterable input) => maxItems(input, max);
476 :
477 3 : bool maxItems(Iterable input, int max) => input.length <= max;
478 :
479 1 : @override
480 : bool operator ==(Object other) {
481 1 : if (other is MaxItems) {
482 3 : return other.max == max;
483 : }
484 : return false;
485 : }
486 :
487 1 : @override
488 3 : int get hashCode => Object.hash(name, max);
489 :
490 1 : @override
491 3 : Map<String, dynamic> toMap() => {'name': name, 'value': max};
492 : }
493 :
494 : /// Validates that a list has at least [min] items
495 : class MinItems<T> implements Validator<Iterable<T>> {
496 : final int min;
497 :
498 : @override
499 : final String name = 'minItems';
500 :
501 1 : MinItems(this.min);
502 :
503 1 : @override
504 2 : bool call(Iterable input) => minItems(input, min);
505 :
506 3 : bool minItems(Iterable input, int min) => input.length >= min;
507 :
508 1 : @override
509 : bool operator ==(Object other) {
510 1 : if (other is MinItems) {
511 3 : return other.min == min;
512 : }
513 : return false;
514 : }
515 :
516 1 : @override
517 3 : int get hashCode => Object.hash(name, min);
518 :
519 1 : @override
520 3 : Map<String, dynamic> toMap() => {'name': name, 'value': min};
521 : }
522 :
523 : /// Validates that a list has [count] items
524 : class ItemCount<T> implements Validator<Iterable<T>> {
525 : final int count;
526 :
527 : @override
528 : final String name = 'itemCount';
529 :
530 1 : ItemCount(this.count);
531 :
532 1 : @override
533 2 : bool call(Iterable<T> input) => countItems(input, count);
534 :
535 3 : bool countItems(Iterable input, int count) => input.length == count;
536 :
537 1 : @override
538 : bool operator ==(Object other) {
539 1 : if (other is ItemCount) {
540 3 : return other.count == count;
541 : }
542 : return false;
543 : }
544 :
545 1 : @override
546 3 : int get hashCode => Object.hash(name, count);
547 :
548 1 : @override
549 3 : Map<String, dynamic> toMap() => {'name': name, 'value': count};
550 : }
551 :
552 : /// Validates that a list has a unique set of items.
553 : ///
554 : /// Uses an O(n²) pairwise [DeepCollectionEquality] comparison so that nested
555 : /// [List] and [Map] elements are compared by structural value rather than by
556 : /// reference. A `LinkedHashSet` with a deep-equality hasher could give O(n)
557 : /// average-case but would require a matching deep hash function; the pairwise
558 : /// approach is simpler and correct for the expected list sizes in JSON Schema
559 : /// validation.
560 : class UniqueItems<T> implements Validator<Iterable<T>> {
561 : @override
562 : final String name = 'uniqueItems';
563 :
564 : static const _deep = DeepCollectionEquality();
565 :
566 1 : @override
567 1 : bool call(Iterable input) => uniqueItems(input);
568 :
569 : /// Returns `true` if all elements are pairwise distinct under deep equality.
570 1 : bool uniqueItems(Iterable input) {
571 1 : final items = input.toList();
572 3 : for (var i = 0; i < items.length; i++) {
573 4 : for (var j = i + 1; j < items.length; j++) {
574 3 : if (_deep.equals(items[i], items[j])) return false;
575 : }
576 : }
577 : return true;
578 : }
579 :
580 1 : @override
581 1 : bool operator ==(Object other) => other is UniqueItems;
582 :
583 1 : @override
584 2 : int get hashCode => name.hashCode;
585 :
586 1 : @override
587 2 : Map<String, dynamic> toMap() => {'name': name};
588 : }
589 :
590 : /// Validates that a map has at least [min] key/value pairs
591 : class MinProperties implements Validator<Map> {
592 : final int min;
593 :
594 : @override
595 : final String name = 'minProperties';
596 :
597 1 : MinProperties(this.min);
598 :
599 1 : @override
600 3 : bool call(Map input) => input.length >= min;
601 :
602 1 : @override
603 : bool operator ==(Object other) {
604 1 : if (other is MinProperties) {
605 3 : return other.min == min;
606 : }
607 : return false;
608 : }
609 :
610 1 : @override
611 3 : int get hashCode => Object.hash(name, min);
612 :
613 1 : @override
614 3 : Map<String, dynamic> toMap() => {'name': name, 'value': min};
615 : }
616 :
617 : /// Validates that a map has at most [max] key/value pairs
618 : class MaxProperties implements Validator<Map> {
619 : final int max;
620 :
621 : @override
622 : final String name = 'maxProperties';
623 :
624 1 : MaxProperties(this.max);
625 :
626 1 : @override
627 3 : bool call(Map input) => input.length <= max;
628 :
629 1 : @override
630 : bool operator ==(Object other) {
631 1 : if (other is MaxProperties) {
632 3 : return other.max == max;
633 : }
634 : return false;
635 : }
636 :
637 1 : @override
638 3 : int get hashCode => Object.hash(name, max);
639 :
640 1 : @override
641 3 : Map<String, dynamic> toMap() => {'name': name, 'value': max};
642 : }
643 :
644 : /// Validates that a map contains all of the specified [properties]
645 : class Required implements Validator<Map> {
646 : final List<String> properties;
647 :
648 : @override
649 : final String name = 'required';
650 :
651 1 : Required(Iterable properties)
652 3 : : properties = UnmodifiableListView([...properties]);
653 :
654 1 : @override
655 4 : bool call(Map input) => isSubList(properties, input.keys.toList());
656 :
657 1 : @override
658 : bool operator ==(Object other) {
659 1 : if (other is Required) {
660 3 : return hasTheSameElements(other.properties, properties);
661 : }
662 : return false;
663 : }
664 :
665 1 : @override
666 4 : int get hashCode => Object.hashAllUnordered([name, ...properties]);
667 :
668 1 : @override
669 3 : Map<String, dynamic> toMap() => {'name': name, 'value': properties};
670 : }
671 :
672 : /// Checks whether [input] matches a single JSON Schema type string.
673 : ///
674 : /// Returns `true` if the value satisfies [type]. Unknown type strings return
675 : /// `false` (unlike `SchemaRule` which silently ignores them, this Layer 1
676 : /// validator is strict so callers can detect typos).
677 1 : bool _matchesType(String type, dynamic input) {
678 : return switch (type) {
679 2 : 'string' => input is String,
680 2 : 'number' => input is num,
681 : // Per JSON Schema spec §6.1.1, an integer is any number without a
682 : // fractional part — so 1.0 (a Dart double) must be accepted.
683 : // Non-finite doubles (NaN, Infinity) are excluded because their
684 : // modulo is NaN, not 0.
685 1 : 'integer' =>
686 5 : input is int || (input is double && input.isFinite && input % 1 == 0),
687 2 : 'boolean' => input is bool,
688 2 : 'array' => input is List,
689 2 : 'object' => input is Map,
690 1 : 'null' => input == null,
691 : _ => false,
692 : };
693 : }
694 :
695 : /// Validates that a value matches one of the JSON Schema [type] strings.
696 : ///
697 : /// Supports both the single-string form (`TypeValidator('string')`) and the
698 : /// array form (`TypeValidator.fromList(['string', 'null'])`) as required by
699 : /// JSON Schema spec §6.1.1. In the array form the value is valid if it
700 : /// matches *any* of the listed types (logical OR).
701 : ///
702 : /// Supported types: `string`, `number`, `integer`, `boolean`, `array`,
703 : /// `object`, `null`.
704 : class TypeValidator implements Validator<dynamic> {
705 : /// Creates a validator that accepts a single [type] string.
706 2 : TypeValidator(this.type) : types = [type];
707 :
708 : /// Creates a validator that accepts any of [types] (array form).
709 : ///
710 : /// Per JSON Schema spec §6.1.1, a value is valid when its type matches
711 : /// at least one entry in the list.
712 2 : TypeValidator.fromList(this.types) : type = types.join(',');
713 :
714 : /// The expected JSON Schema type string, or a comma-joined list for the
715 : /// array form (used for equality and hashing only).
716 : final String type;
717 :
718 : /// All accepted type strings.
719 : ///
720 : /// Contains exactly one entry in the single-string form.
721 : final List<String> types;
722 :
723 : @override
724 : final String name = 'type';
725 :
726 1 : @override
727 : bool call(dynamic input) {
728 4 : return types.any((t) => _matchesType(t, input));
729 : }
730 :
731 1 : @override
732 : bool operator ==(Object other) =>
733 4 : other is TypeValidator && other.type == type;
734 :
735 1 : @override
736 3 : int get hashCode => Object.hash(name, type);
737 :
738 1 : @override
739 3 : Map<String, dynamic> toMap() => {'name': name, 'value': type};
740 : }
741 :
742 : /// Validates each entry in a map against a per-key [Validator].
743 : ///
744 : /// Only validates keys that are present in the map — absent keys are ignored
745 : /// (use [Required] to enforce presence). Any key in [properties] not present
746 : /// in the input map is silently skipped.
747 : class PropertiesValidator implements Validator<Map> {
748 : /// Per-field validators keyed by field name.
749 : final Map<String, Validator<dynamic>> properties;
750 :
751 : @override
752 : final String name = 'properties';
753 :
754 1 : PropertiesValidator(Map<String, Validator<dynamic>> properties)
755 1 : : properties = Map.unmodifiable(properties);
756 :
757 1 : @override
758 : bool call(Map input) {
759 5 : for (final MapEntry(:key, value: validator) in properties.entries) {
760 1 : if (!input.containsKey(key)) continue;
761 2 : if (!validator(input[key])) return false;
762 : }
763 : return true;
764 : }
765 :
766 1 : @override
767 : bool operator ==(Object other) {
768 1 : if (other is! PropertiesValidator) return false;
769 5 : if (other.properties.length != properties.length) return false;
770 3 : for (final entry in properties.entries) {
771 5 : if (other.properties[entry.key] != entry.value) return false;
772 : }
773 : return true;
774 : }
775 :
776 1 : @override
777 2 : int get hashCode => Object.hashAllUnordered([
778 1 : name,
779 7 : ...properties.entries.map((e) => Object.hash(e.key, e.value)),
780 : ]);
781 :
782 1 : @override
783 1 : Map<String, dynamic> toMap() => {
784 1 : 'name': name,
785 7 : 'value': {for (final e in properties.entries) e.key: e.value.toString()},
786 : };
787 : }
788 :
789 : /// Validates that a map contains no keys outside [allowedProperties].
790 : ///
791 : /// Corresponds to `additionalProperties: false` in JSON Schema. Keys not
792 : /// listed in [allowedProperties] cause validation to fail.
793 : class AdditionalPropertiesValidator implements Validator<Map> {
794 : /// The complete set of permitted property names.
795 : final Set<String> allowedProperties;
796 :
797 : @override
798 : final String name = 'additionalProperties';
799 :
800 1 : AdditionalPropertiesValidator(Iterable<String> allowed)
801 2 : : allowedProperties = Set.unmodifiable(Set<String>.from(allowed));
802 :
803 1 : @override
804 : bool call(Map input) {
805 2 : for (final key in input.keys) {
806 2 : if (!allowedProperties.contains(key)) return false;
807 : }
808 : return true;
809 : }
810 :
811 1 : @override
812 : bool operator ==(Object other) {
813 1 : if (other is! AdditionalPropertiesValidator) return false;
814 5 : if (other.allowedProperties.length != allowedProperties.length) {
815 : return false;
816 : }
817 3 : return other.allowedProperties.containsAll(allowedProperties);
818 : }
819 :
820 1 : @override
821 4 : int get hashCode => Object.hashAllUnordered([name, ...allowedProperties]);
822 :
823 1 : @override
824 1 : Map<String, dynamic> toMap() => {
825 1 : 'name': name,
826 3 : 'value': allowedProperties.toList()..sort(),
827 : };
828 : }
829 :
830 : /// Validates that every element in an iterable satisfies [itemValidator].
831 : ///
832 : /// Corresponds to `items` in JSON Schema. An empty iterable always passes.
833 : class ItemsValidator<T> implements Validator<Iterable<T>> {
834 : /// The validator applied to each element.
835 : final Validator<T> itemValidator;
836 :
837 : @override
838 : final String name = 'items';
839 :
840 1 : ItemsValidator(this.itemValidator);
841 :
842 1 : @override
843 3 : bool call(Iterable<T> input) => input.every(itemValidator.call);
844 :
845 1 : @override
846 : bool operator ==(Object other) =>
847 4 : other is ItemsValidator && other.itemValidator == itemValidator;
848 :
849 1 : @override
850 3 : int get hashCode => Object.hash(name, itemValidator);
851 :
852 1 : @override
853 4 : Map<String, dynamic> toMap() => {'name': name, 'value': itemValidator.name};
854 : }
855 :
856 : /// Validates that, if a map has a specified key then it also has
857 : /// a set of dependent keys.
858 : ///
859 : /// For example:
860 : ///
861 : /// if [properties] is
862 : ///
863 : /// ```dart
864 : /// {
865 : /// 'x': ['a'],
866 : /// 'y': ['b', 'c'],
867 : /// }
868 : /// ```
869 : ///
870 : /// Then if [input] has `x` and `y` then it must also have keys `a`, `b`, and `c`.
871 : ///
872 : /// ... or if [input] only has `x` then it must also have keys `a`.
873 : ///
874 : /// ... or if [input] only has `y` then it must also have keys `b` and `c`.
875 : ///
876 : class DependentRequired implements Validator<Map> {
877 : final Map<String, List<String>> properties;
878 :
879 : @override
880 : final String name = 'dependentRequired';
881 :
882 1 : DependentRequired(Map<String, List<String>> properties)
883 2 : : properties = UnmodifiableMapView({
884 4 : ...properties.map((k, v) => MapEntry(k, UnmodifiableListView(v))),
885 : });
886 :
887 1 : @override
888 : bool call(Map input) {
889 5 : for (final MapEntry(:key, :value) in properties.entries) {
890 1 : if (input.containsKey(key)) {
891 3 : if (!isSubList(value, input.keys.toList())) return false;
892 : }
893 : }
894 : return true;
895 : }
896 :
897 1 : @override
898 : bool operator ==(Object other) {
899 1 : if (other is DependentRequired) {
900 5 : if (properties.length != other.properties.length) return false;
901 5 : if (!hasTheSameElements(properties.keys, other.properties.keys)) {
902 : return false;
903 : }
904 3 : for (final entry in properties.entries) {
905 5 : if (!hasTheSameElements(entry.value, other.properties[entry.key]!)) {
906 : return false;
907 : }
908 : }
909 : return true;
910 : }
911 :
912 : return false;
913 : }
914 :
915 1 : @override
916 : int get hashCode {
917 1 : final pairs = <(String, String)>[];
918 3 : for (final entry in properties.entries) {
919 5 : pairs.addAll([for (final v in entry.value) (entry.key, v)]);
920 : }
921 4 : return Object.hashAllUnordered([name, ...pairs]);
922 : }
923 :
924 1 : @override
925 3 : Map<String, dynamic> toMap() => {'name': name, 'value': properties};
926 : }
|