validateFunctionDefinitions function

Map<String, Map<String, Object?>> validateFunctionDefinitions(
  1. Object? value, {
  2. Set<String> modelNames = const {},
  3. Set<String> queryNames = const {},
  4. Set<String> fileCollectionNames = const {},
})

Validates structure against generated TypeBox tables, then semantic bounds. Returned definitions and all nested values are immutable and canonically ordered.

Implementation

Map<String, Map<String, Object?>> validateFunctionDefinitions(
  Object? value, {
  Set<String> modelNames = const {},
  Set<String> queryNames = const {},
  Set<String> fileCollectionNames = const {},
}) {
  final frozen = _bounded(value, '/functions');
  if (frozen is! Map<String, Object?> || frozen.length > 100) {
    _invalid(
      '/functions',
      'Expected at most 100 named definitions.',
      'Supply a map of function identifiers to explicit callable or HTTP definitions.',
    );
  }
  // Name unsupported schedule/event delivery before the generic structural
  // failure, matching the TypeScript validator's functionKindUnsupported pointers.
  for (final entry in frozen.entries) {
    final definition = entry.value;
    if (definition is! Map<String, Object?>) continue;
    final path = '/functions/${_pointer(entry.key)}';
    if (definition.containsKey('kind') &&
        definition['kind'] != 'callable' &&
        definition['kind'] != 'http') {
      _invalid(
        '$path/kind',
        'Only callable and HTTP handlers have a delivery runtime.',
        'Use callable or http; schedules and event delivery are not supported.',
      );
    }
    for (final key in _deliveryKeys) {
      if (definition.containsKey(key)) {
        _invalid(
          '$path/$key',
          'Trigger and schedule delivery is not supported; only callable and HTTP handlers have a delivery runtime.',
          'Remove $key; declare a callable or HTTP handler and invoke it explicitly.',
        );
      }
    }
  }
  // Validate separately so structural errors identify the offending function.
  for (final entry in frozen.entries) {
    _structural('FunctionsSection', {
      entry.key: entry.value,
    }, '/functions/${_pointer(entry.key)}');
  }
  final result = <String, Map<String, Object?>>{};
  for (final entry in frozen.entries) {
    final name = entry.key, definition = entry.value! as Map<String, Object?>;
    final path = '/functions/$name';
    if (name.startsWith('ffx') || name.startsWith('system')) {
      _invalid(
        path,
        'Function name uses a reserved prefix.',
        'Choose a name without ffx or system.',
      );
    }
    var count = 0;
    void inspect(
      Map<String, Object?> node,
      String at,
      int depth,
      bool property,
    ) {
      if (depth > 8) {
        _invalid(
          at,
          'Schema exceeds eight levels.',
          'Flatten the nested interface.',
        );
      }
      if (property && !node.containsKey('description')) {
        _invalid(
          '$at/description',
          'Property description is required.',
          'Describe the property in 10–500 characters.',
        );
      }
      final type = node['type'] is List
          ? (node['type'] as List)[1]
          : node['type'];
      switch (type) {
        case 'object':
          final properties = node['properties']! as Map<String, Object?>;
          count += properties.length;
          for (final required in node['required']! as List) {
            if (!properties.containsKey(required)) {
              _invalid(
                '$at/required',
                'Required property is undeclared.',
                'Declare $required or remove it from required.',
              );
            }
          }
          for (final property in properties.entries) {
            inspect(
              property.value! as Map<String, Object?>,
              '$at/properties/${property.key}',
              depth + 1,
              true,
            );
          }
        case 'array':
          if ((node['minItems'] as num? ?? 0) > (node['maxItems']! as num)) {
            _invalid(
              at,
              'minItems exceeds maxItems.',
              'Use ordered inclusive array bounds.',
            );
          }
          inspect(
            node['items']! as Map<String, Object?>,
            '$at/items',
            depth + 1,
            false,
          );
        case 'string':
          final min = node['minLength'] as num? ?? 0,
              max = node['maxLength']! as num;
          if (min > max) {
            _invalid(
              at,
              'minLength exceeds maxLength.',
              'Use ordered inclusive string bounds.',
            );
          }
          for (final member in node['enum'] as List? ?? const []) {
            final length = (member as String).runes.length;
            if (length < min || length > max) {
              _invalid(
                '$at/enum',
                'Enum member exceeds string bounds.',
                'Count Unicode scalar values when specifying string bounds.',
              );
            }
          }
        case 'integer' || 'number':
          if ((node['minimum']! as num) > (node['maximum']! as num)) {
            _invalid(
              at,
              'minimum exceeds maximum.',
              'Use ordered finite numeric bounds.',
            );
          }
      }
    }

    if (definition['kind'] == 'callable') {
      for (final key in ['input', 'output']) {
        final schema = definition[key]! as Map<String, Object?>;
        if (schema['type'] != 'object') {
          _invalid(
            '$path/$key/type',
            'Root must be a non-nullable object.',
            'Use type object at the interface root.',
          );
        }
        inspect(schema, '$path/$key', 1, false);
      }
      if (count > 256) {
        _invalid(
          path,
          'Interface exceeds 256 properties.',
          'Reduce combined input and output properties.',
        );
      }
    } else {
      final http = definition['http']! as Map<String, Object?>;
      final cors = http['cors']! as Map<String, Object?>;
      for (final (i, value) in (cors['origins']! as List).indexed) {
        final origin = value as String, uri = Uri.tryParse(value);
        if (uri == null ||
            !['http', 'https'].contains(uri.scheme) ||
            uri.host.isEmpty ||
            uri.host.contains('*') ||
            !_canonicalIpHost(uri.host) ||
            uri.userInfo.isNotEmpty ||
            uri.hasQuery ||
            uri.hasFragment ||
            uri.path.isNotEmpty ||
            uri.origin != origin) {
          _invalid(
            '$path/http/cors/origins/$i',
            'Expected an exact HTTP(S) origin.',
            'Remove paths, credentials, wildcards and noncanonical port spelling.',
          );
        }
      }
    }
    final egress = definition['egress']! as Map<String, Object?>;
    for (final (i, value) in (egress['hosts']! as List).indexed) {
      final host = value as String, labels = value.split('.');
      if (labels.length < 2 ||
          host != host.toLowerCase() ||
          labels.any(
            (label) => !RegExp(
              r'^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$',
            ).hasMatch(label),
          ) ||
          RegExp(r'^\d+(?:\.\d+){3}$').hasMatch(host) ||
          RegExp(r'(^|\.)(localhost|local|internal)$').hasMatch(host)) {
        _invalid(
          '$path/egress/hosts/$i',
          'Expected an exact public DNS name.',
          'Use a lowercase fully qualified host without a port, IP address or wildcard.',
        );
      }
    }
    for (final secret
        in (definition['secrets']! as Map<String, Object?>).entries) {
      if (((secret.value! as Map)['name'] as String).startsWith('FFX_')) {
        _invalid(
          '$path/secrets/${secret.key}/name',
          'Secret name is reserved.',
          'Choose a project-owned name without FFX_.',
        );
      }
    }
    final capabilities = definition['capabilities']! as Map<String, Object?>;
    final grants = <String>{};
    for (final (i, raw) in (capabilities['serviceGrants']! as List).indexed) {
      final grant = raw! as Map<String, Object?>;
      final resources = switch (grant['resourceKind']) {
        'model' => modelNames,
        'query' => queryNames,
        _ => fileCollectionNames,
      };
      if (!resources.contains(grant['resourceName'])) {
        _invalid(
          '$path/capabilities/serviceGrants/$i',
          'Grant references an undeclared resource.',
          'Declare the resource or remove this grant.',
        );
      }
      if (!grants.add('${grant['resourceKind']}/${grant['resourceName']}')) {
        _invalid(
          '$path/capabilities/serviceGrants/$i',
          'Resource has duplicate grants.',
          'Combine operations into one grant.',
        );
      }
    }
    result[name] = definition;
  }
  return Map.unmodifiable(result);
}