LCOV - code coverage report
Current view: top level - src - validation.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 100.0 % 311 311
Test Date: 2026-08-24 08:16:01 Functions: - 0 0
Legend: Lines: hit not hit

            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              : }
        

Generated by: LCOV version 2.0-1