flutter_manifest.dart 23 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:meta/meta.dart';
6
import 'package:pub_semver/pub_semver.dart';
7 8
import 'package:yaml/yaml.dart';

9
import 'base/deferred_component.dart';
10
import 'base/file_system.dart';
11
import 'base/logger.dart';
12
import 'base/user_messages.dart';
13
import 'base/utils.dart';
14
import 'plugins.dart';
15

16 17 18 19
const Set<String> _kValidPluginPlatforms = <String>{
  'android', 'ios', 'web', 'windows', 'linux', 'macos'
};

20
/// A wrapper around the `flutter` section in the `pubspec.yaml` file.
21
class FlutterManifest {
22
  FlutterManifest._({required Logger logger}) : _logger = logger;
23

24
  /// Returns an empty manifest.
25
  factory FlutterManifest.empty({ required Logger logger }) = FlutterManifest._;
26

27
  /// Returns null on invalid manifest. Returns empty manifest on missing file.
28 29 30
  static FlutterManifest? createFromPath(String path, {
    required FileSystem fileSystem,
    required Logger logger,
31 32 33
  }) {
    if (path == null || !fileSystem.isFileSync(path)) {
      return _createFromYaml(null, logger);
34
    }
35 36
    final String manifest = fileSystem.file(path).readAsStringSync();
    return FlutterManifest.createFromString(manifest, logger: logger);
37
  }
38

39
  /// Returns null on missing or invalid manifest.
40
  @visibleForTesting
41
  static FlutterManifest? createFromString(String manifest, { required Logger logger }) {
42
    return _createFromYaml(manifest != null ? loadYaml(manifest) : null, logger);
43 44
  }

45
  static FlutterManifest? _createFromYaml(Object? yamlDocument, Logger logger) {
46
    if (yamlDocument != null && !_validate(yamlDocument, logger)) {
47
      return null;
48
    }
49

50 51
    final FlutterManifest pubspec = FlutterManifest._(logger: logger);
    final Map<Object?, Object?>? yamlMap = yamlDocument as YamlMap?;
52
    if (yamlMap != null) {
53
      pubspec._descriptor = yamlMap.cast<String, Object?>();
54 55
    }

56
    final Map<Object?, Object?>? flutterMap = pubspec._descriptor['flutter'] as Map<Object?, Object?>?;
57
    if (flutterMap != null) {
58
      pubspec._flutterDescriptor = flutterMap.cast<String, Object?>();
59 60
    }

61 62 63
    return pubspec;
  }

64 65
  final Logger _logger;

66
  /// A map representation of the entire `pubspec.yaml` file.
67
  Map<String, Object?> _descriptor = <String, Object?>{};
68 69

  /// A map representation of the `flutter` section in the `pubspec.yaml` file.
70
  Map<String, Object?> _flutterDescriptor = <String, Object?>{};
71

72
  /// True if the `pubspec.yaml` file does not exist.
73 74
  bool get isEmpty => _descriptor.isEmpty;

75
  /// The string value of the top-level `name` property in the `pubspec.yaml` file.
76
  String get appName => _descriptor['name'] as String? ?? '';
77

78 79 80 81 82 83 84
  /// Contains the name of the dependencies.
  /// These are the keys specified in the `dependency` map.
  Set<String> get dependencies {
    final YamlMap? dependencies = _descriptor['dependencies'] as YamlMap?;
    return dependencies != null ? <String>{...dependencies.keys.cast<String>()} : <String>{};
  }

85 86 87
  // Flag to avoid printing multiple invalid version messages.
  bool _hasShowInvalidVersionMsg = false;

88 89
  /// The version String from the `pubspec.yaml` file.
  /// Can be null if it isn't set or has a wrong format.
90 91
  String? get appVersion {
    final String? verStr = _descriptor['version']?.toString();
92 93 94 95
    if (verStr == null) {
      return null;
    }

96
    Version? version;
97 98 99 100
    try {
      version = Version.parse(verStr);
    } on Exception {
      if (!_hasShowInvalidVersionMsg) {
101
        _logger.printStatus(userMessages.invalidVersionSettingHintMessage(verStr), emphasis: true);
102 103 104
        _hasShowInvalidVersionMsg = true;
      }
    }
105
    return version?.toString();
106 107 108 109
  }

  /// The build version name from the `pubspec.yaml` file.
  /// Can be null if version isn't set or has a wrong format.
110 111 112 113
  String? get buildName {
    final String? version = appVersion;
    if (version != null && version.contains('+')) {
      return version.split('+').elementAt(0);
114
    }
115
    return version;
116 117 118 119
  }

  /// The build version number from the `pubspec.yaml` file.
  /// Can be null if version isn't set or has a wrong format.
120 121 122 123
  String? get buildNumber {
    final String? version = appVersion;
    if (version != null && version.contains('+')) {
      final String value = version.split('+').elementAt(1);
124
      return value;
125 126 127 128 129
    } else {
      return null;
    }
  }

130
  bool get usesMaterialDesign {
131
    return _flutterDescriptor['uses-material-design'] as bool? ?? false;
132 133
  }

134 135 136 137
  /// True if this Flutter module should use AndroidX dependencies.
  ///
  /// If false the deprecated Android Support library will be used.
  bool get usesAndroidX {
138 139 140
    final Object? module = _flutterDescriptor['module'];
    if (module is YamlMap) {
      return module['androidX'] == true;
141 142
    }
    return false;
143 144
  }

145 146 147 148 149 150 151 152 153 154 155 156
  /// Any additional license files listed under the `flutter` key.
  ///
  /// This is expected to be a list of file paths that should be treated as
  /// relative to the pubspec in this directory.
  ///
  /// For example:
  ///
  /// ```yaml
  /// flutter:
  ///   licenses:
  ///     - assets/foo_license.txt
  /// ```
157 158 159 160 161 162 163
  List<String> get additionalLicenses {
    final Object? licenses = _flutterDescriptor['licenses'];
    if (licenses is YamlList) {
      return licenses.map((Object? element) => element.toString()).toList();
    }
    return <String>[];
  }
164

165
  /// True if this manifest declares a Flutter module project.
166
  ///
167 168 169
  /// A Flutter project is considered a module when it has a `module:`
  /// descriptor. A Flutter module project supports integration into an
  /// existing host app, and has managed platform host code.
170
  ///
171 172
  /// Such a project can be created using `flutter create -t module`.
  bool get isModule => _flutterDescriptor.containsKey('module');
173 174 175 176 177 178 179 180 181 182 183 184

  /// True if this manifest declares a Flutter plugin project.
  ///
  /// A Flutter project is considered a plugin when it has a `plugin:`
  /// descriptor. A Flutter plugin project wraps custom Android and/or
  /// iOS code in a Dart interface for consumption by other Flutter app
  /// projects.
  ///
  /// Such a project can be created using `flutter create -t plugin`.
  bool get isPlugin => _flutterDescriptor.containsKey('plugin');

  /// Returns the Android package declared by this manifest in its
185
  /// module or plugin descriptor. Returns null, if there is no
186
  /// such declaration.
187
  String? get androidPackage {
188
    if (isModule) {
189 190 191 192
      final Object? module = _flutterDescriptor['module'];
      if (module is YamlMap) {
        return module['androidPackage'] as String?;
      }
193
    }
194 195
    final Map<String, Object?>? platforms = supportedPlatforms;
    if (platforms == null) {
196 197
      // Pre-multi-platform plugin format
      if (isPlugin) {
198 199
        final YamlMap? plugin = _flutterDescriptor['plugin'] as YamlMap?;
        return plugin?['androidPackage'] as String?;
200
      }
201 202
      return null;
    }
203 204 205 206 207
    if (platforms.containsKey('android')) {
      final Object? android = platforms['android'];
      if (android is YamlMap) {
        return android['package'] as String?;
      }
208
    }
209 210
    return null;
  }
211

212 213
  /// Returns the deferred components configuration if declared. Returns
  /// null if no deferred components are declared.
214 215
  late final List<DeferredComponent>? deferredComponents = computeDeferredComponents();
  List<DeferredComponent>? computeDeferredComponents() {
216 217 218 219
    if (!_flutterDescriptor.containsKey('deferred-components')) {
      return null;
    }
    final List<DeferredComponent> components = <DeferredComponent>[];
220 221
    final Object? deferredComponents = _flutterDescriptor['deferred-components'];
    if (deferredComponents is! YamlList) {
222 223
      return components;
    }
224 225 226 227 228
    for (final Object? component in deferredComponents) {
      if (component is! YamlMap) {
        _logger.printError('Expected deferred component manifest to be a map.');
        continue;
      }
229
      List<Uri> assetsUri = <Uri>[];
230
      final List<Object?>? assets = component['assets'] as List<Object?>?;
231 232 233
      if (assets == null) {
        assetsUri = const <Uri>[];
      } else {
234
        for (final Object? asset in assets) {
235 236 237 238 239
          if (asset is! String || asset == null || asset == '') {
            _logger.printError('Deferred component asset manifest contains a null or empty uri.');
            continue;
          }
          try {
240
            assetsUri.add(Uri.parse(asset));
241 242 243 244 245 246 247 248 249
          } on FormatException {
            _logger.printError('Asset manifest contains invalid uri: $asset.');
          }
        }
      }
      components.add(
        DeferredComponent(
          name: component['name'] as String,
          libraries: component['libraries'] == null ?
250
              <String>[] : (component['libraries'] as List<dynamic>).cast<String>(),
251 252 253 254 255 256 257
          assets: assetsUri,
        )
      );
    }
    return components;
  }

258
  /// Returns the iOS bundle identifier declared by this manifest in its
259
  /// module descriptor. Returns null if there is no such declaration.
260
  String? get iosBundleIdentifier {
261
    if (isModule) {
262 263 264 265
      final Object? module = _flutterDescriptor['module'];
      if (module is YamlMap) {
        return module['iosBundleIdentifier'] as String?;
      }
266
    }
267 268 269
    return null;
  }

270 271 272
  /// Gets the supported platforms. This only supports the new `platforms` format.
  ///
  /// If the plugin uses the legacy pubspec format, this method returns null.
273
  Map<String, Object?>? get supportedPlatforms {
274
    if (isPlugin) {
275 276 277 278
      final YamlMap? plugin = _flutterDescriptor['plugin'] as YamlMap?;
      if (plugin?.containsKey('platforms') == true) {
        final YamlMap? platformsMap = plugin!['platforms'] as YamlMap?;
        return platformsMap?.value.cast<String, Object?>();
279 280 281 282 283
      }
    }
    return null;
  }

284
  /// Like [supportedPlatforms], but only returns the valid platforms that are supported in flutter plugins.
285 286
  Map<String, Object?>? get validSupportedPlatforms {
    final Map<String, Object?>? allPlatforms = supportedPlatforms;
287 288 289
    if (allPlatforms == null) {
      return null;
    }
290 291
    final Map<String, Object?> platforms = <String, Object?>{}..addAll(allPlatforms);
    platforms.removeWhere((String key, Object? _) => !_kValidPluginPlatforms.contains(key));
292 293 294 295 296 297
    if (platforms.isEmpty) {
      return null;
    }
    return platforms;
  }

298
  List<Map<String, Object?>> get fontsDescriptor {
299 300 301
    return fonts.map((Font font) => font.descriptor).toList();
  }

302 303
  List<Map<String, Object?>> get _rawFontsDescriptor {
    final List<Object?>? fontList = _flutterDescriptor['fonts'] as List<Object?>?;
304
    return fontList == null
305 306
        ? const <Map<String, Object?>>[]
        : fontList.map<Map<String, Object?>?>(castStringKeyedMap).whereType<Map<String, Object?>>().toList();
307 308
  }

309
  late final List<Uri> assets = _computeAssets();
310
  List<Uri> _computeAssets() {
311
    final List<Object?>? assets = _flutterDescriptor['assets'] as List<Object?>?;
312 313 314
    if (assets == null) {
      return const <Uri>[];
    }
315
    final List<Uri> results = <Uri>[];
316
    for (final Object? asset in assets) {
317
      if (asset is! String || asset == null || asset == '') {
318
        _logger.printError('Asset manifest contains a null or empty uri.');
319 320 321
        continue;
      }
      try {
322
        results.add(Uri(pathSegments: asset.split('/')));
323
      } on FormatException {
324
        _logger.printError('Asset manifest contains invalid uri: $asset.');
325 326 327
      }
    }
    return results;
328 329
  }

330
  late final List<Font> fonts = _extractFonts();
331 332

  List<Font> _extractFonts() {
333
    if (!_flutterDescriptor.containsKey('fonts')) {
334
      return <Font>[];
335
    }
336 337

    final List<Font> fonts = <Font>[];
338 339 340
    for (final Map<String, Object?> fontFamily in _rawFontsDescriptor) {
      final YamlList? fontFiles = fontFamily['fonts'] as YamlList?;
      final String? familyName = fontFamily['family'] as String?;
341
      if (familyName == null) {
342
        _logger.printError('Warning: Missing family name for font.', emphasis: true);
343 344 345
        continue;
      }
      if (fontFiles == null) {
346
        _logger.printError('Warning: No fonts specified for font $familyName', emphasis: true);
347 348 349 350
        continue;
      }

      final List<FontAsset> fontAssets = <FontAsset>[];
351 352
      for (final Map<Object?, Object?> fontFile in fontFiles.cast<Map<Object?, Object?>>()) {
        final String? asset = fontFile['asset'] as String?;
353
        if (asset == null) {
354
          _logger.printError('Warning: Missing asset in fonts for $familyName', emphasis: true);
355 356 357
          continue;
        }

358
        fontAssets.add(FontAsset(
359
          Uri.parse(asset),
360 361
          weight: fontFile['weight'] as int?,
          style: fontFile['style'] as String?,
362 363
        ));
      }
364
      if (fontAssets.isNotEmpty) {
365
        fonts.add(Font(familyName, fontAssets));
366
      }
367 368 369
    }
    return fonts;
  }
370 371 372 373 374 375 376 377

  /// Whether a synthetic flutter_gen package should be generated.
  ///
  /// This can be provided to the [Pub] interface to inject a new entry
  /// into the package_config.json file which points to `.dart_tool/flutter_gen`.
  ///
  /// This allows generated source code to be imported using a package
  /// alias.
378
  late final bool generateSyntheticPackage = _computeGenerateSyntheticPackage();
379 380 381 382
  bool _computeGenerateSyntheticPackage() {
    if (!_flutterDescriptor.containsKey('generate')) {
      return false;
    }
383
    final Object? value = _flutterDescriptor['generate'];
384 385 386
    if (value is! bool) {
      return false;
    }
387
    return value;
388
  }
389 390 391
}

class Font {
392 393 394 395
  Font(this.familyName, this.fontAssets)
    : assert(familyName != null),
      assert(fontAssets != null),
      assert(fontAssets.isNotEmpty);
396 397 398 399

  final String familyName;
  final List<FontAsset> fontAssets;

400 401
  Map<String, Object?> get descriptor {
    return <String, Object?>{
402
      'family': familyName,
403
      'fonts': fontAssets.map<Map<String, Object?>>((FontAsset a) => a.descriptor).toList(),
404 405 406 407 408 409 410 411
    };
  }

  @override
  String toString() => '$runtimeType(family: $familyName, assets: $fontAssets)';
}

class FontAsset {
412 413
  FontAsset(this.assetUri, {this.weight, this.style})
    : assert(assetUri != null);
414

415
  final Uri assetUri;
416 417
  final int? weight;
  final String? style;
418

419 420
  Map<String, Object?> get descriptor {
    final Map<String, Object?> descriptor = <String, Object?>{};
421
    if (weight != null) {
422
      descriptor['weight'] = weight;
423
    }
424

425
    if (style != null) {
426
      descriptor['style'] = style;
427
    }
428

429
    descriptor['asset'] = assetUri.path;
430 431 432 433
    return descriptor;
  }

  @override
434
  String toString() => '$runtimeType(asset: ${assetUri.path}, weight; $weight, style: $style)';
435 436
}

437

438
bool _validate(Object? manifest, Logger logger) {
439
  final List<String> errors = <String>[];
440 441 442
  if (manifest is! YamlMap) {
    errors.add('Expected YAML map');
  } else {
443
    for (final MapEntry<Object?, Object?> kvp in manifest.entries) {
444 445 446 447
      if (kvp.key is! String) {
        errors.add('Expected YAML key to be a string, but got ${kvp.key}.');
        continue;
      }
448
      switch (kvp.key as String?) {
449 450 451 452 453 454 455 456 457 458 459 460
        case 'name':
          if (kvp.value is! String) {
            errors.add('Expected "${kvp.key}" to be a string, but got ${kvp.value}.');
          }
          break;
        case 'flutter':
          if (kvp.value == null) {
            continue;
          }
          if (kvp.value is! YamlMap) {
            errors.add('Expected "${kvp.key}" section to be an object or null, but got ${kvp.value}.');
          } else {
461
            _validateFlutter(kvp.value as YamlMap?, errors);
462 463 464
          }
          break;
        default:
465
        // additionalProperties are allowed.
466 467
          break;
      }
468 469
    }
  }
470

471
  if (errors.isNotEmpty) {
472 473
    logger.printStatus('Error detected in pubspec.yaml:', emphasis: true);
    logger.printError(errors.join('\n'));
474 475
    return false;
  }
476 477 478 479

  return true;
}

480
void _validateFlutter(YamlMap? yaml, List<String> errors) {
481 482 483
  if (yaml == null || yaml.entries == null) {
    return;
  }
484 485 486 487 488
  for (final MapEntry<Object?, Object?> kvp in yaml.entries) {
    final Object? yamlKey = kvp.key;
    final Object? yamlValue = kvp.value;
    if (yamlKey is! String) {
      errors.add('Expected YAML key to be a string, but got $yamlKey (${yamlValue.runtimeType}).');
489 490
      continue;
    }
491
    switch (yamlKey) {
492
      case 'uses-material-design':
493 494
        if (yamlValue is! bool) {
          errors.add('Expected "$yamlKey" to be a bool, but got $yamlValue (${yamlValue.runtimeType}).');
495 496 497
        }
        break;
      case 'assets':
498 499
        if (yamlValue is! YamlList) {

500
          errors.add('Expected "$yamlKey" to be a list, but got $yamlValue (${yamlValue.runtimeType}).');
501 502 503 504 505 506
        } else if (yamlValue.isEmpty) {
          break;
        } else if (yamlValue[0] is! String) {
          errors.add(
            'Expected "$yamlKey" to be a list of strings, but the first element is $yamlValue (${yamlValue.runtimeType}).',
          );
507 508 509
        }
        break;
      case 'fonts':
510
        if (yamlValue is! YamlList) {
511
          errors.add('Expected "$yamlKey" to be a list, but got $yamlValue (${yamlValue.runtimeType}).');
512 513 514 515 516 517
        } else if (yamlValue.isEmpty) {
          break;
        } else if (yamlValue.first is! YamlMap) {
          errors.add(
            'Expected "$yamlKey" to contain maps, but the first element is $yamlValue (${yamlValue.runtimeType}).',
          );
518
        } else {
519
          _validateFonts(yamlValue, errors);
520 521
        }
        break;
522
      case 'licenses':
523
        if (yamlValue is! YamlList) {
524
          errors.add('Expected "$yamlKey" to be a list of files, but got $yamlValue (${yamlValue.runtimeType})');
525 526 527 528 529 530 531 532
        } else if (yamlValue.isEmpty) {
          break;
        } else if (yamlValue.first is! String) {
          errors.add(
            'Expected "$yamlKey" to contain strings, but the first element is $yamlValue (${yamlValue.runtimeType}).',
          );
        } else {
          _validateListType<String>(yamlValue, errors, '"$yamlKey"', 'files');
533 534
        }
        break;
535
      case 'module':
536 537 538
        if (yamlValue is! YamlMap) {
          errors.add('Expected "$yamlKey" to be an object, but got $yamlValue (${yamlValue.runtimeType}).');
          break;
539 540
        }

541
        if (yamlValue['androidX'] != null && yamlValue['androidX'] is! bool) {
542 543
          errors.add('The "androidX" value must be a bool if set.');
        }
544
        if (yamlValue['androidPackage'] != null && yamlValue['androidPackage'] is! String) {
545 546
          errors.add('The "androidPackage" value must be a string if set.');
        }
547
        if (yamlValue['iosBundleIdentifier'] != null && yamlValue['iosBundleIdentifier'] is! String) {
548 549 550 551
          errors.add('The "iosBundleIdentifier" section must be a string if set.');
        }
        break;
      case 'plugin':
552 553
        if (yamlValue is! YamlMap || yamlValue == null) {
          errors.add('Expected "$yamlKey" to be an object, but got $yamlValue (${yamlValue.runtimeType}).');
554
          break;
555
        }
556
        final List<String> pluginErrors = Plugin.validatePluginYaml(yamlValue);
557
        errors.addAll(pluginErrors);
558
        break;
559 560
      case 'generate':
        break;
561 562 563
      case 'deferred-components':
        _validateDeferredComponents(kvp, errors);
        break;
564
      default:
565
        errors.add('Unexpected child "$yamlKey" found under "flutter".');
566 567 568
        break;
    }
  }
569
}
570

571
void _validateListType<T>(YamlList yamlList, List<String> errors, String context, String typeAlias) {
572 573
  for (int i = 0; i < yamlList.length; i++) {
    if (yamlList[i] is! T) {
574
      // ignore: avoid_dynamic_calls
575 576 577 578 579
      errors.add('Expected $context to be a list of $typeAlias, but element $i was a ${yamlList[i].runtimeType}');
    }
  }
}

580 581 582 583 584 585 586 587
void _validateDeferredComponents(MapEntry<Object?, Object?> kvp, List<String> errors) {
  final Object? yamlList = kvp.value;
  if (yamlList != null && (yamlList is! YamlList || yamlList[0] is! YamlMap)) {
    errors.add('Expected "${kvp.key}" to be a list, but got $yamlList (${yamlList.runtimeType}).');
  } else if (yamlList is YamlList) {
    for (int i = 0; i < yamlList.length; i++) {
      final Object? valueMap = yamlList[i];
      if (valueMap is! YamlMap) {
588
        // ignore: avoid_dynamic_calls
589
        errors.add('Expected the $i element in "${kvp.key}" to be a map, but got ${yamlList[i]} (${yamlList[i].runtimeType}).');
590 591
        continue;
      }
592
      if (!valueMap.containsKey('name') || valueMap['name'] is! String) {
593 594
        errors.add('Expected the $i element in "${kvp.key}" to have required key "name" of type String');
      }
595 596 597 598
      if (valueMap.containsKey('libraries')) {
        final Object? libraries = valueMap['libraries'];
        if (libraries is! YamlList) {
          errors.add('Expected "libraries" key in the $i element of "${kvp.key}" to be a list, but got $libraries (${libraries.runtimeType}).');
599
        } else {
600
          _validateListType<String>(libraries, errors, '"libraries" key in the $i element of "${kvp.key}"', 'dart library Strings');
601 602
        }
      }
603 604 605 606
      if (valueMap.containsKey('assets')) {
        final Object? assets = valueMap['assets'];
        if (assets is! YamlList) {
          errors.add('Expected "assets" key in the $i element of "${kvp.key}" to be a list, but got $assets (${assets.runtimeType}).');
607
        } else {
608
          _validateListType<String>(assets, errors, '"assets" key in the $i element of "${kvp.key}"', 'file paths');
609 610
        }
      }
611 612 613 614
    }
  }
}

615 616 617 618
void _validateFonts(YamlList fonts, List<String> errors) {
  if (fonts == null) {
    return;
  }
619
  const Set<int> fontWeights = <int>{
620
    100, 200, 300, 400, 500, 600, 700, 800, 900,
621
  };
622 623 624
  for (final Object? fontMap in fonts) {
    if (fontMap is! YamlMap) {
      errors.add('Unexpected child "$fontMap" found under "fonts". Expected a map.');
625 626
      continue;
    }
627
    for (final Object? key in fontMap.keys.where((Object? key) => key != 'family' && key != 'fonts')) {
628 629 630 631 632 633 634
      errors.add('Unexpected child "$key" found under "fonts".');
    }
    if (fontMap['family'] != null && fontMap['family'] is! String) {
      errors.add('Font family must either be null or a String.');
    }
    if (fontMap['fonts'] == null) {
      continue;
635 636 637
    } else if (fontMap['fonts'] is! YamlList) {
      errors.add('Expected "fonts" to either be null or a list.');
      continue;
638
    }
639 640
    for (final Object? fontMapList in fontMap['fonts']) {
      if (fontMapList is! YamlMap) {
641 642 643
        errors.add('Expected "fonts" to be a list of maps.');
        continue;
      }
644 645 646 647
      for (final MapEntry<Object?, Object?> kvp in fontMapList.entries) {
        final Object? fontKey = kvp.key;
        if (fontKey is! String) {
          errors.add('Expected "$fontKey" under "fonts" to be a string.');
648
        }
649
        switch(fontKey) {
650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665
          case 'asset':
            if (kvp.value is! String) {
              errors.add('Expected font asset ${kvp.value} ((${kvp.value.runtimeType})) to be a string.');
            }
            break;
          case 'weight':
            if (!fontWeights.contains(kvp.value)) {
              errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> weight.');
            }
            break;
          case 'style':
            if (kvp.value != 'normal' && kvp.value != 'italic') {
              errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> style.');
            }
            break;
          default:
666
            errors.add('Unexpected key $fontKey ((${kvp.value.runtimeType})) under font.');
667 668 669 670 671
            break;
        }
      }
    }
  }
672
}