diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index 2708514..50401a5 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -11,6 +11,32 @@ on: workflow_dispatch: jobs: + ci: + name: Dart CI Checks + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + work_dir: [ openapi-generator, openapi-generator-annotations ] + defaults: + run: + working-directory: ${{ matrix.work_dir }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Dart + uses: dart-lang/setup-dart@v1.5.0 + with: + sdk: stable + - name: Install Dependencies + run: dart pub get + - name: Validate formatting + run: dart format ./ --set-exit-if-changed + - name: Run analyzer + run: dart analyze --fatal-warnings + - name: Run tests + run: dart test + build: name: Build example project 🛠️ runs-on: ubuntu-latest @@ -33,20 +59,21 @@ jobs: - run: flutter pub run build_runner build --delete-conflicting-outputs - run: flutter build apk -# - name: Upload artifact (Client) ⬆️💻 -# uses: actions/upload-artifact@v3.1.1 -# with: -# name: example -# path: | -# example/build/web + # - name: Upload artifact (Client) ⬆️💻 + # uses: actions/upload-artifact@v3.1.1 + # with: + # name: example + # path: | + # example/build/web pr_context: name: Save PR context as artifact if: ${{ always() && !cancelled() && github.event_name == 'pull_request' }} runs-on: ubuntu-latest needs: -# - dependency-review + # - dependency-review - build + - ci steps: - name: Save PR context diff --git a/README.md b/README.md index 98120a3..e335a5f 100755 --- a/README.md +++ b/README.md @@ -4,6 +4,15 @@ This codebase houses the dart/flutter implementations of the openapi client sdk code generation libraries. +## TOC + +- [Introduction](#introduction) +- [Usage](#usage) +- [NextGen](#next-generation) +- [Features & Bugs](#features-and-bugs) + +## Introduction + With this project, you can generate openapi client sdk libraries for your openapi specification right in your flutter/dart projects. (see example) @@ -17,37 +26,42 @@ This repo contains the following dart libraries | openapi-generator-annotations | Annotations for annotating dart class with instructions for generating openapi sdk [see here for usage](https://pub.dev/packages/openapi_generator_annotations) | [![pub package](https://img.shields.io/pub/v/openapi_generator_annotations.svg)](https://pub.dev/packages/openapi_generator) | | openapi-generator-cli | Cli code openapi sdk generator for dart [see here for usage](https://pub.dev/packages/openapi_generator_cli) | [![pub package](https://img.shields.io/pub/v/openapi_generator_cli.svg)](https://pub.dev/packages/openapi_generator_cli) | - - ## Usage -Include [openapi-generator-annotations](https://pub.dev/packages/openapi_generator_annotations) as a dependency in the dependencies section of your pubspec.yaml file : +Include [openapi-generator-annotations](https://pub.dev/packages/openapi_generator_annotations) as a dependency in the +dependencies section of your pubspec.yaml file : ```yaml dependencies: openapi_generator_annotations: ^[latest-version] ``` -For testing out the beta features in openapi generator, use the beta branch like below. This is not recommended for production builds + +For testing out the beta features in openapi generator, use the beta branch like below. This is not recommended for +production builds + ```yaml dependencies: - openapi_generator_annotations: + openapi_generator_annotations: git: url: https://github.com/gibahjoe/openapi-generator-dart.git ref: beta path: openapi-generator-annotations ``` - -Add [openapi-generator](https://pub.dev/packages/openapi_generator) in the dev dependencies section of your pubspec.yaml file: +Add [openapi-generator](https://pub.dev/packages/openapi_generator) in the dev dependencies section of your pubspec.yaml +file: ```yaml dev_dependencies: openapi_generator: ^[latest version] ``` -For testing out the beta features in openapi generator, use the beta branch like below. This is not recommended for production builds + +For testing out the beta features in openapi generator, use the beta branch like below. This is not recommended for +production builds + ```yaml dev_dependencies: - openapi_generator: + openapi_generator: git: url: https://github.com/gibahjoe/openapi-generator-dart.git ref: beta @@ -66,17 +80,62 @@ Annotate a dart class with @Openapi() annotation class Example extends OpenapiGeneratorConfig {} ``` -Run +Run + ```shell dart run build_runner build --delete-conflicting-outputs ``` + or + ```shell flutter pub run build_runner build --delete-conflicting-outputs ``` + to generate open api client sdk from spec file specified in annotation. The api sdk will be generated in the folder specified in the annotation. See examples for more details +## Next Generation + +There is some new functionality slated to be added to the generator. This version will have the ability to: + +- cache changes in the OAS spec +- Rerun when there ares difference in the cached copy and current copy +- Pull from a remote source and cache that. + - **Note**: This means that your cache could be potentially stale. But in that case this flow will still pull the + latest and run. + - While this is a possible usage, if you are actively developing your spec it is preferred you provide a local copy. +- Skip generation based off: + - Flags + - No difference between the cache and local +- And all the functionality provided previously. + +Your original workflow stay the same but there is a slight difference in the annotations. + +New: + +```dart +@Openapi( + additionalProperties: + AdditionalProperties(pubName: 'petstore_api', pubAuthor: 'Johnny dep'), + inputSpecFile: 'example/openapi-spec.yaml', + generatorName: Generator.dart, + outputDirectory: 'api/petstore_api', + cachePath: 'some/preferred/directory/cache.json', + useNextGen: true +) +class Example extends OpenapiGeneratorConfig {} +``` + +**IMPORTANT** With the new changes comes 2 new annotation properties: + +- useNextGen (boolean) + - Default: `false` +- cachePath (String) + - Default: `.dart_tool/openapi-generator-cache.json` + - Must be a path to a `json` file. + - Can only be set when `useNextGen` is `true` + ## Features and bugs Please file feature requests and bugs at the [issue tracker][tracker]. diff --git a/example/.gitignore b/example/.gitignore index c1c5687..acbec99 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -44,5 +44,5 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release -api +api/petstore_api/** !api/petstore_api/pubspec.yaml diff --git a/example/api/petstore_api/pubspec.yaml b/example/api/petstore_api/pubspec.yaml new file mode 100644 index 0000000..503c965 --- /dev/null +++ b/example/api/petstore_api/pubspec.yaml @@ -0,0 +1,19 @@ +name: petstore_api +version: 1.0.0 +description: OpenAPI API client +homepage: homepage + +environment: + sdk: '>=2.15.0 <3.0.0' + +dependencies: + dio: '^5.0.0' + one_of: '>=1.5.0 <2.0.0' + one_of_serializer: '>=1.5.0 <2.0.0' + built_value: '>=8.4.0 <9.0.0' + built_collection: '>=5.1.1 <6.0.0' + +dev_dependencies: + built_value_generator: '>=8.4.0 <9.0.0' + build_runner: any + test: ^1.16.0 diff --git a/example/lib/main.dart b/example/lib/main.dart index 7802f63..b1218c8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,14 +6,15 @@ void main() { } @Openapi( - additionalProperties: - DioProperties(pubName: 'petstore_api', pubAuthor: 'Johnny dep..'), - inputSpecFile: 'openapi-spec.yaml', - typeMappings: {'Pet': 'ExamplePet'}, - generatorName: Generator.dio, - runSourceGenOnOutput: true, - alwaysRun: true, - outputDirectory: 'api/petstore_api') + additionalProperties: + DioProperties(pubName: 'petstore_api', pubAuthor: 'Johnny dep..'), + inputSpecFile: 'openapi-spec.yaml', + typeMappings: {'Pet': 'ExamplePet'}, + generatorName: Generator.dio, + runSourceGenOnOutput: true, + alwaysRun: true, + outputDirectory: 'api/petstore_api', +) class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); diff --git a/openapi-generator-annotations/analysis_options.yaml b/openapi-generator-annotations/analysis_options.yaml index a686c1b..bb36d2d 100644 --- a/openapi-generator-annotations/analysis_options.yaml +++ b/openapi-generator-annotations/analysis_options.yaml @@ -1,8 +1,3 @@ -# Defines a default set of lint rules enforced for -# projects at Google. For details and rationale, -# see https://github.com/dart-lang/pedantic#enabled-lints. -include: package:pedantic/analysis_options.yaml - # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. # Uncomment to specify additional rules. # linter: diff --git a/openapi-generator-annotations/example/example.dart b/openapi-generator-annotations/example/example.dart index 6da5b32..beb8b68 100644 --- a/openapi-generator-annotations/example/example.dart +++ b/openapi-generator-annotations/example/example.dart @@ -5,5 +5,7 @@ import 'package:openapi_generator_annotations/openapi_generator_annotations.dart AdditionalProperties(pubName: 'petstore_api', pubAuthor: 'Johnny dep'), inputSpecFile: 'example/openapi-spec.yaml', generatorName: Generator.dio, - outputDirectory: 'api/petstore_api') + outputDirectory: 'api/petstore_api', + // useNextGen: true, + cachePath: 'something') class Example extends OpenapiGeneratorConfig {} diff --git a/openapi-generator-annotations/lib/src/openapi_generator_annotations_base.dart b/openapi-generator-annotations/lib/src/openapi_generator_annotations_base.dart index 8553be5..5d9a367 100644 --- a/openapi-generator-annotations/lib/src/openapi_generator_annotations_base.dart +++ b/openapi-generator-annotations/lib/src/openapi_generator_annotations_base.dart @@ -94,23 +94,54 @@ class Openapi { /// e.g {'inline_object_2': 'SomethingMapped'} final Map? inlineSchemaNameMappings; - const Openapi( - {this.additionalProperties, - this.overwriteExistingFiles, - this.skipSpecValidation = false, - required this.inputSpecFile, - this.templateDirectory, - required this.generatorName, - this.outputDirectory, - this.typeMappings, - this.importMappings, - this.reservedWordsMappings, - this.inlineSchemaNameMappings, - // this.inlineSchemaOptions, - this.apiPackage, - this.fetchDependencies = true, - this.runSourceGenOnOutput = true, - this.alwaysRun = false}); + /// Use the next generation of the generator. + /// + /// This annotation informs the generator to use the new generator pathway. + /// Enabling this option allows for incremental changes to the [inputSpecFile] + /// to be generated even though the annotation was unchanged. + /// + /// Due to some limitations with build_runner and it only running when the + /// asset graph has changed, a generated line get injected at the beginning of + /// the file at the end of each run. + /// + /// This will become the default behaviour in the next Major version (v5). + /// + /// Default: false + final bool useNextGen; + + /// The path where to store the cached copy of the specification. + /// + /// For use with [useNextGen]. + final String? cachePath; + + /// Use a custom pubspec when running the generator. + final String? projectPubspecPath; + + /// Include in depth logging output from run commands. + final bool debugLogging; + + const Openapi({ + this.additionalProperties, + @deprecated this.overwriteExistingFiles, + this.skipSpecValidation = false, + required this.inputSpecFile, + this.templateDirectory, + required this.generatorName, + this.outputDirectory, + this.typeMappings, + this.importMappings, + this.reservedWordsMappings, + this.inlineSchemaNameMappings, + // this.inlineSchemaOptions, + this.apiPackage, + this.fetchDependencies = true, + this.runSourceGenOnOutput = true, + @deprecated this.alwaysRun = false, + this.cachePath, + this.useNextGen = false, + this.projectPubspecPath, + this.debugLogging = false, + }); } class AdditionalProperties { @@ -171,22 +202,64 @@ class AdditionalProperties { /// an error if the discriminator is missing. final bool legacyDiscriminatorBehavior; - const AdditionalProperties( - {this.allowUnicodeIdentifiers = false, - this.ensureUniqueParams = true, - this.useEnumExtension = false, - this.prependFormOrBodyParameters = false, - this.pubAuthor, - this.pubAuthorEmail, - this.pubDescription, - this.pubHomepage, - this.legacyDiscriminatorBehavior = true, - this.pubName, - this.pubVersion, - this.sortModelPropertiesByRequiredFlag = true, - this.sortParamsByRequiredFlag = true, - this.sourceFolder, - this.wrapper = Wrapper.none}); + const AdditionalProperties({ + this.allowUnicodeIdentifiers = false, + this.ensureUniqueParams = true, + this.useEnumExtension = false, + this.prependFormOrBodyParameters = false, + this.pubAuthor, + this.pubAuthorEmail, + this.pubDescription, + this.pubHomepage, + this.legacyDiscriminatorBehavior = true, + this.pubName, + this.pubVersion, + this.sortModelPropertiesByRequiredFlag = true, + this.sortParamsByRequiredFlag = true, + this.sourceFolder, + this.wrapper = Wrapper.none, + }); + + /// Produces an [AdditionalProperties] object from the [ConstantReader] [map]. + AdditionalProperties.fromMap(Map map) + : this( + allowUnicodeIdentifiers: map['allowUnicodeIdentifiers'] ?? false, + ensureUniqueParams: map['ensureUniqueParams'] ?? true, + useEnumExtension: map['useEnumExtension'] ?? true, + prependFormOrBodyParameters: + map['prependFormOrBodyParameters'] ?? false, + pubAuthor: map['pubAuthor'], + pubAuthorEmail: map['pubAuthorEmail'], + pubDescription: map['pubDescription'], + pubHomepage: map['pubHomepage'], + pubName: map['pubName'], + pubVersion: map['pubVersion'], + legacyDiscriminatorBehavior: + map['legacyDiscriminatorBehavior'] ?? true, + sortModelPropertiesByRequiredFlag: + map['sortModelPropertiesByRequiredFlag'] ?? true, + sortParamsByRequiredFlag: map['sortParamsByRequiredFlag'] ?? true, + sourceFolder: map['sourceFolder'], + wrapper: EnumTransformer.wrapper(map['wrapper']), + ); + + Map toMap() => { + 'allowUnicodeIdentifiers': allowUnicodeIdentifiers, + 'ensureUniqueParams': ensureUniqueParams, + 'useEnumExtension': useEnumExtension, + 'prependFormOrBodyParameters': prependFormOrBodyParameters, + if (pubAuthor != null) 'pubAuthor': pubAuthor, + if (pubAuthorEmail != null) 'pubAuthorEmail': pubAuthorEmail, + if (pubDescription != null) 'pubDescription': pubDescription, + if (pubHomepage != null) 'pubHomepage': pubHomepage, + if (pubName != null) 'pubName': pubName, + if (pubVersion != null) 'pubVersion': pubVersion, + 'legacyDiscriminatorBehavior': legacyDiscriminatorBehavior, + 'sortModelPropertiesByRequiredFlag': sortModelPropertiesByRequiredFlag, + 'sortParamsByRequiredFlag': sortParamsByRequiredFlag, + if (sourceFolder != null) 'sourceFolder': sourceFolder, + 'wrapper': EnumTransformer.wrapperName(wrapper) + }; } /// Allows you to customize how inline schemas are handled or named @@ -201,10 +274,10 @@ class InlineSchemaOptions { final bool skipSchemaReuse; /// will restore the 6.x (or below) behaviour to refactor allOf inline schemas - ///into $ref. (v7.0.0 will skip the refactoring of these allOf inline schmeas by default) + ///into $ref. (v7.0.0 will skip the refactoring of these allOf inline schemas by default) final bool refactorAllofInlineSchemas; - /// Email address of the author in generated pubspec + /// Email address of the author in generated pubspec final bool resolveInlineEnums; const InlineSchemaOptions( @@ -213,6 +286,25 @@ class InlineSchemaOptions { this.skipSchemaReuse = true, this.refactorAllofInlineSchemas = true, this.resolveInlineEnums = true}); + + /// Produces an [InlineSchemaOptions] that is easily consumable from the [ConstantReader]. + InlineSchemaOptions.fromMap(Map map) + : this( + arrayItemSuffix: map['arrayItemSuffix'], + mapItemSuffix: map['mapItemSuffix'], + skipSchemaReuse: map['skipSchemaReuse'] ?? true, + refactorAllofInlineSchemas: map['refactorAllofInlineSchemas'] ?? true, + resolveInlineEnums: map['resolveInlineEnums'] ?? true, + ); + + /// A convenience function that simplifies the output to the compiler. + Map toMap() => { + if (arrayItemSuffix != null) 'arrayItemSuffix': arrayItemSuffix!, + if (mapItemSuffix != null) 'mapItemSuffix': mapItemSuffix!, + 'skipSchemaReuse': skipSchemaReuse, + 'refactorAllofInlineSchemas': refactorAllofInlineSchemas, + 'resolveInlineEnums': resolveInlineEnums, + }; } class DioProperties extends AdditionalProperties { @@ -239,7 +331,8 @@ class DioProperties extends AdditionalProperties { bool sortModelPropertiesByRequiredFlag = true, bool sortParamsByRequiredFlag = true, bool useEnumExtension = true, - String? sourceFolder}) + String? sourceFolder, + Wrapper wrapper = Wrapper.none}) : super( allowUnicodeIdentifiers: allowUnicodeIdentifiers, ensureUniqueParams: ensureUniqueParams, @@ -254,7 +347,27 @@ class DioProperties extends AdditionalProperties { sortModelPropertiesByRequiredFlag, sortParamsByRequiredFlag: sortParamsByRequiredFlag, sourceFolder: sourceFolder, - useEnumExtension: useEnumExtension); + useEnumExtension: useEnumExtension, + wrapper: wrapper); + + DioProperties.fromMap(Map map) + : dateLibrary = EnumTransformer.dioDateLibrary(map['dateLibrary']), + nullableFields = map['nullableFields'] != null + ? map['nullableFields'] == 'true' + : null, + serializationLibrary = EnumTransformer.dioSerializationLibrary( + map['serializationLibrary']), + super.fromMap(map); + + Map toMap() => Map.from(super.toMap()) + ..addAll({ + if (dateLibrary != null) + 'dateLibrary': EnumTransformer.dioDateLibraryName(dateLibrary!), + if (nullableFields != null) 'nullableFields': nullableFields, + if (serializationLibrary != null) + 'serializationLibrary': + EnumTransformer.dioSerializationLibraryName(serializationLibrary!), + }); } class DioAltProperties extends AdditionalProperties { @@ -295,7 +408,8 @@ class DioAltProperties extends AdditionalProperties { bool sortModelPropertiesByRequiredFlag = true, bool sortParamsByRequiredFlag = true, bool useEnumExtension = true, - String? sourceFolder}) + String? sourceFolder, + Wrapper wrapper = Wrapper.none}) : super( allowUnicodeIdentifiers: allowUnicodeIdentifiers, ensureUniqueParams: ensureUniqueParams, @@ -310,7 +424,31 @@ class DioAltProperties extends AdditionalProperties { sortModelPropertiesByRequiredFlag, sortParamsByRequiredFlag: sortParamsByRequiredFlag, sourceFolder: sourceFolder, - useEnumExtension: useEnumExtension); + useEnumExtension: useEnumExtension, + wrapper: wrapper); + + DioAltProperties.fromMap(Map map) + : nullSafe = map['nullSafe'] != null ? map['nullSafe'] == 'true' : null, + nullSafeArrayDefault = map['nullSafeArrayDefault'] != null + ? map['nullSafeArrayDefault'] == 'true' + : null, + listAnyOf = + map['listAnyOf'] != null ? map['listAnyOf'] == 'true' : null, + pubspecDependencies = map['pubspecDependencies'], + pubspecDevDependencies = map['pubspecDevDependencies'], + super.fromMap(map); + + Map toMap() => Map.from(super.toMap()) + ..addAll({ + if (nullSafe != null) 'nullSafe': nullSafe, + if (nullSafeArrayDefault != null) + 'nullSafeArrayDefault': nullSafeArrayDefault, + if (listAnyOf != null) 'listAnyOf': listAnyOf, + if (pubspecDependencies != null) + 'pubspecDependencies': pubspecDependencies, + if (pubspecDevDependencies != null) + 'pubspecDevDependencies': pubspecDevDependencies, + }); } enum DioDateLibrary { @@ -346,4 +484,97 @@ enum Generator { dioAlt, } +// TODO: Upon release of NextGen as default migrate to sdk 2.17 for enhanced enums +// remove this work around. +/// Transforms the enums used with the [Openapi] annotation. +class EnumTransformer { + static DioDateLibrary? dioDateLibrary(String? name) { + switch (name) { + case 'timemachine': + return DioDateLibrary.timemachine; + case 'core': + return DioDateLibrary.core; + } + return null; + } + + static String dioDateLibraryName(DioDateLibrary lib) { + switch (lib) { + case DioDateLibrary.timemachine: + return 'timemachine'; + default: + return 'core'; + } + } + + static DioSerializationLibrary? dioSerializationLibrary(String? name) { + switch (name) { + case 'json_serializable': + return DioSerializationLibrary.json_serializable; + case 'built_value': + return DioSerializationLibrary.built_value; + } + return null; + } + + static String dioSerializationLibraryName(DioSerializationLibrary lib) { + switch (lib) { + case DioSerializationLibrary.json_serializable: + return 'json_serializable'; + default: + return 'built_value'; + } + } + + /// Converts the given [name] to the matching [Generator] name. + /// + /// Defaults to [Generator.dart]; + static Generator generator(String? name) { + switch (name) { + case 'dio': + return Generator.dio; + case 'dioAlt': + return Generator.dioAlt; + default: + return Generator.dart; + } + } + + static String generatorName(Generator generator) { + switch (generator) { + case Generator.dio: + return 'dart-dio'; + case Generator.dioAlt: + return 'dart2-api'; + default: + return 'dart'; + } + } + + /// Converts the given [name] to the matching [Wrapper] name. + /// + /// Defaults to [Wrapper.none]; + static Wrapper wrapper(String? name) { + switch (name) { + case 'fvm': + return Wrapper.fvm; + case 'flutterw': + return Wrapper.flutterw; + default: + return Wrapper.none; + } + } + + static String wrapperName(Wrapper wrapper) { + switch (wrapper) { + case Wrapper.flutterw: + return 'flutterw'; + case Wrapper.fvm: + return 'fvm'; + default: + return 'none'; + } + } +} + enum Wrapper { fvm, flutterw, none } diff --git a/openapi-generator-annotations/pubspec.yaml b/openapi-generator-annotations/pubspec.yaml index 4839859..0117754 100644 --- a/openapi-generator-annotations/pubspec.yaml +++ b/openapi-generator-annotations/pubspec.yaml @@ -8,5 +8,6 @@ environment: sdk: '>=2.12.0 <3.0.0' dev_dependencies: - pedantic: test: + source_gen_test: ^1.0.6 + lint: \ No newline at end of file diff --git a/openapi-generator-annotations/test/additional_properties_test.dart b/openapi-generator-annotations/test/additional_properties_test.dart new file mode 100644 index 0000000..d9d3760 --- /dev/null +++ b/openapi-generator-annotations/test/additional_properties_test.dart @@ -0,0 +1,324 @@ +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; +import 'package:test/test.dart'; + +void main() { + group('AdditionalProperties', () { + test('defaults', () { + final props = AdditionalProperties(); + expect(props.wrapper, Wrapper.none); + expect(props.allowUnicodeIdentifiers, isFalse); + expect(props.ensureUniqueParams, isTrue); + expect(props.useEnumExtension, isFalse); + expect(props.prependFormOrBodyParameters, isFalse); + expect(props.legacyDiscriminatorBehavior, isTrue); + expect(props.sortModelPropertiesByRequiredFlag, isTrue); + expect(props.sortParamsByRequiredFlag, isTrue); + // Default null props + [ + props.pubVersion, + props.pubName, + props.pubHomepage, + props.pubDescription, + props.pubAuthor, + props.pubAuthorEmail, + props.sourceFolder + ].forEach((element) => expect(element, isNull)); + }); + test('toMap', () { + final props = AdditionalProperties(); + final map = props.toMap(); + expect(map['wrapper'], 'none'); + expect(map['allowUnicodeIdentifiers'], isFalse); + expect(map['ensureUniqueParams'], isTrue); + expect(map['useEnumExtension'], isFalse); + expect(map['prependFormOrBodyParameters'], isFalse); + expect(map['legacyDiscriminatorBehavior'], isTrue); + expect(map['sortModelPropertiesByRequiredFlag'], isTrue); + expect(map['sortParamsByRequiredFlag'], isTrue); + + // Doesn't include null fields + [ + 'pubVersion', + 'pubName', + 'pubHomepage', + 'pubDescription', + 'pubAuthor', + 'pubAuthorEmail', + 'sourceFolder' + ].forEach((element) => expect(map.containsKey(element), isFalse)); + }); + test('fromMap', () { + final props = AdditionalProperties( + pubVersion: '1.0.0', + pubName: 'test', + pubHomepage: 'test', + pubDescription: 'test', + pubAuthorEmail: 'test@test.test', + pubAuthor: 'test'); + final map = { + 'allowUnicodeIdentifiers': props.allowUnicodeIdentifiers, + 'ensureUniqueParams': props.ensureUniqueParams, + 'useEnumExtension': props.useEnumExtension, + 'prependFormOrBodyParameters': props.prependFormOrBodyParameters, + 'pubAuthor': props.pubAuthor, + 'pubAuthorEmail': props.pubAuthorEmail, + 'pubDescription': props.pubDescription, + 'pubHomepage': props.pubHomepage, + 'pubName': props.pubName, + 'pubVersion': props.pubVersion, + 'legacyDiscriminatorBehavior': props.legacyDiscriminatorBehavior, + 'sortModelPropertiesByRequiredFlag': + props.sortModelPropertiesByRequiredFlag, + 'sortParamsByRequiredFlag': props.sortParamsByRequiredFlag, + 'sourceFolder': props.sourceFolder, + 'wrapper': 'none', + }; + final actual = AdditionalProperties.fromMap(map); + expect(actual.wrapper, props.wrapper); + expect(actual.allowUnicodeIdentifiers, props.allowUnicodeIdentifiers); + expect(actual.ensureUniqueParams, props.ensureUniqueParams); + expect(actual.useEnumExtension, props.useEnumExtension); + expect(actual.prependFormOrBodyParameters, + props.prependFormOrBodyParameters); + expect(actual.legacyDiscriminatorBehavior, + props.legacyDiscriminatorBehavior); + expect(actual.sortModelPropertiesByRequiredFlag, + props.sortModelPropertiesByRequiredFlag); + expect(actual.sortParamsByRequiredFlag, props.sortParamsByRequiredFlag); + expect(actual.pubVersion, props.pubVersion); + expect(actual.pubName, props.pubName); + expect(actual.pubHomepage, props.pubHomepage); + expect(actual.pubDescription, props.pubDescription); + expect(actual.pubAuthor, props.pubAuthor); + expect(actual.pubAuthorEmail, props.pubAuthorEmail); + expect(actual.sourceFolder, props.sourceFolder); + }); + }); + + group('DioProperties', () { + test('defaults', () { + final props = DioProperties(); + expect(props.wrapper, Wrapper.none); + expect(props.allowUnicodeIdentifiers, isFalse); + expect(props.ensureUniqueParams, isTrue); + expect(props.useEnumExtension, isTrue); + expect(props.prependFormOrBodyParameters, isFalse); + expect(props.legacyDiscriminatorBehavior, isTrue); + expect(props.sortModelPropertiesByRequiredFlag, isTrue); + expect(props.sortParamsByRequiredFlag, isTrue); + // Default null props + [ + props.pubVersion, + props.pubName, + props.pubHomepage, + props.pubDescription, + props.pubAuthor, + props.pubAuthorEmail, + props.sourceFolder, + props.nullableFields, + props.serializationLibrary, + props.dateLibrary, + ].forEach((element) => expect(element, isNull)); + }); + test('toMap', () { + final props = DioProperties(); + final map = props.toMap(); + expect(map['wrapper'], 'none'); + expect(map['allowUnicodeIdentifiers'], isFalse); + expect(map['ensureUniqueParams'], isTrue); + expect(map['useEnumExtension'], isTrue); + expect(map['prependFormOrBodyParameters'], isFalse); + expect(map['legacyDiscriminatorBehavior'], isTrue); + expect(map['sortModelPropertiesByRequiredFlag'], isTrue); + expect(map['sortParamsByRequiredFlag'], isTrue); + + // Doesn't include null fields + [ + 'pubVersion', + 'pubName', + 'pubHomepage', + 'pubDescription', + 'pubAuthor', + 'pubAuthorEmail', + 'sourceFolder' + 'dateLibrary', + 'nullableFields', + 'serializationLibrary' + ].forEach((element) => expect(map.containsKey(element), isFalse)); + }); + test('fromMap', () { + final props = DioProperties( + pubVersion: '1.0.0', + pubName: 'test', + pubHomepage: 'test', + pubDescription: 'test', + pubAuthorEmail: 'test@test.test', + pubAuthor: 'test', + nullableFields: true, + dateLibrary: DioDateLibrary.core, + serializationLibrary: DioSerializationLibrary.json_serializable, + ); + final map = { + 'allowUnicodeIdentifiers': props.allowUnicodeIdentifiers, + 'ensureUniqueParams': props.ensureUniqueParams, + 'useEnumExtension': props.useEnumExtension, + 'prependFormOrBodyParameters': props.prependFormOrBodyParameters, + 'pubAuthor': props.pubAuthor, + 'pubAuthorEmail': props.pubAuthorEmail, + 'pubDescription': props.pubDescription, + 'pubHomepage': props.pubHomepage, + 'pubName': props.pubName, + 'pubVersion': props.pubVersion, + 'legacyDiscriminatorBehavior': props.legacyDiscriminatorBehavior, + 'sortModelPropertiesByRequiredFlag': + props.sortModelPropertiesByRequiredFlag, + 'sortParamsByRequiredFlag': props.sortParamsByRequiredFlag, + 'sourceFolder': props.sourceFolder, + 'wrapper': 'none', + 'nullableFields': '${props.nullableFields}', + 'dateLibrary': 'core', + 'serializationLibrary': 'json_serializable', + }; + final actual = DioProperties.fromMap(map); + expect(actual.wrapper, props.wrapper); + expect(actual.allowUnicodeIdentifiers, props.allowUnicodeIdentifiers); + expect(actual.ensureUniqueParams, props.ensureUniqueParams); + expect(actual.useEnumExtension, props.useEnumExtension); + expect(actual.prependFormOrBodyParameters, + props.prependFormOrBodyParameters); + expect(actual.legacyDiscriminatorBehavior, + props.legacyDiscriminatorBehavior); + expect(actual.sortModelPropertiesByRequiredFlag, + props.sortModelPropertiesByRequiredFlag); + expect(actual.sortParamsByRequiredFlag, props.sortParamsByRequiredFlag); + expect(actual.pubVersion, props.pubVersion); + expect(actual.pubName, props.pubName); + expect(actual.pubHomepage, props.pubHomepage); + expect(actual.pubDescription, props.pubDescription); + expect(actual.pubAuthor, props.pubAuthor); + expect(actual.pubAuthorEmail, props.pubAuthorEmail); + expect(actual.sourceFolder, props.sourceFolder); + expect(actual.nullableFields, props.nullableFields); + expect(actual.dateLibrary, props.dateLibrary); + expect(actual.serializationLibrary, props.serializationLibrary); + }); + }); + group('DioAltProperties', () { + test('defaults', () { + final props = DioAltProperties(); + expect(props.wrapper, Wrapper.none); + expect(props.allowUnicodeIdentifiers, isFalse); + expect(props.ensureUniqueParams, isTrue); + expect(props.useEnumExtension, isTrue); + expect(props.prependFormOrBodyParameters, isFalse); + expect(props.legacyDiscriminatorBehavior, isTrue); + expect(props.sortModelPropertiesByRequiredFlag, isTrue); + expect(props.sortParamsByRequiredFlag, isTrue); + // Default null props + [ + props.pubVersion, + props.pubName, + props.pubHomepage, + props.pubDescription, + props.pubAuthor, + props.pubAuthorEmail, + props.sourceFolder, + props.nullSafe, + props.nullSafeArrayDefault, + props.listAnyOf, + props.pubspecDevDependencies, + props.pubspecDependencies + ].forEach((element) => expect(element, isNull)); + }); + test('toMap', () { + final props = DioAltProperties(); + final map = props.toMap(); + expect(map['wrapper'], 'none'); + expect(map['allowUnicodeIdentifiers'], isFalse); + expect(map['ensureUniqueParams'], isTrue); + expect(map['useEnumExtension'], isTrue); + expect(map['prependFormOrBodyParameters'], isFalse); + expect(map['legacyDiscriminatorBehavior'], isTrue); + expect(map['sortModelPropertiesByRequiredFlag'], isTrue); + expect(map['sortParamsByRequiredFlag'], isTrue); + + // Doesn't include null fields + [ + 'pubVersion', + 'pubName', + 'pubHomepage', + 'pubDescription', + 'pubAuthor', + 'pubAuthorEmail', + 'sourceFolder' + 'nullSafe,' + 'nullSafeArrayDefault,' + 'listAnyOf,' + 'pubspecDevDependencies,' + 'pubspecDependencies' + ].forEach((element) => expect(map.containsKey(element), isFalse)); + }); + test('fromMap', () { + final props = DioAltProperties( + pubVersion: '1.0.0', + pubName: 'test', + pubHomepage: 'test', + pubDescription: 'test', + pubAuthorEmail: 'test@test.test', + pubAuthor: 'test', + nullSafe: true, + nullSafeArrayDefault: true, + listAnyOf: false, + pubspecDevDependencies: 'something', + pubspecDependencies: 'test', + ); + final map = { + 'allowUnicodeIdentifiers': props.allowUnicodeIdentifiers, + 'ensureUniqueParams': props.ensureUniqueParams, + 'useEnumExtension': props.useEnumExtension, + 'prependFormOrBodyParameters': props.prependFormOrBodyParameters, + 'pubAuthor': props.pubAuthor, + 'pubAuthorEmail': props.pubAuthorEmail, + 'pubDescription': props.pubDescription, + 'pubHomepage': props.pubHomepage, + 'pubName': props.pubName, + 'pubVersion': props.pubVersion, + 'legacyDiscriminatorBehavior': props.legacyDiscriminatorBehavior, + 'sortModelPropertiesByRequiredFlag': + props.sortModelPropertiesByRequiredFlag, + 'sortParamsByRequiredFlag': props.sortParamsByRequiredFlag, + 'sourceFolder': props.sourceFolder, + 'wrapper': 'none', + 'nullSafe': '${props.nullSafe}', + 'nullSafeArrayDefault': '${props.nullSafeArrayDefault}', + 'listAnyOf': '${props.listAnyOf}', + 'pubspecDevDependencies': props.pubspecDevDependencies, + 'pubspecDependencies': props.pubspecDependencies, + }; + final actual = DioAltProperties.fromMap(map); + expect(actual.wrapper, props.wrapper); + expect(actual.allowUnicodeIdentifiers, props.allowUnicodeIdentifiers); + expect(actual.ensureUniqueParams, props.ensureUniqueParams); + expect(actual.useEnumExtension, props.useEnumExtension); + expect(actual.prependFormOrBodyParameters, + props.prependFormOrBodyParameters); + expect(actual.legacyDiscriminatorBehavior, + props.legacyDiscriminatorBehavior); + expect(actual.sortModelPropertiesByRequiredFlag, + props.sortModelPropertiesByRequiredFlag); + expect(actual.sortParamsByRequiredFlag, props.sortParamsByRequiredFlag); + expect(actual.pubVersion, props.pubVersion); + expect(actual.pubName, props.pubName); + expect(actual.pubHomepage, props.pubHomepage); + expect(actual.pubDescription, props.pubDescription); + expect(actual.pubAuthor, props.pubAuthor); + expect(actual.pubAuthorEmail, props.pubAuthorEmail); + expect(actual.sourceFolder, props.sourceFolder); + expect(actual.nullSafe, props.nullSafe); + expect(actual.nullSafeArrayDefault, props.nullSafeArrayDefault); + expect(actual.listAnyOf, props.listAnyOf); + expect(actual.pubspecDevDependencies, props.pubspecDevDependencies); + expect(actual.pubspecDependencies, props.pubspecDependencies); + }); + }); +} diff --git a/openapi-generator-annotations/test/enum_transformer_test.dart b/openapi-generator-annotations/test/enum_transformer_test.dart new file mode 100644 index 0000000..5a31940 --- /dev/null +++ b/openapi-generator-annotations/test/enum_transformer_test.dart @@ -0,0 +1,111 @@ +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; +import 'package:test/test.dart'; + +void main() { + group('EnumTransformer', () { + group('Transforms name -> value', () { + group('Wrapper', () { + test('fvm', () => expect(EnumTransformer.wrapper('fvm'), Wrapper.fvm)); + test( + 'flutterw', + () => + expect(EnumTransformer.wrapper('flutterw'), Wrapper.flutterw)); + test('defaults to none', + () => expect(EnumTransformer.wrapper('invalid'), Wrapper.none)); + test('none', + () => expect(EnumTransformer.wrapper('none'), Wrapper.none)); + }); + group('Generator', () { + test('dart', + () => expect(EnumTransformer.generator('dart'), Generator.dart)); + test('dio', + () => expect(EnumTransformer.generator('dio'), Generator.dio)); + test( + 'dioAlt', + () => + expect(EnumTransformer.generator('dioAlt'), Generator.dioAlt)); + test('defaults to dart', + () => expect(EnumTransformer.generator(null), Generator.dart)); + }); + group('DioDateLibrary', () { + test( + 'core', + () => expect( + EnumTransformer.dioDateLibrary('core'), DioDateLibrary.core)); + test( + 'timemachine', + () => expect(EnumTransformer.dioDateLibrary('timemachine'), + DioDateLibrary.timemachine)); + test('defaults to null', + () => expect(EnumTransformer.dioDateLibrary(null), null)); + }); + group('DioSerializationLibrary', () { + test( + 'built_value', + () => expect(EnumTransformer.dioSerializationLibrary('built_value'), + DioSerializationLibrary.built_value)); + test( + 'json_serializable', + () => expect( + EnumTransformer.dioSerializationLibrary('json_serializable'), + DioSerializationLibrary.json_serializable)); + test('defaults to null', + () => expect(EnumTransformer.dioSerializationLibrary(null), null)); + }); + }); + group('Transforms value -> name', () { + group('Wrapper', () { + test('fvm', + () => expect(EnumTransformer.wrapperName(Wrapper.fvm), 'fvm')); + test( + 'flutterw', + () => expect( + EnumTransformer.wrapperName(Wrapper.flutterw), 'flutterw')); + test('defaults to none', + () => expect(EnumTransformer.wrapperName(Wrapper.none), 'none')); + test('none', + () => expect(EnumTransformer.wrapper('none'), Wrapper.none)); + }); + group('Generator', () { + test( + 'dart', + () => + expect(EnumTransformer.generatorName(Generator.dart), 'dart')); + test( + 'dio', + () => expect( + EnumTransformer.generatorName(Generator.dio), 'dart-dio')); + test( + 'dioAlt', + () => expect( + EnumTransformer.generatorName(Generator.dioAlt), 'dart2-api')); + }); + group('DioDateLibrary', () { + test( + 'core', + () => expect( + EnumTransformer.dioDateLibraryName(DioDateLibrary.core), + 'core')); + test( + 'timemachine', + () => expect( + EnumTransformer.dioDateLibraryName(DioDateLibrary.timemachine), + 'timemachine')); + }); + group('DioSerializationLibrary', () { + test( + 'built_value', + () => expect( + EnumTransformer.dioSerializationLibraryName( + DioSerializationLibrary.built_value), + 'built_value')); + test( + 'json_serializable', + () => expect( + EnumTransformer.dioSerializationLibraryName( + DioSerializationLibrary.json_serializable), + 'json_serializable')); + }); + }); + }); +} diff --git a/openapi-generator-annotations/test/inline_schema_options_test.dart b/openapi-generator-annotations/test/inline_schema_options_test.dart new file mode 100644 index 0000000..a5f397c --- /dev/null +++ b/openapi-generator-annotations/test/inline_schema_options_test.dart @@ -0,0 +1,46 @@ +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; +import 'package:test/test.dart'; + +void main() { + group('InlineSchemaOptions', () { + test('defaults', () { + final options = InlineSchemaOptions(); + expect(options.skipSchemaReuse, isTrue); + expect(options.refactorAllofInlineSchemas, isTrue); + expect(options.resolveInlineEnums, isTrue); + + [options.arrayItemSuffix, options.mapItemSuffix] + .forEach((element) => expect(element, isNull)); + }); + test('toMap', () { + final options = InlineSchemaOptions(); + final map = options.toMap(); + + expect(map['skipSchemaReuse'], isTrue); + expect(map['refactorAllofInlineSchemas'], isTrue); + expect(map['resolveInlineEnums'], isTrue); + + ['arrayItemSuffix', 'mapItemSuffix'] + .forEach((element) => expect(map.containsKey(element), isFalse)); + }); + test('fromMap', () { + final options = + InlineSchemaOptions(arrayItemSuffix: 'test', mapItemSuffix: 'test'); + final map = { + 'arrayItemSuffix': 'test', + 'mapItemSuffix': 'test', + 'skipSchemaReuse': true, + 'refactorAllofInlineSchemas': true, + 'resolveInlineEnums': true, + }; + + final actual = InlineSchemaOptions.fromMap(map); + expect(actual.skipSchemaReuse, options.skipSchemaReuse); + expect(actual.refactorAllofInlineSchemas, + options.refactorAllofInlineSchemas); + expect(actual.resolveInlineEnums, options.resolveInlineEnums); + expect(actual.arrayItemSuffix, options.arrayItemSuffix); + expect(actual.mapItemSuffix, options.mapItemSuffix); + }); + }); +} diff --git a/openapi-generator-annotations/test/openapi_generator_annotations_test.dart b/openapi-generator-annotations/test/openapi_generator_annotations_test.dart index 69ff9e6..1b4297c 100644 --- a/openapi-generator-annotations/test/openapi_generator_annotations_test.dart +++ b/openapi-generator-annotations/test/openapi_generator_annotations_test.dart @@ -1,15 +1,57 @@ +import 'package:openapi_generator_annotations/src/openapi_generator_annotations_base.dart'; import 'package:test/test.dart'; void main() { - group('A group of tests', () { -// Awesome awesome; -// -// setUp(() { -// awesome = Awesome(); -// }); -// -// test('First Test', () { -// expect(awesome.isAwesome, isTrue); -// }); + group('OpenApi', () { + test('defaults', () { + final props = Openapi(inputSpecFile: '', generatorName: Generator.dart); + expect(props.additionalProperties, isNull); + expect(props.overwriteExistingFiles, isNull); + expect(props.skipSpecValidation, false); + expect(props.inputSpecFile, ''); + expect(props.templateDirectory, isNull); + expect(props.generatorName, Generator.dart); + expect(props.outputDirectory, isNull); + expect(props.typeMappings, isNull); + expect(props.importMappings, isNull); + expect(props.reservedWordsMappings, isNull); + expect(props.inlineSchemaNameMappings, isNull); + expect(props.apiPackage, isNull); + expect(props.fetchDependencies, true); + expect(props.runSourceGenOnOutput, true); + expect(props.alwaysRun, false); + expect(props.cachePath, isNull); + expect(props.useNextGen, false); + expect(props.projectPubspecPath, isNull); + expect(props.debugLogging, isFalse); + }); + group('NextGen', () { + test('Sets cachePath', () { + final api = Openapi( + inputSpecFile: '', + generatorName: Generator.dart, + cachePath: 'somePath'); + expect(api.cachePath, 'somePath'); + }); + test('Sets useNextGenFlag', () { + final api = Openapi( + inputSpecFile: '', generatorName: Generator.dart, useNextGen: true); + expect(api.useNextGen, isTrue); + }); + test('Sets projectPubspecPath', () { + final api = Openapi( + inputSpecFile: '', + generatorName: Generator.dart, + projectPubspecPath: 'test'); + expect(api.projectPubspecPath, 'test'); + }); + test('Set debug logging', () { + final api = Openapi( + inputSpecFile: '', + generatorName: Generator.dart, + debugLogging: true); + expect(api.debugLogging, isTrue); + }); + }); }); } diff --git a/openapi-generator/.gitignore b/openapi-generator/.gitignore index 7793bfe..cf8d17f 100755 --- a/openapi-generator/.gitignore +++ b/openapi-generator/.gitignore @@ -166,3 +166,6 @@ Temporary Items .apdisk .idea/ example/.dart_tool + +# Generated test output +test/specs/test-cached.json \ No newline at end of file diff --git a/openapi-generator/example/pubspec.yaml b/openapi-generator/example/pubspec.yaml index 67d3d11..daf75f0 100644 --- a/openapi-generator/example/pubspec.yaml +++ b/openapi-generator/example/pubspec.yaml @@ -27,16 +27,15 @@ dependency_overrides: path: ../../openapi-generator-cli dependencies: - flutter: - sdk: flutter +# flutter: +# sdk: flutter openapi_generator_annotations: ^4.10.0 cupertino_icons: ^1.0.2 dev_dependencies: - flutter_test: - sdk: flutter +# flutter_test: +# sdk: flutter build_runner: - openapi_generator_annotations: ^4.10.0 openapi_generator: ^4.10.0 diff --git a/openapi-generator/lib/src/determine_flutter_project_status.dart b/openapi-generator/lib/src/determine_flutter_project_status.dart new file mode 100644 index 0000000..fff2873 --- /dev/null +++ b/openapi-generator/lib/src/determine_flutter_project_status.dart @@ -0,0 +1,44 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; +import 'package:yaml/yaml.dart'; + +/// Determines whether a project has a dependency on the Flutter sdk. +/// +/// If a wrapper annotation is provided that is not null or [Wrapper.none] and +/// is one of [Wrapper.flutterw] or [Wrapper.fvm] it is safe to assume the project +/// requires the Flutter sdk. +/// +/// As a fallback we check the pubspec at the root of the current directory, which +/// will be where the build_runner command will have been called from, and check +/// the Pubspec's dependency list for the 'flutter' key. +/// +/// Note: This has support for providing a custom path to a pubspec but there isn't +/// any current implementation to receive it via the generator itself. +FutureOr checkPubspecAndWrapperForFlutterSupport( + {Wrapper? wrapper = Wrapper.none, String? providedPubspecPath}) async { + if ([Wrapper.flutterw, Wrapper.fvm].contains(wrapper)) { + return true; + } else { + // Use the path provided or default the directory the command was called from. + final pubspecPath = providedPubspecPath ?? + '${Directory.current.path}${Platform.pathSeparator}pubspec.yaml'; + + final pubspecFile = File(pubspecPath); + + if (!pubspecFile.existsSync()) { + return Future.error('Pubspec doesn\'t exist at path: $pubspecPath'); + } + + final contents = await pubspecFile.readAsString(); + if (contents.isEmpty) { + return Future.error('Invalid pubspec.yaml'); + } + + final pubspec = loadYaml(contents) as YamlMap; + + return pubspec.containsKey('dependencies') && + (pubspec.nodes['dependencies'] as YamlMap).containsKey('flutter'); + } +} diff --git a/openapi-generator/lib/src/extensions/type_methods.dart b/openapi-generator/lib/src/extensions/type_methods.dart index 72a7160..10a272b 100644 --- a/openapi-generator/lib/src/extensions/type_methods.dart +++ b/openapi-generator/lib/src/extensions/type_methods.dart @@ -1,6 +1,8 @@ import 'dart:mirrors'; import 'package:analyzer/dart/element/type.dart'; +import 'package:openapi_generator/src/utils.dart'; +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; import 'package:source_gen/source_gen.dart' show ConstantReader, TypeChecker; /// Extension adding the type methods to `ConstantReader`. @@ -71,3 +73,55 @@ extension TypeMethods on ConstantReader { return values[enumIndex]; } } + +extension ReadProperty on ConstantReader { + T readPropertyOrDefault(String name, T defaultValue) { + final v = peek(name); + if (v == null) { + return defaultValue; + } + + if (defaultValue is AdditionalProperties? || + defaultValue is InlineSchemaOptions?) { + final mapping = v + .revive() + .namedArguments + .map((key, value) => MapEntry(key, convertToPropertyValue(value))); + if (defaultValue is AdditionalProperties?) { + if (defaultValue is DioProperties?) { + return DioProperties.fromMap(mapping) as T; + } else if (defaultValue is DioAltProperties?) { + return DioAltProperties.fromMap(mapping) as T; + } else { + return AdditionalProperties.fromMap(mapping) as T; + } + } else { + return InlineSchemaOptions.fromMap(mapping) as T; + } + } + + if (isA(v, Map)) { + return v.mapValue.map((key, value) => MapEntry( + convertToPropertyValue(key!), convertToPropertyValue(value!))) as T; + } else if (isA(v, bool)) { + return v.boolValue as T; + } else if (isA(v, double)) { + return v.doubleValue as T; + } else if (isA(v, int)) { + return v.intValue as T; + } else if (isA(v, String)) { + return v.stringValue as T; + } else if (isA(v, Set)) { + return v.setValue as T; + } else if (isA(v, List)) { + return v.listValue as T; + } else if (isA(v, Enum)) { + return v.enumValue(); + } else { + return defaultValue; + } + } +} + +bool isA(ConstantReader? v, Type t) => + v?.instanceOf(TypeChecker.fromRuntime(t)) ?? false; diff --git a/openapi-generator/lib/src/gen_on_spec_changes.dart b/openapi-generator/lib/src/gen_on_spec_changes.dart new file mode 100644 index 0000000..fd59188 --- /dev/null +++ b/openapi-generator/lib/src/gen_on_spec_changes.dart @@ -0,0 +1,183 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; +import 'package:openapi_generator/src/models/output_message.dart'; +import 'package:yaml/yaml.dart'; + +// .json +final jsonRegex = RegExp(r'^.*.json$'); +// .yml & .yaml +final yamlRegex = RegExp(r'^.*(.ya?ml)$'); + +final _supportedRegexes = [jsonRegex, yamlRegex]; + +/// Load the provided OpenApiSpec from the disk into an in-memory mapping. +/// +/// Throws an error when the file extension doesn't match one of the expected +/// extensions: +/// - json +/// - yaml +/// - yml +/// +/// It also throws an error when the specification doesn't exist on disk. +/// +/// WARNING: THIS DOESN'T VALIDATE THE SPECIFICATION CONTENT +FutureOr> loadSpec( + {required String specPath, bool isCached = false}) async { + // If the spec file doesn't match any of the currently supported spec formats + // reject the request. + if (!_supportedRegexes.any((fileEnding) => fileEnding.hasMatch(specPath))) { + return Future.error( + OutputMessage( + message: 'Invalid spec file format.', + level: Level.SEVERE, + stackTrace: StackTrace.current, + ), + ); + } + + final isRemote = RegExp(r'^https?://').hasMatch(specPath); + if (!isRemote) { + final file = File(specPath); + if (file.existsSync()) { + final contents = await file.readAsString(); + late Map spec; + if (yamlRegex.hasMatch(specPath)) { + // Load yaml and convert to file + spec = convertYamlMapToDartMap(yamlMap: loadYaml(contents)); + } else { + // Convert to json map via json.decode + spec = jsonDecode(contents); + } + + return spec; + } + } else { + // TODO: Support custom headers? + final url = Uri.parse(specPath); + final resp = await http.get(url); + if (resp.statusCode == 200) { + if (yamlRegex.hasMatch(specPath)) { + return convertYamlMapToDartMap(yamlMap: loadYaml(resp.body)); + } else { + return jsonDecode(resp.body); + } + } else { + return Future.error( + OutputMessage( + message: + 'Unable to request remote spec. Ensure it is public or use a local copy instead.', + level: Level.SEVERE, + additionalContext: resp.statusCode, + stackTrace: StackTrace.current, + ), + ); + } + } + + // In the event that the cached spec isn't found, provide an empty mapping + // to diff against. This will cause the isSpecDirty check to return true. + // This can occur on a fresh build / clone. + if (isCached) { + return {}; + } + + return Future.error( + OutputMessage( + message: 'Unable to find spec file $specPath', + level: Level.WARNING, + stackTrace: StackTrace.current), + ); +} + +/// Verify if the [loadedSpec] has a diff compared to the [cachedSpec]. +/// +/// Returns true when the number of root keys is different. +bool isSpecDirty({ + required Map cachedSpec, + required Map loadedSpec, +}) { + return jsonEncode(cachedSpec) != jsonEncode(loadedSpec); +} + +/// Convert the [YamlMap] to a Dart [Map]. +/// +/// Converts a [YamlMap] and it's children into a [Map]. This involes expanding +/// [YamlList] & [YamlMap] nodes into their entries. [YamlScalar] values are +/// directly set. +Map convertYamlMapToDartMap({required YamlMap yamlMap}) { + final transformed = {}; + + yamlMap.forEach((key, value) { + late dynamic content; + if (value is YamlList) { + // Parse list entries + content = convertYamlListToDartList(yamlList: value); + } else if (value is YamlMap) { + // Parse the sub map + content = convertYamlMapToDartMap( + yamlMap: YamlMap.internal(value.nodes, value.span, value.style)); + } else if (value is YamlScalar) { + // Pull the value out of the scalar + content = value.value; + } else { + // Value is a supported dart type + content = value; + } + transformed['$key'] = content; + }); + + return transformed; +} + +/// Converts the given [yamlList] into a Dart [List]. +/// +/// Recursively converts the given [yamlList] to a Dart [List]; converting all +/// nested lists into their constituent values. +List convertYamlListToDartList({required YamlList yamlList}) { + final converted = []; + + yamlList.forEach((element) { + if (element is YamlList) { + converted.add(convertYamlListToDartList(yamlList: element)); + } else if (element is YamlMap) { + converted.add(convertYamlMapToDartMap(yamlMap: element)); + } else { + converted.add(element); + } + }); + + return converted; +} + +/// Caches the updated [spec] to disk for use in future comparisons. +/// +/// Caches the [spec] to the given [outputLocation]. By default this will be likely +/// be the .dart_tool/openapi-generator-cache.json +Future cacheSpec({ + required String outputLocation, + required Map spec, +}) async { + final outputPath = outputLocation; + final outputFile = File(outputPath); + if (outputFile.existsSync()) { + log('Found cached asset updating'); + } else { + log('No previous openapi-generated cache found. Creating cache'); + } + + return await outputFile.writeAsString(jsonEncode(spec), flush: true).then( + (_) => log('Successfully wrote cache.'), + onError: (e, st) => Future.error( + OutputMessage( + message: 'Failed to write cache', + additionalContext: e, + stackTrace: st, + ), + ), + ); +} diff --git a/openapi-generator/lib/src/models/command.dart b/openapi-generator/lib/src/models/command.dart new file mode 100644 index 0000000..6e3e112 --- /dev/null +++ b/openapi-generator/lib/src/models/command.dart @@ -0,0 +1,31 @@ +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; + +/// Creates a representation of a cli request for Flutter or Dart. +class Command { + final String _executable; + final List _arguments; + + String get executable => _executable; + + List get arguments => _arguments; + + Command._(this._executable, this._arguments); + + /// Provides an in memory representation of the Dart of Flutter cli command. + /// + /// If [executable] is the Dart executable or the [wrapper] is [Wrapper.none] + /// it provides the raw executable. Otherwise it wraps it in the appropriate + /// wrapper, flutterw and fvm respectively. + Command({ + Wrapper wrapper = Wrapper.none, + required String executable, + required List arguments, + }) : this._( + executable == 'dart' || wrapper == Wrapper.none + ? executable + : wrapper == Wrapper.flutterw + ? './flutterw' + : 'fvm', + arguments, + ); +} diff --git a/openapi-generator/lib/src/models/generator_arguments.dart b/openapi-generator/lib/src/models/generator_arguments.dart new file mode 100644 index 0000000..af47f00 --- /dev/null +++ b/openapi-generator/lib/src/models/generator_arguments.dart @@ -0,0 +1,258 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:openapi_generator/src/extensions/type_methods.dart'; +import 'package:openapi_generator/src/models/output_message.dart'; +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; +import 'package:source_gen/source_gen.dart' as src_gen; + +import '../utils.dart'; + +/// The default storage location of the cached copy of the specification. +/// +/// When the annotation has the [Openapi.cachePath] set this value isn't used. +final defaultCachedPath = + '${Directory.current.path}${Platform.pathSeparator}.dart_tool${Platform.pathSeparator}openapi-generator-cache.json'; + +/// Represents the Annotation fields passed to the [OpenapiGenerator]. +class GeneratorArguments { + /// Informs the generator to always run on changes. + /// + /// WARNING! This will soon be noop. See [useNextGen] for more + /// details. + /// + /// Default: false + @deprecated + final bool alwaysRun; + + /// Informs the generator to follow the next generation path way. + /// + /// NextGen: + /// The next generation of the [OpenapiGenerator] will always run in the + /// event there is a change to the Openapi specification. In this version of + /// the generator the builder caches an instance of the current [inputFile], + /// if one doesn't already exist, this way in the event that are modifications + /// to the spec they can be generated. That cached copy is a translated + /// JSON copy (see [Yaml library]() about output). + /// + /// Default: false + final bool useNextGen; + + /// The [cachePath] is the location of the translated copy of the [inputFile] + /// before modifications. + /// + /// The default location is: .dart_tool/openapi-generator-cache.json + final String cachePath; + final bool isDebug; + + /// Use a custom pubspec file when generating. + /// + /// Defaults to the pubspec at the root of [Directory.current]. + final String? pubspecPath; + + /// The directory where the generated sources will be placed. + /// + /// Default: Directory.current.path + final String outputDirectory; + + /// Informs the generator to run source gen on the output. + /// + /// Default: true + final bool runSourceGen; + + /// Informs the generator to fetch dependencies within the new generated API. + /// + /// Default: true + final bool shouldFetchDependencies; + + /// Informs the generator to skip validating the OpenApi specification. + /// + /// Default: false + final bool skipValidation; + + /// Use the provided spec instead of one located in [Directory.current]. + /// + /// Default: openapi.(ya?ml) | openapi.json + String _inputFile; + + /// The directory containing the template files. + final String templateDirectory; + + /// Informs the generator what kind of library should be generated. + /// + /// Default: [Generator.dart] + final Generator generator; + + /// Informs the generator to use the specified [wrapper] for Flutter commands. + Wrapper get wrapper => additionalProperties?.wrapper ?? Wrapper.none; + + /// Defines mappings between a class and the import to be used. + final Map importMappings; + + /// Defines mappings between OpenAPI spec types and generated types. + final Map typeMappings; + + /// Adds reserved words mappings. + /// + /// Supported by [Generator.dio] & [Generator.dioAlt] generators. + final Map reservedWordsMappings; + + /// Additional properties to be passed into the OpenAPI compiler. + final AdditionalProperties? additionalProperties; + + /// Defines a mapping for nested (inline) schema and the generated name. + final Map inlineSchemaNameMappings; + + /// Customizes the way inline schema are handled. + final InlineSchemaOptions? inlineSchemaOptions; + + GeneratorArguments({ + required src_gen.ConstantReader annotations, + bool alwaysRun = false, + String inputSpecFile = '', + String templateDirectory = '', + Generator generator = Generator.dart, + Map typeMapping = const {}, + Map importMapping = const {}, + Map reservedWordsMapping = const {}, + Map inlineSchemaNameMapping = const {}, + AdditionalProperties? additionalProperties, + InlineSchemaOptions? inlineSchemaOptions, + bool skipValidation = false, + bool runSourceGen = true, + String? outputDirectory, + bool fetchDependencies = true, + bool useNextGen = false, + String? cachePath, + String? pubspecPath, + bool isDebug = false, + }) : alwaysRun = annotations.readPropertyOrDefault('alwaysRun', alwaysRun), + _inputFile = + annotations.readPropertyOrDefault('inputSpecFile', inputSpecFile), + templateDirectory = annotations.readPropertyOrDefault( + 'templateDirectory', templateDirectory), + generator = + annotations.readPropertyOrDefault('generatorName', generator), + typeMappings = + annotations.readPropertyOrDefault('typeMappings', typeMapping), + importMappings = + annotations.readPropertyOrDefault('importMappings', importMapping), + reservedWordsMappings = annotations.readPropertyOrDefault( + 'reservedWordsMappings', reservedWordsMapping), + inlineSchemaNameMappings = annotations.readPropertyOrDefault( + 'inlineSchemaNameMappings', inlineSchemaNameMapping), + additionalProperties = annotations.readPropertyOrDefault( + 'additionalProperties', additionalProperties), + inlineSchemaOptions = annotations.readPropertyOrDefault( + 'inlineSchemaOptions', inlineSchemaOptions), + skipValidation = annotations.readPropertyOrDefault( + 'skipSpecValidation', skipValidation), + runSourceGen = annotations.readPropertyOrDefault( + 'runSourceGenOnOutput', runSourceGen), + shouldFetchDependencies = annotations.readPropertyOrDefault( + 'fetchDependencies', fetchDependencies), + outputDirectory = annotations.readPropertyOrDefault( + 'outputDirectory', outputDirectory ?? Directory.current.path), + useNextGen = + annotations.readPropertyOrDefault('useNextGen', useNextGen), + cachePath = annotations.readPropertyOrDefault( + 'cachePath', cachePath ?? defaultCachedPath), + pubspecPath = annotations.readPropertyOrDefault( + 'projectPubspecPath', + pubspecPath ?? + '${Directory.current.path}${Platform.pathSeparator}pubspec.yaml'), + isDebug = annotations.readPropertyOrDefault('debugLogging', isDebug); + + /// The stringified name of the [Generator]. + String get generatorName => generator == Generator.dart + ? 'dart' + : generator == Generator.dio + ? 'dart-dio' + : 'dart2-api'; + + /// Informs the generator to generate source based on the [generator]. + /// + /// This is only false in the case where [generator] is set to [Generator.dart] + /// as that version of the [Generator] uses the 'dart:http' library as the + /// networking layer. + bool get shouldGenerateSources => generator != Generator.dart; + + /// Identifies if the specification is a remote specification. + /// + /// Used when the specification is hosted on an external server. This will cause + /// the compiler to pulls from the remote source. When this is true a cache will + /// still be created but a warning will be emitted to the user. + bool get isRemote => _inputFile.isNotEmpty + ? RegExp(r'^https?://').hasMatch(_inputFile) + : false; + + bool get hasLocalCache => File(cachePath).existsSync(); + + /// Looks for a default spec file within [Directory.current] if [_inputFile] + /// wasn't set. + /// + /// Looks for + /// In the event that a specification file isn't provided look within the + /// project to see if one of the supported defaults, a file named + /// openapi.(ya?ml|json), is present. + /// + /// Subsequent calls will be able to use the [_inputFile] when successful in + /// the event that a default is found. + Future get inputFileOrFetch async { + final curr = Directory.current; + if (_inputFile.isNotEmpty) { + return _inputFile; + } + + try { + final entry = curr.listSync().firstWhere( + (e) => RegExp(r'^.*/(openapi\.(ya?ml|json))$').hasMatch(e.path)); + _inputFile = entry.path; + return _inputFile; + } catch (e, st) { + return Future.error( + OutputMessage( + message: + 'No spec file found. One must be present in the project or hosted remotely.', + level: Level.SEVERE, + additionalContext: e, + stackTrace: st, + ), + ); + } + } + + /// The arguments to be passed to generator jar file. + FutureOr> get jarArgs async => [ + 'generate', + if (outputDirectory.isNotEmpty) '-o=$outputDirectory', + '-i=${await inputFileOrFetch}', + if (templateDirectory.isNotEmpty) '-t=$templateDirectory', + '-g=$generatorName', + if (skipValidation) '--skip-validate-spec', + if (reservedWordsMappings.isNotEmpty) + '--reserved-words-mappings=${reservedWordsMappings.entries.fold('', foldStringMap())}', + if (inlineSchemaNameMappings.isNotEmpty) + '--inline-schema-name-mappings=${inlineSchemaNameMappings.entries.fold('', foldStringMap())}', + if (importMappings.isNotEmpty) + '--import-mappings=${importMappings.entries.fold('', foldStringMap())}', + if (typeMappings.isNotEmpty) + '--type-mappings=${typeMappings.entries.fold('', foldStringMap())}', + if (inlineSchemaOptions != null) + '--inline-schema-options=${inlineSchemaOptions!.toMap().entries.fold('', foldStringMap(keyModifier: convertToPropertyKey))}', + if (additionalProperties != null) + '--additional-properties=${convertAdditionalProperties(additionalProperties!).fold('', foldStringMap(keyModifier: convertToPropertyKey))}' + ]; + + Iterable> convertAdditionalProperties( + AdditionalProperties props) { + if (props is DioProperties) { + return props.toMap().entries; + } else if (props is DioAltProperties) { + return props.toMap().entries; + } else { + return props.toMap().entries; + } + } +} diff --git a/openapi-generator/lib/src/models/output_message.dart b/openapi-generator/lib/src/models/output_message.dart new file mode 100644 index 0000000..0b12646 --- /dev/null +++ b/openapi-generator/lib/src/models/output_message.dart @@ -0,0 +1,24 @@ +import 'package:logging/logging.dart'; + +/// A message to be displayed to the end user. +/// +/// Provides a common base shape to report logs to the end user. Also, acts as an +/// error wrapper. +class OutputMessage { + final Level level; + final String message; + final Object? additionalContext; + final StackTrace? stackTrace; + + const OutputMessage({ + required this.message, + this.level = Level.INFO, + this.additionalContext, + this.stackTrace, + }); + + @override + String toString() { + return '$message ${additionalContext ?? ''} ${stackTrace ?? ''}'; + } +} diff --git a/openapi-generator/lib/src/openapi_generator_runner.dart b/openapi-generator/lib/src/openapi_generator_runner.dart index b9f8cd8..1fd7c7c 100755 --- a/openapi-generator/lib/src/openapi_generator_runner.dart +++ b/openapi-generator/lib/src/openapi_generator_runner.dart @@ -1,16 +1,21 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; -import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:build/build.dart'; +import 'package:logging/logging.dart'; +import 'package:openapi_generator/src/determine_flutter_project_status.dart'; +import 'package:openapi_generator/src/gen_on_spec_changes.dart'; +import 'package:openapi_generator/src/models/output_message.dart'; +import 'package:openapi_generator/src/utils.dart'; import 'package:openapi_generator_annotations/openapi_generator_annotations.dart' as annots; -import 'package:path/path.dart' as path; import 'package:source_gen/source_gen.dart'; -import 'extensions/type_methods.dart'; +import 'models/command.dart'; +import 'models/generator_arguments.dart'; class OpenapiGenerator extends GeneratorForAnnotation { final bool testMode; @@ -19,375 +24,474 @@ class OpenapiGenerator extends GeneratorForAnnotation { @override FutureOr generateForAnnotatedElement( - Element element, ConstantReader annotation, BuildStep buildStep) async { - log.info(' - :::::::::::::::::::::::::::::::::::::::::::'); - log.info(' - :: Openapi generator for dart ::'); - log.info(' - :::::::::::::::::::::::::::::::::::::::::::'); + Element element, ConstantReader annotations, BuildStep buildStep) async { + logOutputMessage( + log: log, + communication: OutputMessage( + message: [ + '', + ':::::::::::::::::::::::::::::::::::::::::::', + ':: Openapi generator for dart ::', + ':::::::::::::::::::::::::::::::::::::::::::', + ].join('\n'), + ), + ); try { if (element is! ClassElement) { final friendlyName = element.displayName; + throw InvalidGenerationSourceError( 'Generator cannot target `$friendlyName`.', todo: 'Remove the [Openapi] annotation from `$friendlyName`.', ); - } - var separator = '?*?'; - var openApiCliCommand = 'generate'; - - openApiCliCommand = - appendInputFileCommandArgs(annotation, openApiCliCommand, separator); - - openApiCliCommand = appendTemplateDirCommandArgs( - annotation, openApiCliCommand, separator); - - var generatorName = - annotation.peek('generatorName')?.enumValue(); - var generator = getGeneratorNameFromEnum(generatorName!); - openApiCliCommand = '$openApiCliCommand$separator-g$separator$generator'; - - var outputDirectory = - _readFieldValueAsString(annotation, 'outputDirectory', ''); - if (outputDirectory.isNotEmpty) { - var alwaysRun = _readFieldValueAsBool(annotation, 'alwaysRun', false)!; - var filePath = path.join(outputDirectory, 'lib/api.dart'); - if (!alwaysRun && await File(filePath).exists()) { - print( - 'OpenapiGenerator :: Codegen skipped because alwaysRun is set to [$alwaysRun] and $filePath already exists'); - return ''; + } else { + if (!(annotations.read('useNextGen').literalValue as bool)) { + if (annotations.read('cachePath').literalValue != null) { + throw InvalidGenerationSourceError( + 'useNextGen must be set when using cachePath', + todo: + 'Either set useNextGen: true on the annotation or remove the custom cachePath', + ); + } } - openApiCliCommand = - '$openApiCliCommand$separator-o$separator$outputDirectory'; - } - - openApiCliCommand = appendTypeMappingCommandArgs( - annotation, openApiCliCommand, separator); - - openApiCliCommand = appendImportMappingCommandArgs( - annotation, openApiCliCommand, separator); - - openApiCliCommand = appendReservedWordsMappingCommandArgs( - annotation, openApiCliCommand, separator); - - openApiCliCommand = appendInlineSchemaNameMappingCommandArgs( - annotation, openApiCliCommand, separator); - - openApiCliCommand = appendAdditionalPropertiesCommandArgs( - annotation, openApiCliCommand, separator); - - // openApiCliCommand = appendInlineSchemeOptionsCommandArgs( - // annotation, openApiCliCommand, separator); - - openApiCliCommand = appendSkipValidateSpecCommandArgs( - annotation, openApiCliCommand, separator); - log.info( - 'OpenapiGenerator :: [${openApiCliCommand.replaceAll(separator, ' ')}]'); - - var binPath = (await Isolate.resolvePackageUri(Uri.parse( - 'package:openapi_generator_cli/openapi-generator.jar')))! - .toFilePath(windows: Platform.isWindows); - - // Include java environment variables in openApiCliCommand - var javaOpts = Platform.environment['JAVA_OPTS'] ?? ''; - - var arguments = [ - '-jar', - "${"$binPath"}", - ...openApiCliCommand.split(separator).toList(), - ]; - if (javaOpts.isNotEmpty) { - arguments.insert(0, javaOpts); - } - - var exitCode = 0; - var pr = await Process.run('java', arguments); - if (pr.exitCode != 0) { - log.severe(pr.stderr); - } - - log.info( - ' - :: Codegen ${pr.exitCode != 0 ? 'Failed' : 'completed successfully'}'); - exitCode = pr.exitCode; - - if (!_readFieldValueAsBool(annotation, 'fetchDependencies')!) { - log.warning(' - :: Skipping install step because you said so...'); - return ''; - } - - if (exitCode == 0) { - final command = - _getCommandWithWrapper('flutter', ['pub', 'get'], annotation); - var installOutput = await Process.run( - command.executable, command.arguments, - runInShell: Platform.isWindows, - workingDirectory: '$outputDirectory'); - - if (installOutput.exitCode != 0) { - log.severe(installOutput.stderr); + // Transform the annotations. + final args = GeneratorArguments(annotations: annotations); + + // Determine if the project has a dependency on the flutter sdk or not. + final baseCommand = await checkPubspecAndWrapperForFlutterSupport( + wrapper: args.wrapper, providedPubspecPath: args.pubspecPath) + ? 'flutter' + : 'dart'; + + if (!args.useNextGen) { + final path = + '${args.outputDirectory}${Platform.pathSeparator}lib${Platform.pathSeparator}api.dart'; + if (await File(path).exists()) { + if (!args.alwaysRun) { + logOutputMessage( + log: log, + communication: OutputMessage( + message: + '- :: Library exists definition at [$path] exists and configuration is annotated with alwaysRun: [${args.alwaysRun}]. This option will be removed in a future version. ::', + level: Level.WARNING, + ), + ); + return ''; + } + } + } else { + // If the flag to use the next generation of the generator is applied + // use the new functionality. + return generatorV2( + args: args, + baseCommand: baseCommand, + annotatedPath: buildStep.inputId.path); } - print(' :: Install exited with code ${installOutput.exitCode}'); - exitCode = installOutput.exitCode; - } - if (!_readFieldValueAsBool(annotation, 'runSourceGenOnOutput')!) { - log.warning(' :: Skipping source gen step because you said so...'); - return ''; + await runOpenApiJar(arguments: args); + await fetchDependencies(baseCommand: baseCommand, args: args); + await generateSources(baseCommand: baseCommand, args: args); } - - if (exitCode == 0) { - //run buildrunner to generate files - switch (generatorName) { - case annots.Generator.dart: - log.info( - ' :: skipping source gen because generator does not need it ::'); - break; - case annots.Generator.dio: - case annots.Generator.dioAlt: - try { - var runnerOutput = - await runSourceGen(annotation, outputDirectory); - if (runnerOutput.exitCode != 0) { - log.severe(runnerOutput.stderr); - } - log.info( - ' :: build runner exited with code ${runnerOutput.exitCode} ::'); - } catch (e) { - log.severe(e); - log.severe(' :: could not complete source gen ::'); - } - break; - } + } catch (e, st) { + late OutputMessage communication; + if (e is! OutputMessage) { + communication = OutputMessage( + message: '- :: There was an error generating the spec. ::', + level: Level.SEVERE, + additionalContext: e, + stackTrace: st, + ); + } else { + communication = e; } - } catch (e) { - log.severe('Error generating spec $e'); - rethrow; + + logOutputMessage(log: log, communication: communication); + } finally { + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':::::::::::::::::::::::::::::::::::::::::::', + ), + ); } return ''; } - Future runSourceGen( - ConstantReader annotation, String outputDirectory) async { - log.info(':: running source code generation ::'); - var c = 'pub run build_runner build --delete-conflicting-outputs'; - final command = - _getCommandWithWrapper('flutter', c.split(' ').toList(), annotation); - ProcessResult runnerOutput; - runnerOutput = await Process.run(command.executable, command.arguments, - runInShell: Platform.isWindows, workingDirectory: '$outputDirectory'); - log.severe(runnerOutput.stderr); - return runnerOutput; - } - - String appendAdditionalPropertiesCommandArgs( - ConstantReader annotation, String command, String separator) { - var additionalProperties = ''; - var reader = annotation.read('additionalProperties'); - if (!reader.isNull) { - reader.revive().namedArguments.entries.forEach((entry) => { - additionalProperties = - '$additionalProperties${additionalProperties.isEmpty ? '' : ','}${convertToPropertyKey(entry.key)}=${convertToPropertyValue(entry.value)}' - }); + /// Runs the OpenAPI compiler with the given [args]. + Future runOpenApiJar({required GeneratorArguments arguments}) async { + final args = await arguments.jarArgs; + logOutputMessage( + log: log, + communication: OutputMessage( + message: 'OpenapiGenerator :: [ ${args.join(' ')} ]', + ), + ); + + var binPath = (await Isolate.resolvePackageUri( + Uri.parse('package:openapi_generator_cli/openapi-generator.jar')))! + .toFilePath(windows: Platform.isWindows); + + // Include java environment variables in openApiCliCommand + var javaOpts = Platform.environment['JAVA_OPTS'] ?? ''; + + ProcessResult result; + if (!testMode) { + result = await Process.run( + 'java', + [ + if (javaOpts.isNotEmpty) javaOpts, + '-jar', + binPath, + ...args, + ], + workingDirectory: Directory.current.path, + runInShell: Platform.isWindows, + ); + } else { + result = ProcessResult(999999, 0, null, null); } - if (additionalProperties.isNotEmpty) { - command = - '$command$separator--additional-properties=$additionalProperties'; + if (result.exitCode != 0) { + return Future.error( + OutputMessage( + message: ':: Codegen Failed. Generator output: ::', + level: Level.SEVERE, + additionalContext: result.stderr, + stackTrace: StackTrace.current, + ), + ); + } else { + logOutputMessage( + log: log, + communication: OutputMessage( + message: [ + if (arguments.isDebug) result.stdout, + ':: Codegen completed successfully. ::', + ].join('\n'), + ), + ); } - return command; } - String appendInlineSchemeOptionsCommandArgs( - ConstantReader annotation, String command, String separator) { - var inlineSchemaOptions = ''; - var reader = annotation.read('inlineSchemaOptions'); - if (!reader.isNull) { - reader.revive().namedArguments.entries.forEach((entry) => { - inlineSchemaOptions = - '$inlineSchemaOptions${inlineSchemaOptions.isEmpty ? '' : ','}${convertToPropertyKey(entry.key)}=${convertToPropertyValue(entry.value)}' - }); + /// Next-gen of the generation. + /// + /// Proposal for reworking how to generated the user's changes based on spec + /// changes vs flags. This will allow for incremental changes to be generated + /// in the specification instead of only running when the configuration file + /// changes as it should be relatively stable. + FutureOr generatorV2( + {required GeneratorArguments args, + required String baseCommand, + required String annotatedPath}) async { + if (args.isRemote) { + logOutputMessage( + log: log, + communication: OutputMessage( + message: + ':: Using a remote specification, a cache will still be create but may be outdated. ::', + level: Level.WARNING, + ), + ); } - - if (inlineSchemaOptions.isNotEmpty) { - command = - '$command$separator--inline-schema-options $inlineSchemaOptions'; + try { + if (!await hasDiff(args: args)) { + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: No diff between versions, not running generator. ::', + ), + ); + } else { + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: Dirty Spec found. Running generation. ::', + ), + ); + await runOpenApiJar(arguments: args); + await fetchDependencies(baseCommand: baseCommand, args: args); + await generateSources(baseCommand: baseCommand, args: args); + if (!args.hasLocalCache) { + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: No local cache found. Creating one. ::', + ), + ); + } else { + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: Local cache found. Overwriting existing one. ::', + ), + ); + } + await cacheSpec( + outputLocation: args.cachePath, + spec: await loadSpec(specPath: await args.inputFileOrFetch)); + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: Successfully cached spec changes. ::', + ), + ); + } + } catch (e, st) { + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: Failed to generate content. ::', + additionalContext: e, + stackTrace: st, + level: Level.SEVERE, + ), + ); + } finally { + await updateAnnotatedFile(annotatedPath: annotatedPath).then( + (_) => logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: Successfully updated annotated file. ::', + ), + ), + onError: (e, st) => logOutputMessage( + log: log, + communication: OutputMessage( + message: 'Failed to update annotated class file.', + level: Level.SEVERE, + additionalContext: e, + stackTrace: st, + ), + ), + ); + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':::::::::::::::::::::::::::::::::', + ), + ); } - return command; + return ''; } - String appendTypeMappingCommandArgs( - ConstantReader annotation, String command, String separator) { - var typeMappingsMap = _readFieldValueAsMap(annotation, 'typeMappings', {})!; - if (typeMappingsMap.isNotEmpty) { - command = - '$command$separator--type-mappings=${getMapAsString(typeMappingsMap)}'; - } - return command; + /// Load both specs into memory and verify if there is a diff between them. + FutureOr hasDiff({required GeneratorArguments args}) async { + final cachedSpec = await loadSpec(specPath: args.cachePath, isCached: true); + final loadedSpec = await loadSpec(specPath: await args.inputFileOrFetch); + + logOutputMessage( + log: log, + communication: OutputMessage( + message: [ + ':: Loaded cached and current spec files. ::', + if (args.isDebug) ...[jsonEncode(cachedSpec), jsonEncode(loadedSpec)], + ].join('\n'), + ), + ); + + return isSpecDirty(cachedSpec: cachedSpec, loadedSpec: loadedSpec); } - String appendImportMappingCommandArgs( - ConstantReader annotation, String command, String separator) { - var importMappings = - _readFieldValueAsMap(annotation, 'importMappings', {})!; - if (importMappings.isNotEmpty) { - command = - '$command$separator--import-mappings=${getMapAsString(importMappings)}'; + /// Conditionally generates the new sources based on the [args.runSourceGen] & + /// [args.generator]. + FutureOr generateSources( + {required String baseCommand, required GeneratorArguments args}) async { + if (!args.runSourceGen) { + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: Skipping source gen step due to flag being set. ::', + level: Level.WARNING, + ), + ); + } else if (!args.shouldGenerateSources) { + logOutputMessage( + log: log, + communication: OutputMessage( + message: + ':: Skipping source gen because generator does not need it. ::', + ), + ); + } else { + return await runSourceGen(baseCommand: baseCommand, args: args).then( + (_) => logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: Sources generated successfully. ::', + ), + ), + onError: (e, st) => Future.error( + OutputMessage( + message: ':: Could not complete source generation ::', + additionalContext: e, + stackTrace: st, + level: Level.SEVERE, + ), + ), + ); } - return command; } - String appendReservedWordsMappingCommandArgs( - ConstantReader annotation, String command, String separator) { - var reservedWordsMappingsMap = - _readFieldValueAsMap(annotation, 'reservedWordsMappings', {})!; - if (reservedWordsMappingsMap.isNotEmpty) { - command = - '$command$separator--reserved-words-mappings=${getMapAsString(reservedWordsMappingsMap)}'; + /// Runs build_runner on the newly generated library in [args.outputDirectory]. + Future runSourceGen( + {required String baseCommand, required GeneratorArguments args}) async { + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: Running source code generation. ::', + ), + ); + final command = Command( + executable: baseCommand, + arguments: 'pub run build_runner build --delete-conflicting-outputs' + .split(' ') + .toList(), + wrapper: args.wrapper); + + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: ${command.executable} ${command.arguments.join(' ')} ::', + ), + ); + + ProcessResult results; + if (!testMode) { + results = await Process.run( + command.executable, + command.arguments, + runInShell: Platform.isWindows, + workingDirectory: args.outputDirectory, + ); + } else { + results = ProcessResult(99999, 0, null, null); } - return command; - } - String appendInlineSchemaNameMappingCommandArgs( - ConstantReader annotation, String command, String separator) { - var inlineSchemaNameMappings = - _readFieldValueAsMap(annotation, 'inlineSchemaNameMappings', {})!; - if (inlineSchemaNameMappings.isNotEmpty) { - command = - '$command$separator--inline-schema-name-mappings=${getMapAsString(inlineSchemaNameMappings)}'; + if (results.exitCode != 0) { + return Future.error( + OutputMessage( + message: + ':: Failed to generate source code. Build Command output: ::', + level: Level.SEVERE, + additionalContext: results.stderr, + stackTrace: StackTrace.current, + ), + ); + } else { + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: Codegen completed successfully. ::', + ), + ); } - return command; } - String getGeneratorNameFromEnum(annots.Generator generator) { - var genName = 'dart'; - switch (generator) { - case annots.Generator.dart: - break; - case annots.Generator.dio: - genName = 'dart-dio'; - break; - case annots.Generator.dioAlt: - genName = 'dart2-api'; - break; - default: - throw InvalidGenerationSourceError( - 'Generator name must be any of ${annots.Generator.values}.', + /// Conditionally fetches the dependencies in the newly generate library. + FutureOr fetchDependencies( + {required String baseCommand, required GeneratorArguments args}) async { + if (!args.shouldFetchDependencies) { + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: Skipping install step because flag was set. ::', + level: Level.WARNING, + ), + ); + } else { + final command = Command( + executable: baseCommand, + arguments: ['pub', 'get'], + wrapper: args.wrapper); + + logOutputMessage( + log: log, + communication: OutputMessage( + message: + ':: Installing dependencies with generated source. ${command.executable} ${command.arguments.join(' ')} ::', + ), + ); + + ProcessResult results; + if (!testMode) { + results = await Process.run( + command.executable, + command.arguments, + runInShell: Platform.isWindows, + workingDirectory: args.outputDirectory, ); - } - return genName; - } - - String appendTemplateDirCommandArgs( - ConstantReader annotation, String command, String separator) { - var templateDir = - _readFieldValueAsString(annotation, 'templateDirectory', ''); - if (templateDir.isNotEmpty) { - command = '$command$separator-t$separator$templateDir'; - } - return command; - } - - String appendInputFileCommandArgs( - ConstantReader annotation, String command, String separator) { - var inputFile = _readFieldValueAsString(annotation, 'inputSpecFile', ''); - if (inputFile.isNotEmpty) { - command = '$command$separator-i$separator$inputFile'; - } - return command; - } - - String appendSkipValidateSpecCommandArgs( - ConstantReader annotation, String command, String separator) { - var skipSpecValidation = - _readFieldValueAsBool(annotation, 'skipSpecValidation', false)!; - if (skipSpecValidation) { - command = '$command$separator--skip-validate-spec'; - } - return command; - } - - String getMapAsString(Map data) { - return data.entries - .map((entry) => - '${entry.key.toStringValue()}=${entry.value.toStringValue()}') - .join(','); - } + } else { + results = ProcessResult(999999, 0, null, null); + } - Command _getCommandWithWrapper( - String command, List arguments, ConstantReader annotation) { - final wrapper = annotation - .read('additionalProperties') - .read('wrapper') - .enumValue(); - switch (wrapper) { - case annots.Wrapper.flutterw: - return Command('./flutterw', arguments); - case annots.Wrapper.fvm: - return Command('fvm', [command, ...arguments]); - case annots.Wrapper.none: - default: - return Command(command, arguments); + if (results.exitCode != 0) { + return Future.error( + OutputMessage( + message: ':: Install within generated sources failed. ::', + level: Level.SEVERE, + additionalContext: results.stderr, + stackTrace: StackTrace.current, + ), + ); + } else { + logOutputMessage( + log: log, + communication: OutputMessage( + message: [ + if (args.isDebug) results.stdout, + ':: Install completed successfully. ::', + ].join('\n'), + ), + ); + } } } - String _readFieldValueAsString( - ConstantReader annotation, String fieldName, String defaultValue) { - var reader = annotation.read(fieldName); - - return reader.isNull ? defaultValue : reader.stringValue; - } - - Map? _readFieldValueAsMap(ConstantReader annotation, String fieldName, - [Map? defaultValue]) { - var reader = annotation.read(fieldName); - - return reader.isNull ? defaultValue : reader.mapValue; - } - - bool? _readFieldValueAsBool(ConstantReader annotation, String fieldName, - [bool? defaultValue]) { - var reader = annotation.read(fieldName); - - return reader.isNull ? defaultValue : reader.boolValue; - } - - String convertToPropertyKey(String key) { - switch (key) { - case 'nullSafeArrayDefault': - return 'nullSafe-array-default'; - case 'pubspecDependencies': - return 'pubspec-dependencies'; - case 'pubspecDevDependencies': - return 'pubspec-dev-dependencies'; - case 'arrayItemSuffix': - return 'ARRAY_ITEM_SUFFIX'; - case 'mapItemSuffix': - return 'MAP_ITEM_SUFFIX'; - case 'skipSchemaReuse': - return 'SKIP_SCHEMA_REUSE'; - case 'refactorAllofInlineSchemas': - return 'REFACTOR_ALLOF_INLINE_SCHEMAS'; - case 'resolveInlineEnums': - return 'RESOLVE_INLINE_ENUMS'; + /// Update the currently cached spec with the [updatedSpec]. + Future updateCachedSpec({ + required Map updatedSpec, + required String cachedPath, + }) async => + cacheSpec(spec: updatedSpec, outputLocation: cachedPath); + + Future updateAnnotatedFile({required annotatedPath}) async { + // The should exist since that is what triggered the build to begin with so + // there is no point in verifying it exists. It is also a relative file since + // it exists within the project. + final f = File(annotatedPath); + var content = f.readAsLinesSync(); + final now = DateTime.now().toIso8601String(); + final generated = '$lastRunPlaceHolder: $now'; + if (content.first.contains(lastRunPlaceHolder)) { + content = content.sublist(1); + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: Found generated timestamp. Updating with $now ::', + ), + ); + } else { + logOutputMessage( + log: log, + communication: OutputMessage( + message: ':: Creating generated timestamp with $now ::', + ), + ); } - return key; - } - - String convertToPropertyValue(DartObject value) { - if (value.isNull) { - return ''; + try { + content.insert(0, generated); + f.writeAsStringSync(content.join('\n'), flush: true); + } catch (e, st) { + return Future.error( + OutputMessage( + message: 'Failed to update the annotated class file.', + additionalContext: e, + stackTrace: st, + level: Level.SEVERE, + ), + ); } - return value.toStringValue() ?? - value.toBoolValue()?.toString() ?? - value.toIntValue()?.toString() ?? - value.getField('_name')?.toStringValue() ?? - ''; } } - -class Command { - final String executable; - final List arguments; - - Command(this.executable, this.arguments); -} diff --git a/openapi-generator/lib/src/utils.dart b/openapi-generator/lib/src/utils.dart new file mode 100644 index 0000000..f7cf8ae --- /dev/null +++ b/openapi-generator/lib/src/utils.dart @@ -0,0 +1,63 @@ +import 'package:analyzer/dart/constant/value.dart'; +import 'package:logging/logging.dart'; + +import 'models/output_message.dart'; + +/// A utility function that prints out a log meant for the end user. +void logOutputMessage( + {required Logger log, required OutputMessage communication}) => + log.log(communication.level, communication.message + '\n', + communication.additionalContext, communication.stackTrace); + +/// Transforms a [Map] into a string. +String getMapAsString(Map data) { + return data.entries + .map((entry) => + '${entry.key.toStringValue()}=${entry.value.toStringValue()}') + .join(','); +} + +/// Converts a [DartObject] to it's given type. +String convertToPropertyValue(DartObject value) { + if (value.isNull) { + return ''; + } + return value.toStringValue() ?? + value.toBoolValue()?.toString() ?? + value.toIntValue()?.toString() ?? + value.getField('_name')?.toStringValue() ?? + ''; +} + +/// Converts a key into an expected field name. +String convertToPropertyKey(String key) { + switch (key) { + case 'nullSafeArrayDefault': + return 'nullSafe-array-default'; + case 'pubspecDependencies': + return 'pubspec-dependencies'; + case 'pubspecDevDependencies': + return 'pubspec-dev-dependencies'; + case 'arrayItemSuffix': + return 'ARRAY_ITEM_SUFFIX'; + case 'mapItemSuffix': + return 'MAP_ITEM_SUFFIX'; + case 'skipSchemaReuse': + return 'SKIP_SCHEMA_REUSE'; + case 'refactorAllofInlineSchemas': + return 'REFACTOR_ALLOF_INLINE_SCHEMAS'; + case 'resolveInlineEnums': + return 'RESOLVE_INLINE_ENUMS'; + } + return key; +} + +String Function(String, MapEntry) foldStringMap({ + String Function(String)? keyModifier, + String Function(dynamic)? valueModifier, +}) => + (String prev, MapEntry curr) => + '${prev.trim().isEmpty ? '' : '$prev,'}${keyModifier != null ? keyModifier(curr.key) : curr.key}=${valueModifier != null ? valueModifier(curr.value) : curr.value}'; + +final lastRunPlaceHolder = + '// GENERATED DO NOT MODIFY BY HAND: openapi-generator-last-run'; diff --git a/openapi-generator/pubspec.yaml b/openapi-generator/pubspec.yaml index 1797dfc..1b77aeb 100755 --- a/openapi-generator/pubspec.yaml +++ b/openapi-generator/pubspec.yaml @@ -4,7 +4,7 @@ version: 4.11.0 homepage: https://github.com/gibahjoe/openapi-generator-dart environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.15.0 <3.0.0' dependencies: build: '>=0.12.6 <=3.0.0' @@ -13,11 +13,17 @@ dependencies: openapi_generator_annotations: ^4.11.0 analyzer: '>=2.0.0 <=6.0.0' openapi_generator_cli: ^4.11.0 + yaml: ^3.1.2 + http: ^1.1.0 dev_dependencies: test: ^1.24.2 build_runner: '>=1.0.0 <3.0.0' build_test: '>=1.2.0 <3.0.0' + source_gen_test: ^1.0.6 pedantic: coverage: ^1.6.3 +dependency_overrides: + openapi_generator_annotations: + path: ../openapi-generator-annotations \ No newline at end of file diff --git a/openapi-generator/test/builder_test.dart b/openapi-generator/test/builder_test.dart index 3d5d69e..6ddd401 100644 --- a/openapi-generator/test/builder_test.dart +++ b/openapi-generator/test/builder_test.dart @@ -1,12 +1,17 @@ +import 'dart:convert'; import 'dart:io'; -import 'package:build/build.dart'; import 'package:build_test/build_test.dart'; -import 'package:openapi_generator/src/openapi_generator_runner.dart'; +import 'package:openapi_generator/src/gen_on_spec_changes.dart'; +import 'package:openapi_generator/src/models/generator_arguments.dart'; +import 'package:openapi_generator/src/utils.dart'; +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; import 'package:source_gen/source_gen.dart'; import 'package:test/expect.dart'; import 'package:test/scaffolding.dart'; +import 'utils.dart'; + /// We test the build runner by mocking the specs and then checking the output /// content for the expected generate command. void main() { @@ -25,7 +30,7 @@ void main() { outputDirectory: 'api/petstore_api') '''), contains( - "generate -i ../openapi-spec.yaml -g dart-dio -o api/petstore_api --type-mappings=Pet=ExamplePet --additional-properties=pubName=petstore_api,pubAuthor=Johnny dep...")); + 'generate -o=api/petstore_api -i=../openapi-spec.yaml -g=dart-dio --type-mappings=Pet=ExamplePet --additional-properties=allowUnicodeIdentifiers=false,ensureUniqueParams=true,useEnumExtension=true,prependFormOrBodyParameters=false,pubAuthor=Johnny dep...,pubName=petstore_api,legacyDiscriminatorBehavior=true,sortModelPropertiesByRequiredFlag=true,sortParamsByRequiredFlag=true,wrapper=none')); }); test('to generate command with import and type mappings', () async { @@ -35,10 +40,12 @@ void main() { inputSpecFile: '../openapi-spec.yaml', typeMappings: {'int-or-string':'IntOrString'}, importMappings: {'IntOrString':'./int_or_string.dart'}, - generatorName: Generator.dio) + generatorName: Generator.dio, + outputDirectory: '${testSpecPath}output', + ) '''), contains( - 'generate -i ../openapi-spec.yaml -g dart-dio --type-mappings=int-or-string=IntOrString --import-mappings=IntOrString=./int_or_string.dart')); + 'generate -o=${testSpecPath}output -i=../openapi-spec.yaml -g=dart-dio --import-mappings=IntOrString=./int_or_string.dart --type-mappings=int-or-string=IntOrString')); }); test('to generate command with inline schema mappings', () async { @@ -48,10 +55,12 @@ void main() { inputSpecFile: '../openapi-spec.yaml', typeMappings: {'int-or-string':'IntOrString'}, inlineSchemaNameMappings: {'inline_object_2':'SomethingMapped','inline_object_4':'nothing_new'}, - generatorName: Generator.dio) + generatorName: Generator.dio, + outputDirectory: '${testSpecPath}output', + ) '''), contains(''' - generate -i ../openapi-spec.yaml -g dart-dio --type-mappings=int-or-string=IntOrString --inline-schema-name-mappings=inline_object_2=SomethingMapped,inline_object_4=nothing_new + generate -o=${testSpecPath}output -i=../openapi-spec.yaml -g=dart-dio --inline-schema-name-mappings=inline_object_2=SomethingMapped,inline_object_4=nothing_new --type-mappings=int-or-string=IntOrString ''' .trim())); }); @@ -83,12 +92,12 @@ void main() { outputDirectory: 'api/petstore_api') '''), contains(''' - generate -i ../openapi-spec.yaml -g dart-dio -o api/petstore_api --type-mappings=Pet=ExamplePet --additional-properties=pubName=petstore_api,pubAuthor=Johnny dep... + generate -o=api/petstore_api -i=../openapi-spec.yaml -g=dart-dio --type-mappings=Pet=ExamplePet --additional-properties=allowUnicodeIdentifiers=false,ensureUniqueParams=true,useEnumExtension=true,prependFormOrBodyParameters=false,pubAuthor=Johnny dep...,pubName=petstore_api,legacyDiscriminatorBehavior=true,sortModelPropertiesByRequiredFlag=true,sortParamsByRequiredFlag=true,wrapper=none ''' .trim())); }); - test('to generate command with import and type mapprings for dioAlt', + test('to generate command with import and type mappings for dioAlt', () async { expect( await generate(''' @@ -96,342 +105,441 @@ void main() { inputSpecFile: '../openapi-spec.yaml', typeMappings: {'int-or-string':'IntOrString'}, importMappings: {'IntOrString':'./int_or_string.dart'}, - generatorName: Generator.dioAlt) + generatorName: Generator.dioAlt, + outputDirectory: '${testSpecPath}output', + ) '''), contains( - 'generate -i ../openapi-spec.yaml -g dart2-api --type-mappings=int-or-string=IntOrString --import-mappings=IntOrString=./int_or_string.dart')); + 'generate -o=${testSpecPath}output -i=../openapi-spec.yaml -g=dart2-api --import-mappings=IntOrString=./int_or_string.dart --type-mappings=int-or-string=IntOrString')); }); }); -} -// Test setup. + group('NextGen', () { + late String generatedOutput; + final specPath = + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml'; + final basePath = '${testSpecPath}output-nextgen/'; + final f = File('${basePath}cache.json'); + tearDown(() { + final b = File(basePath); + if (b.existsSync()) b.deleteSync(recursive: true); + }); -final String pkgName = 'pkg'; + group('runs', () { + setUpAll(() { + if (!f.existsSync()) { + f.createSync(recursive: true); + } + f.writeAsStringSync('{}'); + }); + tearDown(() { + if (f.existsSync()) { + f.deleteSync(); + } + }); + test('fails with invalid configuration', () async { + generatedOutput = await generate(''' + @Openapi( + inputSpecFile: '$specPath', + typeMappings: {'int-or-string':'IntOrString'}, + importMappings: {'IntOrString':'./int_or_string.dart'}, + generatorName: Generator.dioAlt, + useNextGen: false, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/invalid_config/' + ) + '''); + expect(generatedOutput, + contains('useNextGen must be set when using cachePath')); + }); + test('Logs warning when using remote spec', () async { + generatedOutput = await generate(''' + @Openapi( + inputSpecFile: '$specPath', + typeMappings: {'int-or-string':'IntOrString'}, + importMappings: {'IntOrString':'./int_or_string.dart'}, + generatorName: Generator.dioAlt, + useNextGen: true, + outputDirectory: '${f.parent.path}/logs-when-remote' + ) + '''); + expect( + generatedOutput, + contains( + ':: Using a remote specification, a cache will still be create but may be outdated. ::')); + }); + test('when the spec is dirty', () async { + final src = ''' + @Openapi( + inputSpecFile: '$specPath', + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/when-spec-is-dirty' + ) + '''; + generatedOutput = await generate(src); + expect( + generatedOutput, contains('Dirty Spec found. Running generation.')); + }); + test('and terminates early when there is no diff', () async { + f.writeAsStringSync(jsonEncode(await loadSpec(specPath: specPath))); + final src = ''' + @Openapi( + inputSpecFile: '$specPath', + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/early-term' + ) + '''; + generatedOutput = await generate(src); + expect(generatedOutput, + contains(':: No diff between versions, not running generator. ::')); + }); + test('openApiJar with expected args', () async { + f.writeAsStringSync(jsonEncode({'someKey': 'someValue'})); + final annotations = (await resolveSource( + File('$testSpecPath/next_gen_builder_test_config.dart') + .readAsStringSync(), + (resolver) async => + (await resolver.findLibraryByName('test_lib'))!)) + .getClass('TestClassConfig')! + .metadata + .map((e) => ConstantReader(e.computeConstantValue()!)) + .first; + final args = GeneratorArguments(annotations: annotations); + generatedOutput = await generate(''' + @Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: './test/specs/output-nextgen/expected-args' +) + '''); + expect( + generatedOutput, + contains( + 'OpenapiGenerator :: [ ${(await args.jarArgs).join(' ')} ]')); + }); + test('adds generated comment', () async { + f.writeAsStringSync(jsonEncode({'someKey': 'someValue'})); + final contents = File('$testSpecPath/next_gen_builder_test_config.dart') + .readAsStringSync(); + final copy = + File('./test/specs/next_gen_builder_test_config_copy.dart'); + copy.writeAsStringSync(contents, flush: true); + generatedOutput = await generate(''' + @Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: './test/specs/output-nextgen/add-generated-comment' +) + ''', path: copy.path); + + var hasOutput = copy.readAsStringSync().contains(lastRunPlaceHolder); + expect(generatedOutput, contains('Creating generated timestamp with ')); -final Builder builder = LibraryBuilder(OpenapiGenerator(), - generatedExtension: '.openapi_generator'); + generatedOutput = await generate(''' + @Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: './test/specs/output-nextgen/add-generated-comment' +) + ''', path: copy.path); -Future generate(String source) async { - var srcs = { - 'openapi_generator_annotations|lib/src/openapi_generator_annotations_base.dart': - File('../openapi-generator-annotations/lib/src/openapi_generator_annotations_base.dart') - .readAsStringSync(), - 'openapi_generator|lib/myapp.dart': ''' - import 'package:openapi_generator_annotations/src/openapi_generator_annotations_base.dart'; - $source - class MyApp { - } - ''', - 'openapi_generator|openapi-spec.yaml': spec - }; + hasOutput = copy.readAsStringSync().contains(lastRunPlaceHolder); + expect(generatedOutput, + contains('Found generated timestamp. Updating with')); - // Capture any error from generation; if there is one, return that instead of - // the generated output. - String? error; - void captureError(dynamic logRecord) { - // print(logRecord.runtimeType); - // print(logRecord); - // if (logRecord.error is InvalidGenerationSourceError) { - // if (error != null) throw StateError('Expected at most one error.'); - // error = logRecord.error.toString(); - // } - error = '${error ?? ''}\n${logRecord.message}'; - } + copy.deleteSync(); + expect(hasOutput, isTrue); + }); + group('source gen', () { + group('uses Flutter', () { + group('with wrapper', () { + test('fvm', () async { + generatedOutput = await generate(''' +@Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/fvm', + additionalProperties: AdditionalProperties( + wrapper: Wrapper.fvm, + ), +) + '''); + expect( + generatedOutput, contains('Running source code generation.')); + expect( + generatedOutput, + contains( + 'fvm pub run build_runner build --delete-conflicting-outputs')); + }); + test('flutterw', () async { + generatedOutput = await generate(''' +@Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/flutterw', + additionalProperties: AdditionalProperties( + wrapper: Wrapper.flutterw, + ), +) + '''); + expect( + generatedOutput, contains('Running source code generation.')); + expect( + generatedOutput, + contains( + './flutterw pub run build_runner build --delete-conflicting-outputs')); + }); + }); + test('without wrapper', () async { + final annotations = (await resolveSource( + File('$testSpecPath/next_gen_builder_flutter_test_config.dart') + .readAsStringSync(), + (resolver) async => + (await resolver.findLibraryByName('test_lib'))!)) + .getClass('TestClassConfig')! + .metadata + .map((e) => ConstantReader(e.computeConstantValue()!)) + .first; + final args = GeneratorArguments(annotations: annotations); + generatedOutput = await generate(''' +@Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/flutter', + projectPubspecPath: './test/specs/flutter_pubspec.test.yaml', +) + '''); - var writer = InMemoryAssetWriter(); - await testBuilder(builder, srcs, - rootPackage: pkgName, writer: writer, onLog: captureError); - return error ?? - String.fromCharCodes( - writer.assets[AssetId(pkgName, 'lib/value.g.dart')] ?? []); -} + expect(args.wrapper, Wrapper.none); + expect( + generatedOutput, contains('Running source code generation.')); + expect( + generatedOutput, + contains( + 'flutter pub run build_runner build --delete-conflicting-outputs')); + }); + }); + test('uses dart', () async { + final annotations = (await resolveSource( + File('$testSpecPath/next_gen_builder_test_config.dart') + .readAsStringSync(), + (resolver) async => + (await resolver.findLibraryByName('test_lib'))!)) + .getClass('TestClassConfig')! + .metadata + .map((e) => ConstantReader(e.computeConstantValue()!)) + .first; + final args = GeneratorArguments(annotations: annotations); + generatedOutput = await generate(''' +@Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/dart', + projectPubspecPath: './test/specs/dart_pubspec.test.yaml', +) + '''); + + expect(args.wrapper, Wrapper.none); + expect(generatedOutput, contains('Running source code generation.')); + expect( + generatedOutput, + contains( + 'dart pub run build_runner build --delete-conflicting-outputs')); + }); + group('except when', () { + test('flag is set', () async { + final annotations = (await resolveSource( + ''' +library test_lib; + +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; + +@Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/no-src', + runSourceGenOnOutput: false, +) +class TestClassConfig extends OpenapiGeneratorConfig {} + ''', + (resolver) async => + (await resolver.findLibraryByName('test_lib'))!)) + .getClass('TestClassConfig')! + .metadata + .map((e) => ConstantReader(e.computeConstantValue()!)) + .first; + final args = GeneratorArguments(annotations: annotations); -var spec = ''' -openapi: 3.0.1 -info: - title: OpenAPI Petstore - description: This is a sample server Petstore server. For this sample, you can use - the api key `special-key` to test the authorization filters. - license: - name: Apache-2.0 - url: https://www.apache.org/licenses/LICENSE-2.0.html - version: 1.0.0 -servers: - - url: http://petstore.swagger.io/v2 -tags: - - name: pet - description: Everything about your Pets - - name: store - description: Access to Petstore orders - - name: user - description: Operations about user -paths: - /pet: - put: - tags: - - pet - summary: Update an existing pet - operationId: updatePet - requestBody: - description: Pet object that needs to be added to the store - content: - application/json: - schema: - \$ref: '#/components/schemas/Pet' - application/xml: - schema: - \$ref: '#/components/schemas/Pet' - required: true - responses: - 400: - description: Invalid ID supplied - content: {} - 404: - description: Pet not found - content: {} - 405: - description: Validation exception - content: {} - security: - - petstore_auth: - - write:pets - - read:pets - x-codegen-request-body-name: body - post: - tags: - - pet - summary: Add a new pet to the store - operationId: addPet - requestBody: - description: Pet object that needs to be added to the store - content: - application/json: - schema: - \$ref: '#/components/schemas/Pet' - application/xml: - schema: - \$ref: '#/components/schemas/Pet' - required: true - responses: - 405: - description: Invalid input - content: {} - security: - - petstore_auth: - - write:pets - - read:pets - x-codegen-request-body-name: body -components: - schemas: - Order: - title: Pet Order - type: object - properties: - id: - type: integer - format: int64 - petId: - type: integer - format: int64 - quantity: - type: integer - format: int32 - shipDate: - type: string - format: date-time - status: - type: string - description: Order Status - enum: - - placed - - approved - - delivered - complete: - type: boolean - default: false - description: An order for a pets from the pet store - xml: - name: Order - Category: - title: Pet category - type: object - properties: - id: - type: integer - format: int64 - name: - type: string - description: A category for a pet - xml: - name: Category - User: - title: a User - type: object - properties: - id: - type: integer - format: int64 - username: - type: string - firstName: - type: string - lastName: - type: string - email: - type: string - password: - type: string - phone: - type: string - userStatus: - type: integer - description: User Status - format: int32 - description: A User who is purchasing from the pet store - xml: - name: User - Tag: - title: Pet Tag - type: object - properties: - id: - type: integer - format: int64 - name: - type: string - description: A tag for a pet - xml: - name: Tag - Pet: - title: a Pet - required: - - name - - photoUrls - type: object - properties: - id: - type: integer - format: int64 - category: - \$ref: '#/components/schemas/Category' - name: - type: string - example: doggie - photoUrls: - type: array - xml: - name: photoUrl - wrapped: true - items: - type: string - tags: - type: array - xml: - name: tag - wrapped: true - items: - \$ref: '#/components/schemas/Tag' - status: - type: string - description: pet status in the store - enum: - - available - - pending - - sold - types: - type: "array" - items: - type: "string" - enum: - - "TRANSFER_FROM" - - "TRANSFER_TO" - - "MINT" - - "BURN" - - "MAKE_BID" - - "GET_BID" - - "LIST" - - "BUY" - - "SELL" - description: A pet for sale in the pet store - xml: - name: Pet - Patri: - title: Patri - required: - - name - - photoUrls - type: object - properties: - id: - type: integer - format: int64 - category: - \$ref: '#/components/schemas/Category' - name: - type: string - example: doggie - photoUrls: - type: array - xml: - name: photoUrl - wrapped: true - items: - type: string - tags: - type: array - xml: - name: tag - wrapped: true - items: - \$ref: '#/components/schemas/Tag' - status: - type: string - description: pet status in the store - enum: - - available - - pending - - sold - types: - type: "array" - items: - type: "string" - enum: - - "TRANSFER_FROM" - - "TRANSFER_TO" - - "MINT" - - "BURN" - - "MAKE_BID" - - "GET_BID" - - "LIST" - - "BUY" - - "SELL" - description: A pet for sale in the pet store - xml: - name: Pet - ApiResponse: - title: An uploaded response - type: object - properties: - code: - type: integer - format: int32 - type: - type: string - message: - type: string - description: Describes the result of uploading an image resource - securitySchemes: - petstore_auth: - type: oauth2 - flows: - implicit: - authorizationUrl: http://petstore.swagger.io/api/oauth/dialog - scopes: - write:pets: modify pets in your account - read:pets: read your pets - api_key: - type: apiKey - name: api_key - in: header + expect(args.runSourceGen, isFalse); + generatedOutput = await generate(''' +@Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/no-src', + runSourceGenOnOutput: false, +) + '''); + expect(generatedOutput, + contains('Skipping source gen step due to flag being set.')); + }); + test('generator is dart', () async { + final annotations = (await resolveSource( + ''' +library test_lib; -'''; +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; + +@Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dart, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/dart-gen' +) +class TestClassConfig extends OpenapiGeneratorConfig {} + ''', + (resolver) async => + (await resolver.findLibraryByName('test_lib'))!)) + .getClass('TestClassConfig')! + .metadata + .map((e) => ConstantReader(e.computeConstantValue()!)) + .first; + final args = GeneratorArguments(annotations: annotations); + expect(args.runSourceGen, isTrue); + generatedOutput = await generate(''' +@Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dart, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/dart-gen' +) + '''); + expect( + generatedOutput, + contains( + 'Skipping source gen because generator does not need it.')); + }); + }); + test('logs when successful', () async { + generatedOutput = await generate(''' +@Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/success', + projectPubspecPath: './test/specs/dart_pubspec.test.yaml', +) + '''); + expect(generatedOutput, contains('Codegen completed successfully.')); + expect(generatedOutput, contains('Sources generated successfully.')); + }); + }); + group('fetch dependencies', () { + test('except when flag is present', () async { + generatedOutput = await generate(''' +@Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/no-fetch', + projectPubspecPath: './test/specs/dart_pubspec.test.yaml', + fetchDependencies: false, +) + '''); + expect(generatedOutput, + contains('Skipping install step because flag was set.')); + }); + test('succeeds', () async { + generatedOutput = await generate(''' +@Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/no-fetch', + projectPubspecPath: './test/specs/dart_pubspec.test.yaml', +) + '''); + expect(generatedOutput, + contains('Installing dependencies with generated source.')); + expect(generatedOutput, contains('Install completed successfully.')); + }); + }); + group('update cache', () { + final src = ''' + @Openapi( + inputSpecFile: '$specPath', + useNextGen: true, + cachePath: '${f.path}', + outputDirectory: '${f.parent.path}/update-cache', + ) + '''; + + test('creating a cache file when not found', () async { + // Ensure that other tests don't make this available; + if (f.existsSync()) { + f.deleteSync(); + } + generatedOutput = await generate(src); + expect( + generatedOutput, contains('No local cache found. Creating one.')); + expect(f.existsSync(), isTrue); + expect(jsonDecode(f.readAsStringSync()), + await loadSpec(specPath: specPath)); + }); + test('updates the cache file when found', () async { + f.writeAsStringSync(jsonEncode({'someKey': 'someValue'})); + generatedOutput = await generate(src); + final expectedSpec = await loadSpec(specPath: specPath); + final actualSpec = jsonDecode(f.readAsStringSync()); + expect(actualSpec, expectedSpec); + expect(generatedOutput, + contains('Local cache found. Overwriting existing one.')); + }); + test('logs when successful', () async { + f.writeAsStringSync(jsonEncode({'someKey': 'someValue'})); + generatedOutput = await generate(src); + expect( + generatedOutput, contains('Successfully cached spec changes.')); + }); + }); + }); + }); +} diff --git a/openapi-generator/test/command_test.dart b/openapi-generator/test/command_test.dart new file mode 100644 index 0000000..be502a7 --- /dev/null +++ b/openapi-generator/test/command_test.dart @@ -0,0 +1,35 @@ +import 'package:openapi_generator/src/models/command.dart'; +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; +import 'package:test/test.dart'; + +void main() { + group('Command', () { + final testArgs = ['pub', 'get']; + group('handles flutter wrapping', () { + test('Wrapper.flutterw', () { + final command = Command( + executable: 'flutter', + arguments: testArgs, + wrapper: Wrapper.flutterw); + expect(command.arguments, testArgs); + expect(command.executable, './flutterw'); + }); + test('Wrapper.fvw', () { + final command = Command( + executable: 'flutter', arguments: testArgs, wrapper: Wrapper.fvm); + expect(command.arguments, testArgs); + expect(command.executable, 'fvm'); + }); + test('doesn\'t wrap Wrapper.none', () { + final command = Command(executable: 'flutter', arguments: testArgs); + expect(command.arguments, testArgs); + expect(command.executable, 'flutter'); + }); + }); + test('wraps doesn\'t dart', () { + final command = Command(executable: 'dart', arguments: testArgs); + expect(command.arguments, testArgs); + expect(command.executable, 'dart'); + }); + }); +} diff --git a/openapi-generator/test/determine_flutter_projet_status_test.dart b/openapi-generator/test/determine_flutter_projet_status_test.dart new file mode 100644 index 0000000..14464ed --- /dev/null +++ b/openapi-generator/test/determine_flutter_projet_status_test.dart @@ -0,0 +1,68 @@ +import 'dart:io'; + +import 'package:openapi_generator/src/determine_flutter_project_status.dart'; +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; +import 'package:test/test.dart'; + +final basePath = + '${Directory.current.path}${Platform.pathSeparator}test${Platform.pathSeparator}specs${Platform.pathSeparator}'; + +void main() { + group('Determines Flutter Project status', () { + test('via wrapper', () async { + expect( + await checkPubspecAndWrapperForFlutterSupport(wrapper: Wrapper.fvm), + isTrue); + expect( + await checkPubspecAndWrapperForFlutterSupport( + wrapper: Wrapper.flutterw), + isTrue); + }); + group('checks the pubspec', () { + group('and throws when', () { + test('pubspec is empty', () async { + // TODO: There is likely a better way to handle this. + try { + await checkPubspecAndWrapperForFlutterSupport( + providedPubspecPath: '${basePath}empty_pubspec.yaml'); + fail('Should\'ve thrown invalid error'); + } catch (e, _) { + expect(e, 'Invalid pubspec.yaml'); + } + }); + test('pubspec doesn\'t exist', () async { + final path = '${basePath}doesnotexist.yaml'; + try { + await checkPubspecAndWrapperForFlutterSupport( + providedPubspecPath: path); + fail('Should\'ve thrown missing pubspec error'); + } catch (e, _) { + expect(e, 'Pubspec doesn\'t exist at path: $path'); + } + }); + }); + test('at PWD/pubspec.yaml by default', () async { + // This project doesn't have a dependency on the flutter sdk list in the + // pubspec.yaml. + // + // Since the test command is generally run from the root of this project + // this should be a stable assumption. + expect(await checkPubspecAndWrapperForFlutterSupport(), isFalse); + }); + test('is false if key is missing from dependencies', () async { + final pubspecPath = '${basePath}dart_pubspec.test.yaml'; + expect( + await checkPubspecAndWrapperForFlutterSupport( + providedPubspecPath: pubspecPath), + isFalse); + }); + test('is true when key is in dependencies', () async { + final pubspecPath = '${basePath}flutter_pubspec.test.yaml'; + expect( + await checkPubspecAndWrapperForFlutterSupport( + providedPubspecPath: pubspecPath), + isTrue); + }); + }); + }); +} diff --git a/openapi-generator/test/gen_on_spec_changes_test.dart b/openapi-generator/test/gen_on_spec_changes_test.dart new file mode 100644 index 0000000..3054e19 --- /dev/null +++ b/openapi-generator/test/gen_on_spec_changes_test.dart @@ -0,0 +1,272 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; +import 'package:openapi_generator/src/gen_on_spec_changes.dart'; +import 'package:openapi_generator/src/models/output_message.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +final testDirPath = + '${Directory.current.path}${Platform.pathSeparator}test${Platform.pathSeparator}specs'; + +final supportedExtensions = { + 'json': '$testDirPath${Platform.pathSeparator}openapi.test.json', + 'yaml': '$testDirPath${Platform.pathSeparator}openapi.test.yaml', + 'yml': '$testDirPath${Platform.pathSeparator}openapi.test.yml' +}; + +void main() { + final Map jsonSpecFile = + jsonDecode(File(supportedExtensions['json']!).readAsStringSync()); + group('Generates on Spec changes', () { + group('Load Spec', () { + test('throws an error for unsupported files', () async { + try { + await loadSpec(specPath: './thisIsSomeInvalidPath.wrong'); + fail('Should\'ve thrown as not supported file type.'); + } catch (e, _) { + expect((e as OutputMessage).message, 'Invalid spec file format.'); + } + }); + test('throws an error for missing config file', () async { + try { + await loadSpec(specPath: './thisIsSomeInvalidPath.yaml'); + fail('Should\'ve thrown as not supported file type.'); + } catch (e, _) { + expect((e as OutputMessage).message, + 'Unable to find spec file ./thisIsSomeInvalidPath.yaml'); + } + }); + test('returns empty map when cache isn\'t found', () async { + try { + final cached = await loadSpec( + specPath: './nonValidCacheSpecPath.yaml', isCached: true); + expect(cached, isEmpty); + } catch (e, _) { + fail( + 'Should return empty map when spec path is cached spec but not found'); + } + }); + group('returns a map', () { + test('json', () async { + try { + final mapped = + await loadSpec(specPath: supportedExtensions['json']!); + expect(mapped, jsonSpecFile); + } catch (e, _) { + print(e); + fail('should have successfully loaded json spec'); + } + }); + test('yaml (requires transformation)', () async { + try { + final loaded = + await loadSpec(specPath: supportedExtensions['yaml']!); + expect(loaded, jsonSpecFile); + } catch (_, __) { + fail('Should successfully convert yaml to Map'); + } + }); + test('yml (requires transformation)', () async { + try { + final loaded = + await loadSpec(specPath: supportedExtensions['yml']!); + expect(loaded, jsonSpecFile); + } catch (_, __) { + fail('Should successfully convert yml to Map'); + } + }); + }); + group('from remote', () { + test('successfully returns the spec', () async { + try { + final url = Uri.parse( + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml'); + final resp = await http.get(url); + final expected = + convertYamlMapToDartMap(yamlMap: loadYaml(resp.body)); + final spec = await loadSpec(specPath: url.toString()); + expect(spec, expected); + } catch (e, _) { + fail('Should load remote files successfully'); + } + }); + // TODO: Add other status codes when? This will mostly impact values + // behind authenticated endpoints, which need a custom auth header. + test('fails when spec is inaccessible', () async { + try { + final url = Uri.parse( + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yml'); + await loadSpec(specPath: url.toString()); + fail('Should fail when remote files can\'t be found'); + } catch (e, _) { + final errorMessage = e as OutputMessage; + expect(errorMessage.level, Level.SEVERE); + expect(errorMessage.additionalContext, 404); + expect(errorMessage.message, + 'Unable to request remote spec. Ensure it is public or use a local copy instead.'); + } + }); + }); + }); + group('verifies dirty status', () { + test('is true when the cached spec is empty', () { + expect(isSpecDirty(cachedSpec: {}, loadedSpec: {'key1': '2'}), isTrue); + }); + test( + 'is false when the cached spec is empty and loaded spec is also empty', + () { + expect(isSpecDirty(cachedSpec: {}, loadedSpec: {}), isFalse); + }); + test('returns false when specs match', () async { + final loaded = await loadSpec(specPath: supportedExtensions['json']!); + expect( + isSpecDirty(cachedSpec: jsonSpecFile, loadedSpec: loaded), isFalse); + }); + test('returns true when a key is renamed', () { + expect( + isSpecDirty(cachedSpec: { + 'rootKey': {'subKey': 'k'} + }, loadedSpec: { + 'rootKey': {'renamedSubKey': 'k'} + }), + isTrue); + }); + test('returns true when root/sub keys differ in length', () { + expect( + isSpecDirty( + cachedSpec: {'someKey': 1}, + loadedSpec: {'someKey': 1, 'someExtraKey': 'content'}), + isTrue); + + expect( + isSpecDirty(cachedSpec: { + 'someExtraKey': {'subKey': 'k', 'subKey1': 4} + }, loadedSpec: { + 'someExtraKey': {'subKey': 'k'} + }), + isTrue); + }); + group('when sub entry', () { + group('list', () { + test('entries change', () { + expect( + isSpecDirty(cachedSpec: { + 'thisIsAList': [1, 2, 4] + }, loadedSpec: { + 'thisIsAList': [1, 2, 3] + }), + isTrue); + }); + test('lengths change', () { + expect( + isSpecDirty(cachedSpec: { + 'thisIsAList': [1, 2, 4] + }, loadedSpec: { + 'thisIsAList': [1, 2, 3, 4] + }), + isTrue); + }); + test('entries changes order', () { + expect( + isSpecDirty(cachedSpec: { + 'thisIsAList': [2, 1, 5] + }, loadedSpec: { + 'thisIsAList': [1, 2, 5] + }), + isTrue); + }); + }); + group('scalar', () { + test('value changed', () { + expect( + isSpecDirty( + cachedSpec: {'scalar': 5}, loadedSpec: {'scalar': 12}), + isTrue); + }); + test('type changed', () { + expect( + isSpecDirty( + cachedSpec: {'scalar': 5}, loadedSpec: {'scalar': '12'}), + isTrue); + }); + }); + }); + }); + group('transforms yaml to dart map', () { + test('converts scalars', () { + expect(convertYamlMapToDartMap(yamlMap: YamlMap.wrap({'scalar': 5})), + {'scalar': 5}); + }); + group('converts lists', () { + test('with YamlMaps', () { + final listContent = [ + 1, + 2, + 3, + 4, + YamlMap.wrap({'entry': 'value'}) + ]; + final listContentExpected = [ + 1, + 2, + 3, + 4, + {'entry': 'value'} + ]; + expect( + convertYamlListToDartList(yamlList: YamlList.wrap(listContent)), + listContentExpected); + }); + test('with nested lists', () { + final listContent = [ + 1, + 2, + 3, + 4, + YamlList.wrap( + ['one', 'two', 'three'], + ) + ]; + final listContentExpected = [ + 1, + 2, + 3, + 4, + ['one', 'two', 'three'], + ]; + expect( + convertYamlListToDartList(yamlList: YamlList.wrap(listContent)), + listContentExpected); + }); + }); + test('converts submap to map', () { + final expectedMap = { + 'mapWithSubMap': { + 'subMap': {'scalar': 5, 'meh': 'value'}, + } + }; + expect( + convertYamlMapToDartMap( + yamlMap: YamlMap.wrap({ + 'mapWithSubMap': YamlMap.wrap(expectedMap['mapWithSubMap']) + })), + expectedMap); + }); + }); + test('cache diff', () async { + try { + final path = '$testDirPath${Platform.pathSeparator}test-cached.json'; + await cacheSpec(outputLocation: path, spec: jsonSpecFile); + expect(File(path).existsSync(), isTrue); + // Test the rerun succeeds too + await cacheSpec(outputLocation: path, spec: jsonSpecFile); + expect(File(path).existsSync(), isTrue); + } catch (e, _) { + fail('should\'ve successfully cached diff'); + } + }); + }); +} diff --git a/openapi-generator/test/generator_arguments_test.dart b/openapi-generator/test/generator_arguments_test.dart new file mode 100644 index 0000000..f7a97fb --- /dev/null +++ b/openapi-generator/test/generator_arguments_test.dart @@ -0,0 +1,198 @@ +import 'dart:io'; + +import 'package:build_test/build_test.dart'; +import 'package:openapi_generator/src/models/generator_arguments.dart'; +import 'package:openapi_generator/src/models/output_message.dart'; +import 'package:openapi_generator/src/utils.dart'; +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; +import 'package:source_gen/source_gen.dart' as src_gen; +import 'package:test/test.dart'; + +void main() { + group('GeneratorArguments', () { + group('defaults', () { + late GeneratorArguments args; + setUpAll(() => + args = GeneratorArguments(annotations: src_gen.ConstantReader(null))); + test('alwaysRun', () => expect(args.alwaysRun, isFalse)); + test('useNextGen', () => expect(args.useNextGen, isFalse)); + test('cachePath', () => expect(args.cachePath, defaultCachedPath)); + test('outputDirectory', + () => expect(args.outputDirectory, Directory.current.path)); + test('runSourceGen', () => expect(args.runSourceGen, isTrue)); + test('shouldFetchDependencies', + () => expect(args.shouldFetchDependencies, isTrue)); + test('skipValidation', () => expect(args.skipValidation, isFalse)); + test( + 'pubspecPath', + () => expect( + args.pubspecPath, '${Directory.current.path}/pubspec.yaml')); + group('inputFile', () { + test('errors when no spec is found', () async { + await args.inputFileOrFetch.onError((e, __) { + expect((e as OutputMessage).message, + 'No spec file found. One must be present in the project or hosted remotely.'); + return ''; + }); + }); + + test('updates path when one is found', () async { + final f = File( + Directory.current.path + '${Platform.pathSeparator}openapi.json'); + f.createSync(); + f.writeAsStringSync(''); + final p = await args.inputFileOrFetch; + expect(p, f.path); + expect(await args.inputFileOrFetch, f.path); + f.deleteSync(); + }); + }); + test('templateDirectory', () => expect(args.templateDirectory, isEmpty)); + test('generator', () => expect(args.generator, Generator.dart)); + + test('importMappings', () => expect(args.importMappings, isEmpty)); + test('typeMappings', () => expect(args.typeMappings, isEmpty)); + test('reservedWordsMappings', + () => expect(args.reservedWordsMappings, isEmpty)); + test('inlineSchemaNameMappings', + () => expect(args.inlineSchemaNameMappings, isEmpty)); + + test('generatorName', () => expect(args.generatorName, 'dart')); + test('shouldGenerateSources', + () => expect(args.shouldGenerateSources, isFalse)); + test('isRemote', () => expect(args.isRemote, isFalse)); + test('additionalProperties', + () => expect(args.additionalProperties, isNull)); + test( + 'wrapper defaults to none', () => expect(args.wrapper, Wrapper.none)); + test('inlineSchemaOptions', + () => expect(args.inlineSchemaOptions, isNull)); + test('jarArgs', () async { + final f = File( + Directory.current.path + '${Platform.pathSeparator}openapi.json'); + f.createSync(); + f.writeAsStringSync(''); + expect(await args.jarArgs, [ + 'generate', + '-o=${Directory.current.path}', + '-i=${await args.inputFileOrFetch}', + '-g=${args.generatorName}', + ]); + f.deleteSync(); + }); + }); + group('accepts overrides', () { + final annos = src_gen.ConstantReader(null); + final args = GeneratorArguments( + annotations: annos, + alwaysRun: true, + useNextGen: true, + cachePath: 'test', + outputDirectory: 'path', + templateDirectory: 'template', + runSourceGen: false, + generator: Generator.dioAlt, + skipValidation: true, + fetchDependencies: false, + inputSpecFile: 'test.yaml', + importMapping: {'key': 'value'}, + typeMapping: {'package': 'type'}, + reservedWordsMapping: {'const': 'final'}, + inlineSchemaNameMapping: {'L': 'R'}, + additionalProperties: AdditionalProperties(wrapper: Wrapper.fvm), + pubspecPath: 'testing/pubspec.yaml'); + test('alwaysRun', () => expect(args.alwaysRun, isTrue)); + test('useNextGen', () => expect(args.useNextGen, isTrue)); + test('cachePath', () => expect(args.cachePath, 'test')); + test('outputDirectory', () => expect(args.outputDirectory, 'path')); + test('runSourceGen', () => expect(args.runSourceGen, isFalse)); + test('shouldFetchDependencies', + () => expect(args.shouldFetchDependencies, isFalse)); + test('skipValidation', () => expect(args.skipValidation, isTrue)); + test('inputFile', + () async => expect(await args.inputFileOrFetch, 'test.yaml')); + test('templateDirectory', + () => expect(args.templateDirectory, 'template')); + test('generator', () => expect(args.generator, Generator.dioAlt)); + test('wrapper', () => expect(args.wrapper, Wrapper.fvm)); + test('importMappings', + () => expect(args.importMappings, {'key': 'value'})); + test( + 'typeMappings', () => expect(args.typeMappings, {'package': 'type'})); + test('reservedWordsMappings', + () => expect(args.reservedWordsMappings, {'const': 'final'})); + test('inlineSchemaNameMappings', + () => expect(args.inlineSchemaNameMappings, {'L': 'R'})); + test('isRemote', () => expect(args.isRemote, isFalse)); + test('generatorName', () => expect(args.generatorName, 'dart2-api')); + test('shouldGenerateSources', + () => expect(args.shouldGenerateSources, isTrue)); + test('pubspecPath', + () => expect(args.pubspecPath, 'testing/pubspec.yaml')); + test( + 'jarArgs', + () async => expect( + await args.jarArgs, + [ + 'generate', + '-o=${args.outputDirectory}', + '-i=${await args.inputFileOrFetch}', + '-t=${args.templateDirectory}', + '-g=${args.generatorName}', + '--skip-validate-spec', + '--reserved-words-mappings=${args.reservedWordsMappings.entries.fold('', foldStringMap())}', + '--inline-schema-name-mappings=${args.inlineSchemaNameMappings.entries.fold('', foldStringMap())}', + '--import-mappings=${args.importMappings.entries.fold('', foldStringMap())}', + '--type-mappings=${args.typeMappings.entries.fold('', foldStringMap())}', + '--additional-properties=${args.additionalProperties?.toMap().entries.fold('', foldStringMap(keyModifier: convertToPropertyKey))}' + ], + ), + ); + }); + test('uses config', () async { + final config = File( + '${Directory.current.path}${Platform.pathSeparator}test${Platform.pathSeparator}specs${Platform.pathSeparator}test_config.dart') + .readAsStringSync(); + final annotations = (await resolveSource( + config, + (resolver) async => + (await resolver.findLibraryByName('test_lib'))!)) + .getClass('TestClassConfig')! + .metadata + .map((e) => src_gen.ConstantReader(e.computeConstantValue()!)) + .first; + final args = GeneratorArguments(annotations: annotations); + expect(args.alwaysRun, isTrue); + expect(args.useNextGen, isTrue); + expect(args.cachePath, './test/specs/output/cache.json'); + expect(args.outputDirectory, './test/specs/output'); + expect(args.runSourceGen, isTrue); + expect(args.shouldFetchDependencies, isTrue); + expect(args.skipValidation, isFalse); + expect(await args.inputFileOrFetch, './openapi.test.yaml'); + expect(args.templateDirectory, 'template'); + expect(args.generator, Generator.dio); + expect(args.wrapper, Wrapper.fvm); + expect(args.importMappings, {'package': 'test'}); + expect(args.typeMappings, {'key': 'value'}); + expect(args.reservedWordsMappings, {'const': 'final'}); + expect(args.inlineSchemaNameMappings, {'200resp': 'OkResp'}); + expect(args.pubspecPath, './test/specs/dart_pubspec.test.yaml'); + expect(args.isRemote, isFalse); + expect(args.generatorName, 'dart-dio'); + expect(args.shouldGenerateSources, isTrue); + expect(await args.jarArgs, [ + 'generate', + '-o=${args.outputDirectory}', + '-i=${await args.inputFileOrFetch}', + '-t=${args.templateDirectory}', + '-g=${args.generatorName}', + '--reserved-words-mappings=${args.reservedWordsMappings.entries.fold('', foldStringMap())}', + '--inline-schema-name-mappings=${args.inlineSchemaNameMappings.entries.fold('', foldStringMap())}', + '--import-mappings=${args.importMappings.entries.fold('', foldStringMap())}', + '--type-mappings=${args.typeMappings.entries.fold('', foldStringMap())}', + '--additional-properties=${args.additionalProperties!.toMap().entries.fold('', foldStringMap(keyModifier: convertToPropertyKey))}' + ]); + }); + }); +} diff --git a/openapi-generator/test/method_test.dart b/openapi-generator/test/method_test.dart index 1a68282..0af813c 100644 --- a/openapi-generator/test/method_test.dart +++ b/openapi-generator/test/method_test.dart @@ -1,66 +1,45 @@ -import 'package:openapi_generator/src/openapi_generator_runner.dart'; -import 'package:openapi_generator_annotations/openapi_generator_annotations.dart' - as n; +import 'package:openapi_generator/src/utils.dart'; import 'package:test/test.dart'; void main() { group('convertToPropertyKey()', () { - final generator = OpenapiGenerator(); - test('convert "nullSafeArrayDefault"', () { - expect(generator.convertToPropertyKey('nullSafeArrayDefault'), + expect(convertToPropertyKey('nullSafeArrayDefault'), equals('nullSafe-array-default')); }); test('convert "pubspecDependencies"', () { - expect(generator.convertToPropertyKey('pubspecDependencies'), + expect(convertToPropertyKey('pubspecDependencies'), equals('pubspec-dependencies')); }); test('convert "pubspecDevDependencies"', () { - expect(generator.convertToPropertyKey('pubspecDevDependencies'), + expect(convertToPropertyKey('pubspecDevDependencies'), equals('pubspec-dev-dependencies')); }); test('convert "inlineSchemaOptions.arrayItemSuffix"', () { - expect(generator.convertToPropertyKey('arrayItemSuffix'), - equals('ARRAY_ITEM_SUFFIX')); + expect( + convertToPropertyKey('arrayItemSuffix'), equals('ARRAY_ITEM_SUFFIX')); }); test('convert "inlineSchemaOptions.mapItemSuffix"', () { - expect(generator.convertToPropertyKey('mapItemSuffix'), - equals('MAP_ITEM_SUFFIX')); + expect(convertToPropertyKey('mapItemSuffix'), equals('MAP_ITEM_SUFFIX')); }); test('convert "inlineSchemaOptions.skipSchemaReuse"', () { - expect(generator.convertToPropertyKey('skipSchemaReuse'), - equals('SKIP_SCHEMA_REUSE')); + expect( + convertToPropertyKey('skipSchemaReuse'), equals('SKIP_SCHEMA_REUSE')); }); test('convert "inlineSchemaOptions.refactorAllofInlineSchemas"', () { - expect(generator.convertToPropertyKey('refactorAllofInlineSchemas'), + expect(convertToPropertyKey('refactorAllofInlineSchemas'), equals('REFACTOR_ALLOF_INLINE_SCHEMAS')); }); test('convert "inlineSchemaOptions.resolveInlineEnums"', () { - expect(generator.convertToPropertyKey('resolveInlineEnums'), + expect(convertToPropertyKey('resolveInlineEnums'), equals('RESOLVE_INLINE_ENUMS')); }); }); - - group('getGeneratorNameFromEnum()', () { - final generator = OpenapiGenerator(); - test('convert "Generator.dio"', () { - expect(generator.getGeneratorNameFromEnum(n.Generator.dio), - equals('dart-dio')); - }); - test('convert "Generator.dioAlt"', () { - expect(generator.getGeneratorNameFromEnum(n.Generator.dioAlt), - equals('dart2-api')); - }); - test('convert "Generator.dart"', () { - expect( - generator.getGeneratorNameFromEnum(n.Generator.dart), equals('dart')); - }); - }); } diff --git a/openapi-generator/test/output_message_test.dart b/openapi-generator/test/output_message_test.dart new file mode 100644 index 0000000..badbaba --- /dev/null +++ b/openapi-generator/test/output_message_test.dart @@ -0,0 +1,45 @@ +import 'package:logging/logging.dart'; +import 'package:openapi_generator/src/models/output_message.dart'; +import 'package:test/test.dart'; + +void main() { + group('OutputMessage', () { + test('defaults', () { + final message = OutputMessage(message: 'message'); + expect(message.message, 'message'); + expect(message.additionalContext, isNull); + expect(message.stackTrace, isNull); + expect(message.level, Level.INFO); + }); + test('uses provided level', () { + final message = OutputMessage(message: 'message', level: Level.WARNING); + expect(message.message, 'message'); + expect(message.additionalContext, isNull); + expect(message.stackTrace, isNull); + expect(message.level, Level.WARNING); + }); + test('uses provided message', () { + final message = OutputMessage(message: 'sup'); + expect(message.message, 'sup'); + expect(message.additionalContext, isNull); + expect(message.stackTrace, isNull); + expect(message.level, Level.INFO); + }); + test('uses provided error', () { + final message = + OutputMessage(message: 'message', additionalContext: 'thisIsAnError'); + expect(message.message, 'message'); + expect(message.additionalContext, 'thisIsAnError'); + expect(message.stackTrace, isNull); + expect(message.level, Level.INFO); + }); + test('uses provided stacktrace', () { + final stack = StackTrace.current; + final message = OutputMessage(message: 'message', stackTrace: stack); + expect(message.message, 'message'); + expect(message.additionalContext, isNull); + expect(message.stackTrace, stack); + expect(message.level, Level.INFO); + }); + }); +} diff --git a/openapi-generator/test/specs/dart_pubspec.test.yaml b/openapi-generator/test/specs/dart_pubspec.test.yaml new file mode 100644 index 0000000..338c166 --- /dev/null +++ b/openapi-generator/test/specs/dart_pubspec.test.yaml @@ -0,0 +1,22 @@ +name: openapi_generator.test.specs.dart_pubspec +description: a test pubspec +version: 4.11.0 + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + build: '>=0.12.6 <=3.0.0' + source_gen: '>=1.0.0 <=2.0.0' + path: '>=1.0.0 <=2.0.0' + openapi_generator_annotations: ^4.11.0 + analyzer: '>=2.0.0 <=6.0.0' + openapi_generator_cli: ^4.11.0 + yaml: ^3.1.2 + +dev_dependencies: + test: ^1.24.2 + build_runner: '>=1.0.0 <3.0.0' + build_test: '>=1.2.0 <3.0.0' + pedantic: + coverage: ^1.6.3 diff --git a/openapi-generator/test/specs/empty_pubspec.yaml b/openapi-generator/test/specs/empty_pubspec.yaml new file mode 100644 index 0000000..e69de29 diff --git a/openapi-generator/test/specs/flutter_pubspec.test.yaml b/openapi-generator/test/specs/flutter_pubspec.test.yaml new file mode 100644 index 0000000..f0c33df --- /dev/null +++ b/openapi-generator/test/specs/flutter_pubspec.test.yaml @@ -0,0 +1,24 @@ +name: openapi_generator.test.specs.flutter_pubspec +description: a test flutter pubspec +version: 4.11.0 + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + flutter: + sdk: flutter + build: '>=0.12.6 <=3.0.0' + source_gen: '>=1.0.0 <=2.0.0' + path: '>=1.0.0 <=2.0.0' + openapi_generator_annotations: ^4.11.0 + analyzer: '>=2.0.0 <=6.0.0' + openapi_generator_cli: ^4.11.0 + yaml: ^3.1.2 + +dev_dependencies: + test: ^1.24.2 + build_runner: '>=1.0.0 <3.0.0' + build_test: '>=1.2.0 <3.0.0' + pedantic: + coverage: ^1.6.3 diff --git a/openapi-generator/test/specs/next_gen_builder_flutter_test_config.dart b/openapi-generator/test/specs/next_gen_builder_flutter_test_config.dart new file mode 100644 index 0000000..b06211f --- /dev/null +++ b/openapi-generator/test/specs/next_gen_builder_flutter_test_config.dart @@ -0,0 +1,13 @@ +library test_lib; + +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; + +@Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: './test/specs/managed-cache.json', + projectPubspecPath: './test/specs/flutter_pubspec.test.yaml', +) +class TestClassConfig extends OpenapiGeneratorConfig {} diff --git a/openapi-generator/test/specs/next_gen_builder_test_config.dart b/openapi-generator/test/specs/next_gen_builder_test_config.dart new file mode 100644 index 0000000..3657e42 --- /dev/null +++ b/openapi-generator/test/specs/next_gen_builder_test_config.dart @@ -0,0 +1,12 @@ +library test_lib; + +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; + +@Openapi( + inputSpecFile: + 'https://raw.githubusercontent.com/Nexushunter/tagmine-api/main/openapi.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: './test/specs/output-nextgen/expected-args/cache.json', + outputDirectory: './test/specs/output-nextgen/expected-args') +class TestClassConfig extends OpenapiGeneratorConfig {} diff --git a/openapi-generator/test/specs/openapi.test.json b/openapi-generator/test/specs/openapi.test.json new file mode 100644 index 0000000..85f78e6 --- /dev/null +++ b/openapi-generator/test/specs/openapi.test.json @@ -0,0 +1,411 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI Petstore", + "description": "This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.", + "license": { + "name": "Apache-2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v2" + } + ], + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets" + }, + { + "name": "store", + "description": "Access to Petstore orders" + }, + { + "name": "user", + "description": "Operations about user" + } + ], + "paths": { + "/pet": { + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "operationId": "updatePet", + "requestBody": { + "description": "Pet object that needs to be added to the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "400": { + "description": "Invalid ID supplied", + "content": {} + }, + "404": { + "description": "Pet not found", + "content": {} + }, + "405": { + "description": "Validation exception", + "content": {} + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "x-codegen-request-body-name": "body" + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "operationId": "addPet", + "requestBody": { + "description": "Pet object that needs to be added to the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "405": { + "description": "Invalid input", + "content": {} + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "x-codegen-request-body-name": "body" + } + } + }, + "components": { + "schemas": { + "Order": { + "title": "Pet Order", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "petId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean", + "default": false + } + }, + "description": "An order for a pets from the pet store", + "xml": { + "name": "Order" + } + }, + "Category": { + "title": "Pet category", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "description": "A category for a pet", + "xml": { + "name": "Category" + } + }, + "User": { + "title": "a User", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "integer", + "description": "User Status", + "format": "int32" + } + }, + "description": "A User who is purchasing from the pet store", + "xml": { + "name": "User" + } + }, + "Tag": { + "title": "Pet Tag", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "description": "A tag for a pet", + "xml": { + "name": "Tag" + } + }, + "Pet": { + "title": "a Pet", + "required": [ + "name", + "photoUrls" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/components/schemas/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + }, + "types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "TRANSFER_FROM", + "TRANSFER_TO", + "MINT", + "BURN", + "MAKE_BID", + "GET_BID", + "LIST", + "BUY", + "SELL" + ] + } + } + }, + "description": "A pet for sale in the pet store", + "xml": { + "name": "Pet" + } + }, + "Patri": { + "title": "Patri", + "required": [ + "name", + "photoUrls" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/components/schemas/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + }, + "types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "TRANSFER_FROM", + "TRANSFER_TO", + "MINT", + "BURN", + "MAKE_BID", + "GET_BID", + "LIST", + "BUY", + "SELL" + ] + } + } + }, + "description": "A pet for sale in the pet store", + "xml": { + "name": "Pet" + } + }, + "ApiResponse": { + "title": "An uploaded response", + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "description": "Describes the result of uploading an image resource" + } + }, + "securitySchemes": { + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "http://petstore.swagger.io/api/oauth/dialog", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + } + } +} diff --git a/openapi-generator/test/specs/openapi.test.yaml b/openapi-generator/test/specs/openapi.test.yaml new file mode 100644 index 0000000..0bd3de6 --- /dev/null +++ b/openapi-generator/test/specs/openapi.test.yaml @@ -0,0 +1,286 @@ +openapi: 3.0.1 +info: + title: OpenAPI Petstore + description: This is a sample server Petstore server. For this sample, you can use + the api key `special-key` to test the authorization filters. + license: + name: Apache-2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.0 +servers: + - url: http://petstore.swagger.io/v2 +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet + operationId: updatePet + requestBody: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + 400: + description: Invalid ID supplied + content: {} + 404: + description: Pet not found + content: {} + 405: + description: Validation exception + content: {} + security: + - petstore_auth: + - write:pets + - read:pets + x-codegen-request-body-name: body + post: + tags: + - pet + summary: Add a new pet to the store + operationId: addPet + requestBody: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + 405: + description: Invalid input + content: {} + security: + - petstore_auth: + - write:pets + - read:pets + x-codegen-request-body-name: body +components: + schemas: + Order: + title: Pet Order + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + description: An order for a pets from the pet store + xml: + name: Order + Category: + title: Pet category + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + description: A category for a pet + xml: + name: Category + User: + title: a User + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + description: User Status + format: int32 + description: A User who is purchasing from the pet store + xml: + name: User + Tag: + title: Pet Tag + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + description: A tag for a pet + xml: + name: Tag + Pet: + title: a Pet + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + types: + type: "array" + items: + type: "string" + enum: + - "TRANSFER_FROM" + - "TRANSFER_TO" + - "MINT" + - "BURN" + - "MAKE_BID" + - "GET_BID" + - "LIST" + - "BUY" + - "SELL" + description: A pet for sale in the pet store + xml: + name: Pet + Patri: + title: Patri + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + types: + type: "array" + items: + type: "string" + enum: + - "TRANSFER_FROM" + - "TRANSFER_TO" + - "MINT" + - "BURN" + - "MAKE_BID" + - "GET_BID" + - "LIST" + - "BUY" + - "SELL" + description: A pet for sale in the pet store + xml: + name: Pet + ApiResponse: + title: An uploaded response + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + description: Describes the result of uploading an image resource + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://petstore.swagger.io/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/openapi-generator/test/specs/openapi.test.yml b/openapi-generator/test/specs/openapi.test.yml new file mode 100644 index 0000000..0bd3de6 --- /dev/null +++ b/openapi-generator/test/specs/openapi.test.yml @@ -0,0 +1,286 @@ +openapi: 3.0.1 +info: + title: OpenAPI Petstore + description: This is a sample server Petstore server. For this sample, you can use + the api key `special-key` to test the authorization filters. + license: + name: Apache-2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.0 +servers: + - url: http://petstore.swagger.io/v2 +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet + operationId: updatePet + requestBody: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + 400: + description: Invalid ID supplied + content: {} + 404: + description: Pet not found + content: {} + 405: + description: Validation exception + content: {} + security: + - petstore_auth: + - write:pets + - read:pets + x-codegen-request-body-name: body + post: + tags: + - pet + summary: Add a new pet to the store + operationId: addPet + requestBody: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + 405: + description: Invalid input + content: {} + security: + - petstore_auth: + - write:pets + - read:pets + x-codegen-request-body-name: body +components: + schemas: + Order: + title: Pet Order + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + description: An order for a pets from the pet store + xml: + name: Order + Category: + title: Pet category + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + description: A category for a pet + xml: + name: Category + User: + title: a User + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + description: User Status + format: int32 + description: A User who is purchasing from the pet store + xml: + name: User + Tag: + title: Pet Tag + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + description: A tag for a pet + xml: + name: Tag + Pet: + title: a Pet + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + types: + type: "array" + items: + type: "string" + enum: + - "TRANSFER_FROM" + - "TRANSFER_TO" + - "MINT" + - "BURN" + - "MAKE_BID" + - "GET_BID" + - "LIST" + - "BUY" + - "SELL" + description: A pet for sale in the pet store + xml: + name: Pet + Patri: + title: Patri + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + types: + type: "array" + items: + type: "string" + enum: + - "TRANSFER_FROM" + - "TRANSFER_TO" + - "MINT" + - "BURN" + - "MAKE_BID" + - "GET_BID" + - "LIST" + - "BUY" + - "SELL" + description: A pet for sale in the pet store + xml: + name: Pet + ApiResponse: + title: An uploaded response + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + description: Describes the result of uploading an image resource + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://petstore.swagger.io/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/openapi-generator/test/specs/test_config.dart b/openapi-generator/test/specs/test_config.dart new file mode 100644 index 0000000..0326661 --- /dev/null +++ b/openapi-generator/test/specs/test_config.dart @@ -0,0 +1,24 @@ +library test_lib; + +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; + +@Openapi( + inputSpecFile: './openapi.test.yaml', + generatorName: Generator.dio, + useNextGen: true, + cachePath: './test/specs/output/cache.json', + typeMappings: {'key': 'value'}, + templateDirectory: 'template', + alwaysRun: true, + outputDirectory: './test/specs/output', + runSourceGenOnOutput: true, + apiPackage: 'test', + skipSpecValidation: false, + importMappings: {'package': 'test'}, + reservedWordsMappings: {'const': 'final'}, + additionalProperties: AdditionalProperties(wrapper: Wrapper.fvm), + inlineSchemaNameMappings: {'200resp': 'OkResp'}, + overwriteExistingFiles: true, + projectPubspecPath: './test/specs/dart_pubspec.test.yaml', +) +class TestClassConfig extends OpenapiGeneratorConfig {} diff --git a/openapi-generator/test/test_annotations/test_configs.dart b/openapi-generator/test/test_annotations/test_configs.dart new file mode 100644 index 0000000..70bd3f4 --- /dev/null +++ b/openapi-generator/test/test_annotations/test_configs.dart @@ -0,0 +1,79 @@ +library test_annotations; + +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; +import 'package:source_gen_test/annotations.dart'; + +@ShouldGenerate(r''' +const alwaysRun = false; + +const fetchDependencies = true; + +const generatorName = 'dio'; + +const inputSpecFile = ''; + +const runSourceGenOnOutput = true; + +const skipSpecValidation = false; + +const useNextGen = false; +''') +@Openapi(inputSpecFile: '', generatorName: Generator.dio) +class TestClassDefault extends OpenapiGeneratorConfig {} + +@ShouldThrow('useNextGen must be set when using cachePath', element: false) +@Openapi(inputSpecFile: '', generatorName: Generator.dio, cachePath: './') +class TestClassInvalidCachePathUsage extends OpenapiGeneratorConfig {} + +@ShouldGenerate(r''' +const additionalProperties = wrapper = 'flutterw'; + +const alwaysRun = false; + +const fetchDependencies = true; + +const generatorName = 'dart'; + +const inputSpecFile = ''; + +const runSourceGenOnOutput = true; + +const skipSpecValidation = false; + +const useNextGen = false; +''') +@Openapi( + inputSpecFile: '', + generatorName: Generator.dart, + additionalProperties: AdditionalProperties( + wrapper: Wrapper.flutterw, + ), +) +class TestClassHasCustomAnnotations extends OpenapiGeneratorConfig {} + +@ShouldGenerate(r''' +const additionalProperties = wrapper = 'flutterw', nullableFields = 'true'; + +const alwaysRun = false; + +const fetchDependencies = true; + +const generatorName = 'dart'; + +const inputSpecFile = ''; + +const runSourceGenOnOutput = true; + +const skipSpecValidation = false; + +const useNextGen = false; +''') +@Openapi( + inputSpecFile: '', + generatorName: Generator.dart, + additionalProperties: DioProperties( + wrapper: Wrapper.flutterw, + nullableFields: true, + ), +) +class TestClassHasDioProperties extends OpenapiGeneratorConfig {} diff --git a/openapi-generator/test/test_annotations/test_generator.dart b/openapi-generator/test/test_annotations/test_generator.dart new file mode 100644 index 0000000..1bee080 --- /dev/null +++ b/openapi-generator/test/test_annotations/test_generator.dart @@ -0,0 +1,113 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:build/src/builder/build_step.dart'; +import 'package:openapi_generator/src/utils.dart'; +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; +import 'package:source_gen/source_gen.dart' as src_gen; + +class TestGenerator extends src_gen.GeneratorForAnnotation { + final bool requireTestClassPrefix; + + const TestGenerator({this.requireTestClassPrefix = true}); + + @override + Iterable generateForAnnotatedElement(Element element, + src_gen.ConstantReader annotation, BuildStep buildStep) sync* { + assert(!annotation.isNull, 'The source generator should\'nt be null'); + + if (element is! ClassElement) { + throw src_gen.InvalidGenerationSourceError( + 'Only supports annotated classes.', + todo: 'Remove `TestAnnotation` from the associated element.', + element: element, + ); + } + + if (requireTestClassPrefix && !element.name.startsWith('TestClass')) { + throw src_gen.InvalidGenerationSourceError( + 'All classes must start with `TestClass`.', + todo: 'Rename the type or remove the `TestAnnotation` from class.', + element: element, + ); + } + + if (!(annotation.read('useNextGen').literalValue as bool)) { + if (annotation.read('cachePath').literalValue != null) { + throw src_gen.InvalidGenerationSourceError( + 'useNextGen must be set when using cachePath'); + } + } + + // KEEP THIS IN LINE WITH THE FIELDS OF THE ANNOTATION CLASS + final fields = [ + SupportedFields(name: 'additionalProperties', type: AdditionalProperties), + SupportedFields( + name: 'overwriteExistingFiles', isDeprecated: true, type: bool), + SupportedFields(name: 'skipSpecValidation', type: bool), + SupportedFields(name: 'inputSpecFile', isRequired: true, type: String), + SupportedFields(name: 'templateDirectory', type: String), + SupportedFields(name: 'generatorName', isRequired: true, type: Generator), + SupportedFields(name: 'outputDirectory', type: Map), + SupportedFields(name: 'typeMappings', type: Map), + SupportedFields(name: 'importMappings', type: Map), + SupportedFields(name: 'reservedWordsMappings', type: Map), + SupportedFields(name: 'inlineSchemaNameMappings', type: Map), + // SupportedFields(name:'inlineSchemaOptions'), + SupportedFields(name: 'apiPackage', type: String), + SupportedFields(name: 'fetchDependencies', type: bool), + SupportedFields(name: 'runSourceGenOnOutput', type: bool), + SupportedFields(name: 'alwaysRun', isDeprecated: true, type: bool), + SupportedFields(name: 'cachePath', type: String), + SupportedFields(name: 'useNextGen', type: bool), + SupportedFields(name: 'projectPubspecPath', type: String), + ]..sort((a, b) => a.name.compareTo(b.name)); + for (final field in fields) { + final v = annotation.read(field.name); + try { + if ([ + 'inputSpecFile', + 'projectPubspecPath', + 'apiPackage', + 'templateDirectory', + 'generatorName' + ].any((element) => field.name == element)) { + yield 'const ${field.name}=\'${convertToPropertyValue(v.objectValue)}\';\n'; + } else if (field.name == 'additionalProperties') { + final mapping = v.revive().namedArguments.map( + (key, value) => MapEntry(key, convertToPropertyValue(value))); + // TODO: Is this the expected behaviour? + // Iterable> entries; + // if (v.objectValue.type is DioProperties) { + // entries = DioProperties.fromMap(mapping).toMap().entries; + // } else if (v.objectValue.type is DioAltProperties) { + // entries = DioAltProperties.fromMap(mapping).toMap().entries; + // } else { + // entries = AdditionalProperties.fromMap(mapping).toMap().entries; + // } + yield 'const ${field.name}=${mapping.entries.fold('', foldStringMap(valueModifier: (value) => '\'$value\''))};'; + } else { + yield 'const ${field.name}=${convertToPropertyValue(v.objectValue)};\n'; + } + } catch (_, __) { + continue; + } + } + } + + @override + String toString() => + 'TestGenerator (requireTestClassPrefix:$requireTestClassPrefix)'; +} + +class SupportedFields { + final String name; + final bool isRequired; + final bool isDeprecated; + final T? type; + + const SupportedFields({ + required this.name, + this.isDeprecated = false, + this.isRequired = false, + required this.type, + }); +} diff --git a/openapi-generator/test/utils.dart b/openapi-generator/test/utils.dart new file mode 100644 index 0000000..6b9acf7 --- /dev/null +++ b/openapi-generator/test/utils.dart @@ -0,0 +1,60 @@ +import 'dart:io'; + +import 'package:build/build.dart'; +import 'package:build_test/build_test.dart'; +import 'package:openapi_generator/src/models/output_message.dart'; +import 'package:openapi_generator/src/openapi_generator_runner.dart'; +import 'package:source_gen/source_gen.dart'; + +final String pkgName = 'pkg'; + +final Builder builder = LibraryBuilder(OpenapiGenerator(testMode: true), + generatedExtension: '.openapi_generator'); +final testSpecPath = + '${Directory.current.path}${Platform.pathSeparator}test${Platform.pathSeparator}specs${Platform.pathSeparator}'; + +/// Runs an in memory test variant of the generator with the given [source]. +/// +/// [path] available so an override for the adds generated comment test can +/// compare the output. +Future generate(String source, {String path = 'lib/myapp.dart'}) async { + final spec = File('${testSpecPath}openapi.test.yaml').readAsStringSync(); + var srcs = { + 'openapi_generator_annotations|lib/src/openapi_generator_annotations_base.dart': + File('../openapi-generator-annotations/lib/src/openapi_generator_annotations_base.dart') + .readAsStringSync(), + 'openapi_generator|$path': ''' + import 'package:openapi_generator_annotations/src/openapi_generator_annotations_base.dart'; + $source + class MyApp { + } + ''', + 'openapi_generator|openapi-spec.yaml': spec + }; + + // Capture any error from generation; if there is one, return that instead of + // the generated output. + String? error; + void captureError(dynamic logRecord) { + // print(logRecord.runtimeType); + // print(logRecord); + // if (logRecord.error is InvalidGenerationSourceError) { + // if (error != null) throw StateError('Expected at most one error.'); + // error = logRecord.error.toString(); + // } + if (logRecord is OutputMessage) { + error = + '${error ?? ''}\n${logRecord.level} ${logRecord.message} \n ${logRecord.additionalContext} \n ${logRecord.stackTrace}'; + } else { + error = + '${error ?? ''}\n${logRecord.message ?? ''}\n${logRecord.error ?? ''}\n${logRecord.stackTrace ?? ''}'; + } + } + + var writer = InMemoryAssetWriter(); + await testBuilder(builder, srcs, + rootPackage: pkgName, writer: writer, onLog: captureError); + return error ?? + String.fromCharCodes( + writer.assets[AssetId(pkgName, 'lib/value.g.dart')] ?? []); +} diff --git a/openapi-generator/test/verify_generation_test.dart b/openapi-generator/test/verify_generation_test.dart new file mode 100644 index 0000000..e1dec00 --- /dev/null +++ b/openapi-generator/test/verify_generation_test.dart @@ -0,0 +1,17 @@ +import 'dart:io'; + +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; +import 'package:source_gen_test/source_gen_test.dart'; + +import 'test_annotations/test_generator.dart'; + +void main() async { + final reader = await initializeLibraryReaderForDirectory( + '${Directory.current.path}${Platform.pathSeparator}test${Platform.pathSeparator}test_annotations', + 'test_configs.dart', + ); + + initializeBuildLogTracking(); + + testAnnotatedElements(reader, const TestGenerator()); +}