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