parseSdl function

EntityGraphIR parseSdl(
  1. Object? doc
)

Parse + validate a Map SDL document (already decoded from JSON/TOML) into a validated EntityGraphIR.

Throws SdlValidationError with a precise JSON-path on the first problem.

final ir = parseSdl(jsonDecode(rawJson));

Implementation

EntityGraphIR parseSdl(Object? doc) {
  if (doc is! Map) {
    throw const SdlValidationError('SDL root must be an object', r'$');
  }

  final version = (doc['version'] as String?) ?? '1.0';

  final entitiesRaw = doc['entities'];
  if (entitiesRaw is! Map) {
    throw const SdlValidationError(
      '`entities` must be an object',
      r'$.entities',
    );
  }

  final entities = <IrEntity>[];

  for (final MapEntry(:key, :value) in entitiesRaw.entries) {
    final name = key as String;
    final path = r'$.entities.' + name;

    if (value is! Map) {
      throw SdlValidationError('entity must be an object', path);
    }
    final fieldsRaw = value['fields'];
    if (fieldsRaw is! Map) {
      throw SdlValidationError('entity must have a `fields` object', path);
    }

    final fields = <IrField>[];
    String? primaryKey;

    for (final MapEntry(:key, :value) in fieldsRaw.entries) {
      final fname = key as String;
      final fpath = '$path.fields.$fname';

      if (value is! Map) {
        throw SdlValidationError('field must be an object', fpath);
      }
      final rawType = value['type'];
      if (rawType is! String) {
        throw SdlValidationError('field must have a `type` string', fpath);
      }
      final scalarType = _parseScalar(rawType, fpath);

      final isEnum = scalarType == SdlScalarType.enumType;
      if (isEnum) {
        final vals = value['values'];
        if (vals is! List || (vals).isEmpty) {
          throw SdlValidationError(
            'enum field requires non-empty `values`',
            fpath,
          );
        }
      }

      final isPrimary = (value['primary'] as bool?) ?? false;
      if (isPrimary) {
        if (primaryKey != null) {
          throw SdlValidationError(
            'multiple primary keys not supported',
            fpath,
          );
        }
        primaryKey = fname;
      }

      fields.add(
        IrField(
          name: fname,
          type: scalarType,
          primary: isPrimary,
          required: (value['required'] as bool?) ?? false,
          unique: (value['unique'] as bool?) ?? false,
          auto: (value['auto'] as bool?) ?? false,
          defaultValue: value['default'],
          values: isEnum ? (value['values'] as List).cast<String>() : null,
        ),
      );
    }

    if (primaryKey == null) {
      throw SdlValidationError(
        'entity needs exactly one `primary: true` field',
        path,
      );
    }

    final relations = <IrRelation>[];
    final relationsRaw = value['relations'];
    if (relationsRaw is Map) {
      for (final MapEntry(:key, :value) in relationsRaw.entries) {
        final rname = key as String;
        final rpath = '$path.relations.$rname';
        if (value is! Map) {
          throw SdlValidationError('relation must be an object', rpath);
        }
        final rawKind = value['type'];
        if (rawKind is! String) {
          throw SdlValidationError('relation must have a `type`', rpath);
        }
        final target = value['target'];
        if (target is! String) {
          throw SdlValidationError('relation needs a `target`', rpath);
        }
        relations.add(
          IrRelation(
            name: rname,
            type: _parseRelationKind(rawKind, rpath),
            target: target,
            foreignKey: value['foreignKey'] as String?,
            through: value['through'] as String?,
          ),
        );
      }
    }

    entities.add(
      IrEntity(
        name: name,
        table: (value['table'] as String?) ?? name,
        primaryKey: primaryKey,
        fields: fields,
        relations: relations,
      ),
    );
  }

  // Cross-reference relation targets.
  final names = entities.map((e) => e.name).toSet();
  for (final entity in entities) {
    for (final relation in entity.relations) {
      if (!names.contains(relation.target)) {
        throw SdlValidationError(
          'relation target "${relation.target}" is not a defined entity',
          '\$.entities.${entity.name}.relations.${relation.name}',
        );
      }
    }
  }

  final configRaw = doc['config'];
  final config = _parseConfig(configRaw);

  return EntityGraphIR(version: version, entities: entities, config: config);
}