parseSdl function
- 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);
}