dvStudioDefinitionProblems function
List<String>
dvStudioDefinitionProblems(
- DVStudioModelSpec spec, {
- required Map<
String, DVStudioModelSpec> others,
Why spec cannot be stored as a model designed in Studio, or an empty
list when it can. others is every other model the application has, by
the name Studio lists it under.
Implementation
List<String> dvStudioDefinitionProblems(
DVStudioModelSpec spec, {
required Map<String, DVStudioModelSpec> others,
}) {
final List<String> problems = <String>[];
final String name = spec.model;
if (!_modelName.hasMatch(name)) {
problems.add(
'A data model is named in PascalCase, starting with a capital letter: '
'Article, BlogPost.',
);
} else if (_reservedModelNames.contains(name) || name.startsWith('DV')) {
problems.add('$name is a name Dartvel already uses. Choose another.');
}
if (spec.table != dvStudioTableFor(name)) {
problems.add('$name keeps its records in ${dvStudioTableFor(name)}.');
}
if (spec.fields.isEmpty) {
problems.add('A data model needs at least one field.');
}
final Set<String> seen = <String>{};
for (final DVStudioFieldSpec field in spec.fields) {
final String label = field.name.isEmpty ? 'A field' : field.name;
if (!_fieldName.hasMatch(field.name)) {
problems.add(
'$label: a field is named in camelCase, starting with a lowercase '
'letter: title, publishedAt.',
);
} else if (_reservedFieldNames.contains(field.name)) {
problems.add('${field.name} is a word Dart or Dartvel keeps for itself.');
}
if (!seen.add(field.name)) {
problems.add('There are two fields named ${field.name}.');
}
final String base = dvStudioBaseType(field.type);
final List<String>? options = field.options;
if (options != null) {
if (!_modelName.hasMatch(base) || base == name) {
problems.add(
'$label: a field with choices names its choice type in PascalCase, '
'as ${name}Status.',
);
}
if (options.isEmpty) problems.add('$label: list at least one choice.');
for (final String option in options) {
if (!_fieldName.hasMatch(option) ||
_reservedFieldNames.contains(option)) {
problems.add(
'$label: the choice "$option" has to be one camelCase word, '
'as draft or inReview.',
);
}
}
if (options.toSet().length != options.length) {
problems.add('$label lists the same choice twice.');
}
} else if (!dvStudioFieldTypes.contains(base)) {
problems.add(
'$label: ${field.type} is not a type Studio can store. Choose one of '
'${dvStudioFieldTypes.join(', ')}.',
);
}
final String? relation = field.relation;
if (relation != null) {
final DVStudioModelSpec? target =
relation == name ? spec : others[relation];
if (target == null) {
problems.add('$label refers to $relation, and there is no such model.');
} else {
if (base != 'String' && base != 'int') {
problems.add('$label holds a key of $relation, so it is a String.');
}
// The generator finds a relation by its name, `authorId` holding an
// Author's id, so a model written out keeps its relations only if
// the field is named that way.
final String stem = '${relation[0].toLowerCase()}${relation.substring(1)}';
if (!RegExp('^$stem(Id|Slug|Key)\$').hasMatch(field.name)) {
problems.add(
'$label refers to $relation, so it is named ${stem}Id, '
'${stem}Slug or ${stem}Key.',
);
}
}
}
if (field.sensitive) {
problems.add(
'$label: a sensitive field is declared in code, where its encryption '
'and who may read it are decided.',
);
}
if (field.min != null && field.max != null && field.min! > field.max!) {
problems.add('$label: the smallest value is larger than the largest.');
}
if (field.minLength != null &&
field.maxLength != null &&
field.minLength! > field.maxLength!) {
problems.add('$label: the shortest length is longer than the longest.');
}
if ((field.min != null || field.max != null) &&
!const <String>{'int', 'double', 'num'}.contains(base)) {
problems.add('$label: only a number has a smallest or largest value.');
}
if ((field.minLength != null ||
field.maxLength != null ||
field.pattern != null) &&
base != 'String') {
problems.add('$label: only text has a length or a pattern.');
}
if (field.pattern != null) {
final String pattern = field.pattern!;
try {
RegExp(pattern);
} on FormatException catch (error) {
problems.add('$label: the pattern is not valid: ${error.message}');
}
// A pattern is run against whatever a caller of the data API sends.
// One that repeats a repetition -- (a+)+ -- can take the server
// exponential time on a string built to make it, so it is refused
// here rather than discovered there.
if (pattern.length > dvStudioPatternLimit) {
problems.add(
'$label: a pattern is at most $dvStudioPatternLimit characters.',
);
} else if (_nestedRepeat.hasMatch(pattern)) {
problems.add(
'$label: the pattern repeats something that already repeats, which '
'can take the server a very long time to check. Simplify it.',
);
}
}
}
final DVStudioFieldSpec? key =
spec.fields.where((DVStudioFieldSpec f) => f.name == spec.key).firstOrNull;
if (key == null) {
problems.add('The key, ${spec.key}, has to be one of the fields.');
} else {
if (key.type != 'String') {
problems.add(
'The key, ${key.name}, is a String that is never empty: records are '
'found by it.',
);
}
// The key the generator would choose, so the model written out finds its
// records by the same field.
final List<String> texts = <String>[
for (final DVStudioFieldSpec f in spec.fields)
if (f.type == 'String') f.name,
];
final String? generated = texts.contains('slug')
? 'slug'
: texts.contains('id')
? 'id'
: texts.firstOrNull;
if (generated != null && generated != spec.key && key.type == 'String') {
problems.add(
'A model with a $generated field is found by its $generated. Make '
'$generated the key, or rename it.',
);
}
}
for (final DVStudioIndexSpec index in spec.indexes) {
if (index.fields.isEmpty) problems.add('An index names no field.');
for (final String field in index.fields) {
if (!seen.contains(field)) {
problems.add('An index names $field, which is not a field.');
}
}
}
return problems;
}