dvStudioDefinitionProblems function

List<String> dvStudioDefinitionProblems(
  1. DVStudioModelSpec spec, {
  2. 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;
}