checkDataSafety function

List<ReleaseProblem> checkDataSafety(
  1. String csv, {
  2. String where = 'data safety',
})

checkDataSafetyFile over already-read csv.

Implementation

List<ReleaseProblem> checkDataSafety(
  String csv, {
  String where = 'data safety',
}) {
  final problems = <ReleaseProblem>[];

  final List<CsvRecord> records;
  try {
    records = parseCsvRecords(csv);
  } on FormatException catch (e) {
    return [ReleaseProblem(where, e.message)];
  }

  if (records.isEmpty) {
    return [
      ReleaseProblem(
        where,
        'the file is empty — export it again from the Play Console, Data '
        'safety → Export to CSV',
      ),
    ];
  }

  final header = records.first.fields;
  if (header.length != dataSafetyColumns.length ||
      !_sameOrder(header, dataSafetyColumns)) {
    return [
      ReleaseProblem(
        where,
        'the header row is not the one Play exports.\n'
        '    found:    ${header.join(' | ')}\n'
        '    expected: ${dataSafetyColumns.join(' | ')}',
      ),
    ];
  }

  // Every later check reads fields by position, so a short or long row is
  // reported here and skipped rather than being silently read as some other
  // row's data.
  final rows = <DataSafetyRow>[];
  for (var i = 1; i < records.length; i++) {
    final record = records[i].fields;
    final line = records[i].line;
    if (record.length != dataSafetyColumns.length) {
      problems.add(
        ReleaseProblem(
          '$where line $line',
          'has ${record.length} fields, and every row has to have '
              '${dataSafetyColumns.length} — a value containing a comma must be '
              'quoted',
        ),
      );
      continue;
    }
    rows.add(
      DataSafetyRow(
        line: line,
        questionId: record[0],
        responseId: record[1],
        responseValue: record[2],
        requirement: record[3],
        label: record[4],
      ),
    );
  }

  for (final row in rows) {
    if (row.questionId.isEmpty) {
      problems.add(
        ReleaseProblem('$where line ${row.line}', 'has no question id'),
      );
    }
    if (row.requirement.isEmpty) {
      problems.add(
        ReleaseProblem(
          '$where line ${row.line}',
          '${row.questionId} has no answer requirement — the column that says '
              'whether it needs an answer is blank',
        ),
      );
    }
    if (row.requirement == dataSafetyRequired && row.responseValue.isEmpty) {
      problems.add(
        ReleaseProblem(
          '$where line ${row.line}',
          '${row.questionId} is REQUIRED and has no response value — Play '
              'refuses the declaration, after the release has been committed',
        ),
      );
    }
  }

  problems.addAll(_duplicates(rows, where));

  if (rows.every((r) => r.requirement != dataSafetyRequired)) {
    problems.add(
      ReleaseProblem(
        where,
        'no REQUIRED question in ${rows.length} row(s), which a Play export '
        'always has — this is more likely a truncated or hand-assembled file '
        'than a declaration with nothing to answer',
      ),
    );
  }

  return problems;
}