// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import 'package:aft/src/models.dart'; import 'package:aws_common/aws_common.dart'; import 'package:collection/collection.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:smithy/ast.dart'; part 'raw_config.g.dart'; const yamlSerializable = JsonSerializable( anyMap: true, checked: true, disallowUnrecognizedKeys: true, explicitToJson: true, converters: [ VersionConstraintConverter(), PackageSelectorConverter(), ShapeIdConverter(), PubspecConverter(), ], ); /// The typed representation of the `aft.yaml` file. @yamlSerializable class RawAftConfig with AWSSerializable<Map<String, Object?>>, AWSDebuggable { const RawAftConfig({ this.dependencies = const {}, required this.environment, this.ignore = const [], this.components = const [], this.scripts = const {}, }); factory RawAftConfig.fromJson(Map<Object?, Object?>? json) => _$RawAftConfigFromJson(json ?? const {}); /// Global dependency versions for third-party dependencies representing the /// values which have been vetted by manual review and/or those should be used /// consistently across all packages. final Map<String, VersionConstraint> dependencies; /// The current constraints for Dart + Flutter SDKs. final Environment environment; /// Packages to ignore in all repo operations. final List<String> ignore; /// {@macro aft.models.aft_component} final List<RawAftComponent> components; final Map<String, AftScript> scripts; /// Retrieves the component for [packageName], if any. String componentForPackage(String packageName) { return components .firstWhereOrNull( (component) => component.packages.contains(packageName), ) ?.name ?? packageName; } @override String get runtimeTypeName => 'RawAftConfig'; @override Map<String, Object?> toJson() => _$RawAftConfigToJson(this); } @yamlSerializable class Environment with AWSSerializable<Map<String, Object?>>, AWSDebuggable { const Environment({ required this.sdk, required this.flutter, required this.android, required this.ios, required this.macOS, }); factory Environment.fromJson(Map<String, Object?> json) => _$EnvironmentFromJson(json); final VersionConstraint sdk; final VersionConstraint flutter; final AndroidEnvironment android; final IosEnvironment ios; final MacOSEnvironment macOS; @override String get runtimeTypeName => 'Environment'; @override Map<String, Object?> toJson() => _$EnvironmentToJson(this); } @yamlSerializable class AndroidEnvironment with AWSSerializable<Map<String, Object?>>, AWSDebuggable { const AndroidEnvironment({ required this.minSdkVersion, }); factory AndroidEnvironment.fromJson(Map<String, Object?> json) => _$AndroidEnvironmentFromJson(json); final String minSdkVersion; @override String get runtimeTypeName => 'AndroidEnvironment'; @override Map<String, Object?> toJson() => _$AndroidEnvironmentToJson(this); } @yamlSerializable class IosEnvironment with AWSSerializable<Map<String, Object?>>, AWSDebuggable { const IosEnvironment({ required this.minOSVersion, }); factory IosEnvironment.fromJson(Map<String, Object?> json) => _$IosEnvironmentFromJson(json); final String minOSVersion; @override String get runtimeTypeName => 'IosEnvironment'; @override Map<String, Object?> toJson() => _$IosEnvironmentToJson(this); } @yamlSerializable class MacOSEnvironment with AWSSerializable<Map<String, Object?>>, AWSDebuggable { const MacOSEnvironment({ required this.minOSVersion, }); factory MacOSEnvironment.fromJson(Map<String, Object?> json) => _$MacOSEnvironmentFromJson(json); final String minOSVersion; @override String get runtimeTypeName => 'MacOSEnvironment'; @override Map<String, Object?> toJson() => _$MacOSEnvironmentToJson(this); } /// Specifies how to propagate version changes within a component. enum VersionPropagation { /// Propagates only major version changes. major, /// Propagates only minor version changes. minor, /// Propagates all version changes. all, /// Propagates no version changes. none; /// Whether to propagate a version change from [oldVersion] to [newVersion] /// within its component. bool propagateToComponent(Version oldVersion, Version newVersion) { if (oldVersion == newVersion) { return false; } final majorVersionChanged = () { if (newVersion.isPreRelease) { if (oldVersion.isPreRelease) { return newVersion == oldVersion.nextPreRelease; } return true; } return newVersion.major > oldVersion.major; }(); switch (this) { case VersionPropagation.major: return majorVersionChanged; case VersionPropagation.minor: if (majorVersionChanged) { return true; } return newVersion.minor > oldVersion.minor; case VersionPropagation.all: return true; case VersionPropagation.none: return false; } } } /// {@template aft.models.aft_component} /// Strongly connected components which should have minor/major version bumps /// happen in unison, i.e. a version bump to one package cascades to all. /// {@endtemplate} @yamlSerializable class RawAftComponent with AWSSerializable<Map<String, Object?>>, AWSDebuggable { const RawAftComponent({ required this.name, this.summary, required this.packages, this.propagate = VersionPropagation.minor, }); factory RawAftComponent.fromJson(Map<String, Object?> json) => _$RawAftComponentFromJson(json); /// The name of the component. final String name; /// The package name which summarizes all component changes in its changleog. final String? summary; /// The list of packages in the component. final List<String> packages; /// How to align package versions in this component when one changes. final VersionPropagation propagate; @override String get runtimeTypeName => 'RawAftComponent'; @override Map<String, Object?> toJson() => _$RawAftComponentToJson(this); } /// Typed representation of the `sdk.yaml` file. @yamlSerializable class SdkConfig with AWSEquatable<SdkConfig>, AWSSerializable<Map<String, Object?>>, AWSDebuggable { const SdkConfig({ this.ref = 'master', required this.apis, this.plugins = const [], }); factory SdkConfig.fromJson(Map<Object?, Object?>? json) => _$SdkConfigFromJson(json ?? const {}); /// The `aws-models` ref to pull. /// /// Defaults to `master`. final String ref; final Map<String, List<ShapeId>?> apis; final List<String> plugins; @override Map<String, Object?> toJson() => _$SdkConfigToJson(this); @override List<Object?> get props => [ref, apis, plugins]; @override String get runtimeTypeName => 'SdkConfig'; } class VersionConstraintConverter implements JsonConverter<VersionConstraint, String> { const VersionConstraintConverter(); @override VersionConstraint fromJson(String json) => VersionConstraint.parse(json); @override String toJson(VersionConstraint object) => object.toString(); } /// The type of version change to perform. enum VersionBumpType { /// Library packages are allowed to vary in their version, meaning a small /// change to one package (e.g. Update README) should be isolated to the /// affected package. /// /// Examples: /// * If the current version of a 0-based `aws_common` is `0.1.0` and its /// README is updated, it and it alone should be bumped to `0.1.1`. /// Note: a bump to `0.1.1` is technically a “minor†version bump in /// 0-based SemVer, but for consistency we can choose not to use build /// numbers (+). /// * If the current version of a 1-based `amplify_flutter` is `1.0.0` and its /// README is updated, it and it alone should be bumped to `1.0.1`. /// /// This version change is reserved for chores and bug fixes as denoted by /// the following conventional commit tags: `fix`, `bug`, `perf`, `chore`, /// `build`, `ci`, `docs`, `refactor`, `revert`, `style`, `test`. patch, /// A non-breaking version bump for a package represents a divergence from /// the previous version in terms of behavior or functionality in the form of /// a new feature. /// /// Examples: /// * If the current version of a 0-based aws_common is `0.1.0` and it is part /// of a feature change, it is bumped to `0.1.1` alongside all other package /// bumps. /// * If the current version of a 1-based amplify_flutter is `1.0.0` and it is /// part of a feature change, it is bumped to `1.1.0` alongside all other /// package bumps. /// /// This version change is reserved for new features denoted by the `feat` /// conventional commit tag. nonBreaking, /// A breaking version bump is reserved for breaking API changes. **These are /// rarely done.** /// /// * 0-based packages are allowed to break their APIs while 0-based and /// follow the non-breaking version scheme described above, e.g. /// `0.1.0` → `0.2.0`. /// /// * Stable packages (>1.0.0) bump to the next SemVer major version, e.g. /// `1.0.0` → `2.0.0`. /// /// Packages opt in to this behavior by suffixing an exclamation point to /// their commit message title tag, e.g. /// /// - `feat(auth): A new feature` would be a non-breaking feature change. /// - `feat(auth)!: A new feature` would be a breaking feature change. breaking, } @yamlSerializable class AftScript with AWSSerializable<Map<String, Object?>>, AWSDebuggable { const AftScript({ required this.run, this.description, this.from = const PackageSelector.development(), this.failFast = false, }); factory AftScript.fromJson(Map<String, Object?> json) => _$AftScriptFromJson(json); /// The script or script template to run. /// /// Templated scripts are first processed using [mustache](https://mustache.github.io/mustache.5.html) /// using standard variables. final String run; /// An optional textual description of the command which is printed in help /// when running `aft run`. final String? description; /// Selects the packages in which to run the script. /// /// If not provided, defaults to [PackageSelector.development]. final PackageSelector from; /// Whether to halt execution when an error is encountered by a script. /// /// If `false`, the script is run in all targeted packages regardless of the /// outcome in any one package. @JsonKey(name: 'fail-fast') final bool failFast; @override String get runtimeTypeName => 'AftScript'; @override Map<String, Object?> toJson() => _$AftScriptToJson(this); } class PubspecConverter implements JsonConverter<Pubspec, Map<String, Object?>> { const PubspecConverter(); @override Pubspec fromJson(Map<String, Object?> json) => Pubspec.fromJson(json); @override Map<String, Object?> toJson(Pubspec object) => object.toJson(); } extension on Dependency { Map<String, Object?> toJson() { final dependency = this; var dependencyJson = <String, Object?>{}; if (dependency is HostedDependency) { dependencyJson = { 'version': dependency.version.toString(), }; final details = dependency.hosted; if (details != null && details.url != null) { dependencyJson['hosted'] = details.url!.toString(); } } else if (dependency is SdkDependency) { dependencyJson = { 'version': dependency.version.toString(), 'sdk': dependency.sdk, }; } else if (dependency is GitDependency) { dependencyJson = { 'git': { 'url': dependency.url.toString(), if (dependency.ref != null) 'ref': dependency.ref, if (dependency.path != null) 'path': dependency.path, }, }; } else if (dependency is PathDependency) { dependencyJson = { 'path': dependency.path, }; } return dependencyJson; } } extension on Pubspec { Map<String, Object?> toJson() => { 'name': name, if (version != null) 'version': version!.toString(), if (publishTo != null) 'publishTo': publishTo, if (environment != null) 'environment': environment!.map((key, constraint) { return MapEntry(key, constraint?.toString()); }), if (homepage != null) 'homepage': homepage, if (repository != null) 'repository': repository!.toString(), if (issueTracker != null) 'issueTracker': issueTracker!.toString(), if (description != null) 'description': description, 'dependencies': dependencies.map((name, dependency) { return MapEntry(name, dependency.toJson()); }), 'dependencyOverrides': dependencyOverrides.map((name, dependency) { return MapEntry(name, dependency.toJson()); }), 'devDependencies': devDependencies.map((name, dependency) { return MapEntry(name, dependency.toJson()); }), if (flutter != null) 'flutter': flutter, }; }