validateFunctionSchema function

void validateFunctionSchema(
  1. Map<String, Object?> schema,
  2. Object? value, {
  3. Map<String, Object?> references = const {},
  4. int maxDepth = 8,
  5. bool utf16StringBounds = false,
})

Validate decoded project values or generated private schemas without coercion. references is generated from TypeBox; callers cannot supply remote schemas.

Implementation

void validateFunctionSchema(
  Map<String, Object?> schema,
  Object? value, {
  Map<String, Object?> references = const {},
  int maxDepth = 8,
  bool utf16StringBounds = false,
}) {
  var nodes = 0;
  void visit(Map<String, Object?> node, Object? value, int depth) {
    Never invalid() =>
        throw const FormatException('Value violates its function schema.');
    if (++nodes > 100000 || depth > maxDepth) invalid();
    if (node[r'$ref'] case final String ref) {
      final target = references[ref];
      if (target is! Map<String, Object?>) {
        throw StateError('Missing generated schema.');
      }
      visit(target, value, depth);
      return;
    }
    if (node['anyOf'] case final List choices) {
      for (final choice in choices) {
        try {
          visit((choice as Map).cast<String, Object?>(), value, depth);
          return;
        } on FormatException {
          continue;
        }
      }
      invalid();
    }
    // A nullable type list accepts null before any enum/const check, matching
    // the TypeScript validator and the generated nullable enum models.
    final declared = node['type'];
    if (value == null && declared is List && declared.contains('null')) return;
    if (node.containsKey('const') && value != node['const'] ||
        node['enum'] is List && !(node['enum'] as List).contains(value)) {
      invalid();
    }
    final type = declared is List ? declared.last : declared;
    switch (type) {
      case 'null':
        if (value != null) invalid();
      case 'boolean':
        if (value is! bool) invalid();
      case 'number':
      case 'integer':
        if (value is! num ||
            !value.isFinite ||
            type == 'integer' &&
                (value.abs() > 9007199254740991 ||
                    value != value.truncateToDouble()) ||
            node['minimum'] is num && value < (node['minimum'] as num) ||
            node['maximum'] is num && value > (node['maximum'] as num)) {
          invalid();
        }
      case 'string':
        if (value is! String || !_wellFormed(value)) invalid();
        final length = utf16StringBounds ? value.length : value.runes.length;
        if (node['minLength'] is num && length < (node['minLength'] as num) ||
            node['maxLength'] is num && length > (node['maxLength'] as num) ||
            node['pattern'] is String &&
                !RegExp(node['pattern'] as String).hasMatch(value)) {
          invalid();
        }
      case 'array':
        if (value is! List ||
            node['minItems'] is num &&
                value.length < (node['minItems'] as num) ||
            node['maxItems'] is num &&
                value.length > (node['maxItems'] as num)) {
          invalid();
        }
        if (node['uniqueItems'] == true &&
            value.map(jsonEncode).toSet().length != value.length) {
          invalid();
        }
        final items = node['items'];
        for (var i = 0; i < value.length; i++) {
          if (items is List && i >= items.length) {
            if (node['additionalItems'] == false) invalid();
            continue;
          }
          visit(
            ((items is List ? items[i] : items) as Map).cast<String, Object?>(),
            value[i],
            depth + 1,
          );
        }
      case 'object':
        if (value is! Map ||
            value.keys.any((key) => key is! String) ||
            node['minProperties'] is num &&
                value.length < (node['minProperties'] as num) ||
            node['maxProperties'] is num &&
                value.length > (node['maxProperties'] as num)) {
          invalid();
        }
        if ((node['required'] as List? ?? []).any(
          (key) => !value.containsKey(key),
        )) {
          invalid();
        }
        final properties = (node['properties'] as Map? ?? {})
            .cast<String, Object?>();
        final patterns = (node['patternProperties'] as Map? ?? {})
            .cast<String, Object?>();
        for (final entry in value.entries) {
          Object? child = properties[entry.key];
          for (final pattern in patterns.entries) {
            if (RegExp(pattern.key).hasMatch(entry.key)) child = pattern.value;
          }
          child ??= node['additionalProperties'] is Map
              ? node['additionalProperties']
              : null;
          if (child == null && node['additionalProperties'] == false) invalid();
          if (child != null) {
            visit(
              (child as Map).cast<String, Object?>(),
              entry.value,
              depth + 1,
            );
          }
        }
    }
  }

  visit(schema, value, 1);
}