loadMetadata function

AppStoreMetadata loadMetadata(
  1. String path
)

Loads and fully validates the tree rooted at path.

Implementation

AppStoreMetadata loadMetadata(String path) {
  final root = Directory(path);
  if (!root.existsSync()) {
    throw MetadataException('no such metadata directory: $path');
  }

  final metadata = AppStoreMetadata();
  final separator = Platform.pathSeparator;

  final info = Directory('${root.path}${separator}info');
  if (info.existsSync()) {
    const categoryFields = {
      'primary_category': 'primaryCategory',
      'secondary_category': 'secondaryCategory',
    };
    for (final field in categoryFields.entries) {
      final value = _readField(info, field.key);
      if (value != null && value.isNotEmpty) {
        // Apple's ids are SCREAMING_SNAKE_CASE and a lowercase one is silently
        // ignored rather than rejected, which is the worst of both.
        if (value != value.toUpperCase()) {
          throw MetadataException(
            'info/${field.key}.txt is "$value"; App Store category ids are '
            'upper case, e.g. HEALTH_AND_FITNESS',
          );
        }
        metadata.categories[field.value] = value;
      }
    }

    final contentRights = _readField(info, 'content_rights');
    if (contentRights != null && contentRights.isNotEmpty) {
      if (!contentRightsDeclarations.contains(contentRights)) {
        throw MetadataException(
          'info/content_rights.txt is "$contentRights"; Apple accepts '
          '${contentRightsDeclarations.join(" or ")}',
        );
      }
      metadata.contentRights = contentRights;
    }

    final copyright = _readField(info, 'copyright');
    if (copyright != null && copyright.isNotEmpty) {
      // Apple's own guidance is "the year the rights were obtained, then the
      // name of the person or entity" — it renders the © itself, so a string
      // that carries one ends up showing two.
      if (copyright.contains('©')) {
        throw MetadataException(
          'info/copyright.txt contains a © symbol; Apple adds one itself, so '
          'write just "2026 Example Inc."',
        );
      }
      _checkLength('info', 'copyright.txt', copyright, 200);
      metadata.copyright = copyright;
    }
  }

  final ageRating = File('${root.path}${separator}age-rating.json');
  if (ageRating.existsSync()) {
    metadata.ageRating = _loadAgeRating(ageRating);
  }

  final reviewNotes = File('${root.path}${separator}review-notes.md');
  if (reviewNotes.existsSync()) {
    metadata.reviewNotes = readReviewNotes(reviewNotes);
  }

  final listings = Directory('${root.path}${separator}listings');
  if (listings.existsSync()) {
    final localeDirs = listings.listSync().whereType<Directory>().toList()
      ..sort((a, b) => a.path.compareTo(b.path));
    for (final dir in localeDirs) {
      final locale = _loadLocale(dir);
      if (!locale.isEmpty) {
        metadata.locales.add(locale);
      }
    }
  }

  if (metadata.categories.isEmpty &&
      metadata.locales.isEmpty &&
      metadata.ageRating == null &&
      metadata.reviewNotes == null &&
      metadata.contentRights == null &&
      metadata.copyright == null) {
    throw MetadataException(
      '$path holds no info/, no listings/ and no age-rating.json — nothing to '
      'publish',
    );
  }
  return metadata;
}