validate method

SchemaResponse validate(
  1. Map<String, dynamic> data
)

Validate data against this schema.

Implementation

SchemaResponse validate(Map<String, dynamic> data) {
  // Work on a defensive copy; never mutate caller input.
  final input = Map<String, dynamic>.from(data);
  final sanitized = <String, dynamic>{
    for (final entry in data.entries)
      if (allowUnknownKeys || _schema.containsKey(entry.key))
        entry.key: entry.value,
  };
  final errors = <String, String>{};

  for (final key in _schema.keys) {
    final rules = _schema[key]!;
    final hasKey = input.containsKey(key);
    final value = input[key];

    // ---------- required / conditional required ----------
    final isRequired = _asBool(rules[Rules.required]) ?? false;

    final requiredIf = rules[Rules.requiredIf];
    final requiredWhen = rules[Rules.requiredWhen];
    final requiredBecauseIf = _requiredIf(requiredIf, input);
    final requiredBecauseWhen = _requiredWhen(requiredWhen, input);

    final mustExist = isRequired || requiredBecauseIf || requiredBecauseWhen;

    if (!hasKey || value == null) {
      if (mustExist) {
        _putError(
          errors,
          key,
          _msg(key, Rules.required, defaultMsg: '$key is required'),
        );
        sanitized.remove(key);
        continue;
      } else {
        // no value, not required => skip further checks
        sanitized.remove(key);
        continue;
      }
    }

    // ---------- notEmpty ----------
    if (_asBool(rules[Rules.notEmpty]) == true) {
      if ((value is String && value.trim().isEmpty) ||
          (value is Iterable && value.isEmpty) ||
          (value is Map && value.isEmpty)) {
        _putError(
          errors,
          key,
          _msg(key, Rules.notEmpty, defaultMsg: '$key must not be empty'),
        );
        sanitized.remove(key);
        continue;
      }
    }

    // ---------- type check ----------
    final expectedType = rules[Rules.type];
    if (expectedType != null) {
      final ok = _isOfType(value, expectedType);
      if (!ok) {
        _putError(
          errors,
          key,
          _msg(key, Rules.type, defaultMsg: '$key has invalid type'),
        );
        sanitized.remove(key);
        continue;
      }
    }

    // ---------- numeric bounds ----------
    if (value is num) {
      final min = rules[Rules.min];
      if (min is num && value < min) {
        _putError(
          errors,
          key,
          _msg(key, Rules.min, defaultMsg: '$key is below minimum ($min)'),
        );
      }
      final max = rules[Rules.max];
      if (max is num && value > max) {
        _putError(
          errors,
          key,
          _msg(key, Rules.max, defaultMsg: '$key exceeds maximum ($max)'),
        );
      }
    }

    // ---------- string length bounds ----------
    if (value is String) {
      final minLen = rules[Rules.minLength];
      if (minLen is int && value.length < minLen) {
        _putError(
          errors,
          key,
          _msg(
            key,
            Rules.minLength,
            defaultMsg: '$key is too short (min $minLen)',
          ),
        );
      }
      final maxLen = rules[Rules.maxLength];
      if (maxLen is int && value.length > maxLen) {
        _putError(
          errors,
          key,
          _msg(
            key,
            Rules.maxLength,
            defaultMsg: '$key is too long (max $maxLen)',
          ),
        );
      }
    }

    // ---------- list size bounds ----------
    if (value is List) {
      final minItems = rules[Rules.minItems];
      if (minItems is int && value.length < minItems) {
        _putError(
          errors,
          key,
          _msg(
            key,
            Rules.minItems,
            defaultMsg: '$key needs at least $minItems item(s)',
          ),
        );
      }
      final maxItems = rules[Rules.maxItems];
      if (maxItems is int && value.length > maxItems) {
        _putError(
          errors,
          key,
          _msg(
            key,
            Rules.maxItems,
            defaultMsg: '$key allows at most $maxItems item(s)',
          ),
        );
      }
    }

    // ---------- format checks ----------
    if (_asBool(rules[Rules.email]) == true) {
      if (!(value is String && _emailRegex.hasMatch(value))) {
        _putError(
          errors,
          key,
          _msg(key, Rules.email, defaultMsg: '$key is not a valid email'),
        );
      }
    }

    if (_asBool(rules[Rules.phone]) == true) {
      // E.164: +<country><number>, up to 15 digits (relaxed to allow local too)
      if (!(value is String && _phoneE164Regex.hasMatch(value))) {
        _putError(
          errors,
          key,
          _msg(key, Rules.phone, defaultMsg: '$key is not a valid phone'),
        );
      }
    }

    if (_asBool(rules[Rules.url]) == true) {
      final uri = value is String ? Uri.tryParse(value) : null;
      final ok =
          uri != null &&
          ['http', 'https'].contains(uri.scheme) &&
          uri.host.isNotEmpty;
      if (!ok) {
        _putError(
          errors,
          key,
          _msg(key, Rules.url, defaultMsg: '$key is not a valid URL'),
        );
      }
    }

    if (_asBool(rules[Rules.uuid]) == true) {
      if (!(value is String && _uuidRegex.hasMatch(value))) {
        _putError(
          errors,
          key,
          _msg(key, Rules.uuid, defaultMsg: '$key is not a valid UUID'),
        );
      }
    }

    if (_asBool(rules[Rules.ipv4]) == true) {
      if (!(value is String && _ipv4Regex.hasMatch(value))) {
        _putError(
          errors,
          key,
          _msg(key, Rules.ipv4, defaultMsg: '$key is not a valid IPv4'),
        );
      }
    }

    if (_asBool(rules[Rules.ipv6]) == true) {
      if (!(value is String && _isIpv6(value))) {
        _putError(
          errors,
          key,
          _msg(key, Rules.ipv6, defaultMsg: '$key is not a valid IPv6'),
        );
      }
    }

    final pattern = rules[Rules.pattern];
    if (pattern is String || pattern is RegExp) {
      final rx = pattern is RegExp ? pattern : RegExp(pattern as String);
      if (!(value is String && rx.hasMatch(value))) {
        _putError(
          errors,
          key,
          _msg(
            key,
            Rules.pattern,
            defaultMsg: '$key does not match required pattern',
          ),
        );
      }
    }

    // ---------- enums / allowed set ----------
    final allowed = rules[Rules.includes] ?? rules['enum'];
    if (allowed is Iterable) {
      final set = allowed.toSet();
      if (!set.contains(value)) {
        _putError(
          errors,
          key,
          _msg(key, Rules.includes, defaultMsg: '$key must be one of $set'),
        );
      }
    }

    // ---------- equalsTo (confirm) ----------
    final equalsTo = rules[Rules.equalsTo];
    if (equalsTo is String) {
      if (input[equalsTo] != value) {
        _putError(
          errors,
          key,
          _msg(key, Rules.equalsTo, defaultMsg: '$key must equal $equalsTo'),
        );
      }
    }

    // ---------- date & boundaries ----------
    if (_asBool(rules[Rules.date]) == true) {
      if (!(value is String && _isParsableDate(value))) {
        _putError(
          errors,
          key,
          _msg(key, Rules.date, defaultMsg: '$key is not a valid date'),
        );
      } else {
        final dt = DateTime.parse(value);
        final before = rules[Rules.before];
        final after = rules[Rules.after];
        if (before is String &&
            _isParsableDate(before) &&
            !(dt.isBefore(DateTime.parse(before)))) {
          _putError(
            errors,
            key,
            _msg(
              key,
              Rules.before,
              defaultMsg: '$key must be before $before',
            ),
          );
        }
        if (after is String &&
            _isParsableDate(after) &&
            !(dt.isAfter(DateTime.parse(after)))) {
          _putError(
            errors,
            key,
            _msg(key, Rules.after, defaultMsg: '$key must be after $after'),
          );
        }
      }
    }

    // ---------- custom function ----------
    final custom = rules[Rules.custom];
    if (custom
        is String? Function(dynamic value, Map<String, dynamic> data)) {
      final msg = custom(value, input);
      if (msg != null && msg.isNotEmpty) {
        _putError(errors, key, _msg(key, Rules.custom, defaultMsg: msg));
      }
    }

    // If any error exists for this key, remove from sanitized.
    if (errors.containsKey(key)) {
      sanitized.remove(key);
    }
  }

  return SchemaResponse(
    valid: errors.isEmpty,
    data: Map.unmodifiable(input),
    schema: Map.unmodifiable(sanitized),
    errors: Map.unmodifiable(errors),
  );
}