LCOV - code coverage report
Current view: top level - src - validation.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 100.0 % 286 286
Test Date: 2026-06-16 03:31:00 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: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              : }
        

Generated by: LCOV version 2.0-1