From 328c9da3294719210e11f961baa89d9a1c708ac8 Mon Sep 17 00:00:00 2001 From: Gibah Joseph Date: Fri, 8 Nov 2024 16:51:53 +0000 Subject: [PATCH] feat: added `--enum-name-mappings` support This allows to change names of enum properties. Especially needed for enums that has `name` as a property which is reserved in `dio` etc Also fixes #91 --- .../openapi_generator_annotations_base.dart | 13 +- .../openapi_generator_annotations_test.dart | 11 + openapi-generator-cli/bin/main.dart | 1 - .../lib/src/models/generator_arguments.dart | 5 + .../lib/src/openapi_generator_runner.dart | 2 +- openapi-generator/test/builder_test.dart | 99 +++++---- .../test/github_issues_test.dart | 48 ++++ .../test/specs/issue/91/github_issue_#91.json | 208 ++++++++++++++++++ openapi-generator/test/utils.dart | 19 +- 9 files changed, 348 insertions(+), 58 deletions(-) create mode 100644 openapi-generator/test/specs/issue/91/github_issue_#91.json 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 56d4e22..9b613bc 100644 --- a/openapi-generator-annotations/lib/src/openapi_generator_annotations_base.dart +++ b/openapi-generator-annotations/lib/src/openapi_generator_annotations_base.dart @@ -93,6 +93,13 @@ class Openapi { /// --name-mappings final Map? nameMappings; + /// Specifies mappings between the enum name and the new name in the + /// format of enum_name=AnotherName,enum_name2=OtherName2. You can also + /// have multiple occurrences of this option. + + /// --enum-name-mappings + final Map? enumNameMappings; + /// specifies mappings between a given class and the import that should /// be used for that class in the format of type=import,type=import. You /// can also have multiple occurrences of this option. @@ -163,6 +170,7 @@ class Openapi { this.cleanSubOutputDirectory, this.typeMappings, this.nameMappings, + this.enumNameMappings, this.importMappings, this.reservedWordsMappings, this.inlineSchemaNameMappings, @@ -197,7 +205,7 @@ class Openapi { } if (cleanSubOutputDirectory != null) { buffer.writeln( - ' cleanSubOutputDirectory: [${cleanSubOutputDirectory!.join(", ")}],'); + ' cleanSubOutputDirectory: ["${cleanSubOutputDirectory!.join('", "')}"],'); } if (skipSpecValidation != null) { buffer.writeln(' skipSpecValidation: $skipSpecValidation,'); @@ -218,6 +226,9 @@ class Openapi { if (nameMappings != null) { buffer.writeln(' nameMappings: ${_formatMap(nameMappings!)},'); } + if (enumNameMappings != null) { + buffer.writeln(' enumNameMappings: ${_formatMap(enumNameMappings!)},'); + } if (importMappings != null) { buffer.writeln(' importMappings: ${_formatMap(importMappings!)},'); } diff --git a/openapi-generator-annotations/test/openapi_generator_annotations_test.dart b/openapi-generator-annotations/test/openapi_generator_annotations_test.dart index 74de061..52e4ade 100644 --- a/openapi-generator-annotations/test/openapi_generator_annotations_test.dart +++ b/openapi-generator-annotations/test/openapi_generator_annotations_test.dart @@ -29,6 +29,7 @@ void main() { expect(props.projectPubspecPath, isNull); expect(props.debugLogging, isFalse); expect(props.nameMappings, isNull); + expect(props.enumNameMappings, isNull); expect(props.skipIfSpecIsUnchanged, isTrue); }); group('NextGen', () { @@ -316,6 +317,16 @@ void main() { contains('nameMappings: {\'name\':\'customName\'}')); }); + test('should include enumNameMappings when set', () { + final openapi = Openapi( + enumNameMappings: {'name': 'customName'}, + inputSpec: InputSpec(path: 'example_path'), + generatorName: Generator.dart, + ); + expect(openapi.toString().replaceAll('\n', ''), + contains('enumNameMappings: {\'name\':\'customName\'}')); + }); + test('should include importMappings when set', () { final openapi = Openapi( importMappings: { diff --git a/openapi-generator-cli/bin/main.dart b/openapi-generator-cli/bin/main.dart index ed23e47..e6deb32 100644 --- a/openapi-generator-cli/bin/main.dart +++ b/openapi-generator-cli/bin/main.dart @@ -60,7 +60,6 @@ Future> loadOrCreateConfig(String configPath) async { void _logOutput(String message) { stdout.writeln(message); - print(message); } /// Constructs the default OpenAPI Generator JAR file download URL based on the version diff --git a/openapi-generator/lib/src/models/generator_arguments.dart b/openapi-generator/lib/src/models/generator_arguments.dart index 0f36c04..25fe276 100644 --- a/openapi-generator/lib/src/models/generator_arguments.dart +++ b/openapi-generator/lib/src/models/generator_arguments.dart @@ -86,6 +86,8 @@ class GeneratorArguments { /// Defines mappings between OpenAPI spec var/param/model and generated code. final Map? nameMappings; + final Map? enumNameMappings; + /// Adds reserved words mappings. /// /// Supported by [Generator.dio] & [Generator.dioAlt] generators. @@ -106,6 +108,7 @@ class GeneratorArguments { annotations.readPropertyOrDefault('generatorName', Generator.dart), typeMappings = annotations.readPropertyOrNull('typeMappings'), nameMappings = annotations.readPropertyOrNull('nameMappings'), + enumNameMappings = annotations.readPropertyOrNull('enumNameMappings'), importMappings = annotations.readPropertyOrNull('importMappings'), reservedWordsMappings = annotations.readPropertyOrNull('reservedWordsMappings'), @@ -192,6 +195,8 @@ class GeneratorArguments { '--type-mappings=${typeMappings!.entries.fold('', foldStringMap())}', if (nameMappings?.isNotEmpty ?? false) '--name-mappings=${nameMappings!.entries.fold('', foldStringMap())}', + if (enumNameMappings?.isNotEmpty ?? false) + '--enum-name-mappings=${enumNameMappings!.entries.fold('', foldStringMap())}', if (inlineSchemaOptions != null) '--inline-schema-options=${inlineSchemaOptions!.toMap().entries.fold('', foldStringMap(keyModifier: convertToPropertyKey))}', if (additionalProperties != null) diff --git a/openapi-generator/lib/src/openapi_generator_runner.dart b/openapi-generator/lib/src/openapi_generator_runner.dart index 347dfdf..e8480e7 100755 --- a/openapi-generator/lib/src/openapi_generator_runner.dart +++ b/openapi-generator/lib/src/openapi_generator_runner.dart @@ -392,7 +392,7 @@ class OpenapiGenerator extends GeneratorForAnnotation { ); if (results.exitCode != 0) { - print('===> args ${await args.jarArgs}'); + print('===> args ${args.jarArgs}'); return Future.error( OutputMessage( message: 'Failed to generate source code. Build Command output:', diff --git a/openapi-generator/test/builder_test.dart b/openapi-generator/test/builder_test.dart index da39562..d791e72 100644 --- a/openapi-generator/test/builder_test.dart +++ b/openapi-generator/test/builder_test.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -38,40 +37,58 @@ void main() { }); test('to generate command with import and type mappings', () async { - final annotations = await getReaderForAnnotation(''' -@Openapi( - inputSpec: InputSpec(path: '../openapi-spec.yaml'), - typeMappings: {'int-or-string':'IntOrString'}, - importMappings: {'IntOrString':'./int_or_string.dart'}, - generatorName: Generator.dio, - outputDirectory: '${testSpecPath}output', - ) - '''); - final args = GeneratorArguments(annotations: annotations); + final annotations = Openapi( + inputSpec: InputSpec(path: '../openapi-spec.yaml'), + typeMappings: {'int-or-string': 'IntOrString'}, + importMappings: {'IntOrString': './int_or_string.dart'}, + generatorName: Generator.dio, + outputDirectory: '${testSpecPath}output', + ); + final args = await getArguments(annotations); expect( - (await args.jarArgs).join(' '), + args.jarArgs.join(' '), contains( '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 { - final annotations = await getReaderForAnnotation(''' -@Openapi( - inputSpec: InputSpec(path: '../openapi-spec.yaml'), - typeMappings: {'int-or-string':'IntOrString'}, - inlineSchemaNameMappings: {'inline_object_2':'SomethingMapped','inline_object_4':'nothing_new'}, - generatorName: Generator.dio, - outputDirectory: '${testSpecPath}output', - ) - '''); - final args = GeneratorArguments(annotations: annotations); + final annotation = Openapi( + inputSpec: InputSpec(path: '../openapi-spec.yaml'), + typeMappings: {'int-or-string': 'IntOrString'}, + inlineSchemaNameMappings: { + 'inline_object_2': 'SomethingMapped', + 'inline_object_4': 'nothing_new' + }, + generatorName: Generator.dio, + outputDirectory: '${testSpecPath}output', + ); + final args = await getArguments(annotation); expect( - (await args.jarArgs).join(' '), + args.jarArgs.join(' '), equals(''' 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())); }); + + test('to generate command with enum name mappings', () async { + final annotation = Openapi( + inputSpec: InputSpec(path: '../openapi-spec.yaml'), + typeMappings: {'int-or-string': 'IntOrString'}, + inlineSchemaNameMappings: { + 'inline_object_2': 'SomethingMapped', + 'inline_object_4': 'nothing_new' + }, + enumNameMappings: {'name': 'name_', 'inline_object_4': 'nothing_new'}, + generatorName: Generator.dio, + outputDirectory: '${testSpecPath}output', + ); + final args = await getArguments(annotation); + expect( + args.jarArgs, + contains( + '--enum-name-mappings=name=name_,inline_object_4=nothing_new')); + }); }); group('generator dioAlt', () { @@ -110,18 +127,16 @@ class TestClassConfig extends OpenapiGeneratorConfig {} test('to generate command with import and type mappings for dioAlt', () async { - var annots = await getReaderForAnnotation(''' - @Openapi( - inputSpec: InputSpec(path:'../openapi-spec.yaml'), - typeMappings: {'int-or-string':'IntOrString'}, - importMappings: {'IntOrString':'./int_or_string.dart'}, - generatorName: Generator.dioAlt, - outputDirectory: '${testSpecPath}output', - ) - '''); - var args = GeneratorArguments(annotations: annots); + var annot = Openapi( + inputSpec: InputSpec(path: '../openapi-spec.yaml'), + typeMappings: {'int-or-string': 'IntOrString'}, + importMappings: {'IntOrString': './int_or_string.dart'}, + generatorName: Generator.dioAlt, + outputDirectory: '${testSpecPath}output', + ); + var args = await getArguments(annot); expect( - (await args.jarArgs).join(' '), + args.jarArgs.join(' '), equals( 'generate -o=${testSpecPath}output -i=../openapi-spec.yaml -g=dart2-api --import-mappings=IntOrString=./int_or_string.dart --type-mappings=int-or-string=IntOrString')); }); @@ -587,19 +602,3 @@ class TestClassConfig extends OpenapiGeneratorConfig {} }); }); } - -Future getReaderForAnnotation(String annotationDef) async { - final annotations = (await resolveSource(''' -library test_lib; -import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; - -$annotationDef -class TestClassConfig {} - ''', - (resolver) async => (await resolver.findLibraryByName('test_lib'))!)) - .getClass('TestClassConfig')! - .metadata - .map((e) => ConstantReader(e.computeConstantValue()!)) - .first; - return annotations; -} diff --git a/openapi-generator/test/github_issues_test.dart b/openapi-generator/test/github_issues_test.dart index 666532c..4944bde 100644 --- a/openapi-generator/test/github_issues_test.dart +++ b/openapi-generator/test/github_issues_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:openapi_generator/src/process_runner.dart'; +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; import 'package:path/path.dart' as path; import 'package:test/expect.dart'; import 'package:test/scaffolding.dart'; @@ -26,6 +27,53 @@ void main() { // } // }); + group('#91', () { + var issueNumber = '91'; + var parentFolder = path.join(testSpecPath, 'issue', issueNumber); + var workingDirectory = path.join(parentFolder, 'output'); + setUpAll( + () { + var workingDirectory = path.join(parentFolder, 'output'); + cleanup(workingDirectory); + }, + ); + test( + '[dart] Test that broken code is not generated for OPENAPI tictactoe example', + () async { + var inputSpecFile = + File('$parentFolder/github_issue_#$issueNumber.json'); + var generatedOutput = await generateFromAnnotation( + Openapi( + additionalProperties: AdditionalProperties( + pubName: 'tictactoe_api', + pubAuthor: 'Jon Doe', + pubAuthorEmail: 'me@example.com'), + inputSpec: InputSpec(path: inputSpecFile.path), + generatorName: Generator.dart, + cleanSubOutputDirectory: [ + './test/specs/issue/$issueNumber/output' + ], + cachePath: './test/specs/issue/$issueNumber/output/cache.json', + outputDirectory: './test/specs/issue/$issueNumber/output'), + process: processRunner, + ); + + expect(generatedOutput, + contains('Skipping source gen because generator does not need it.'), + reason: generatedOutput); + expect(generatedOutput, contains('Successfully formatted code.'), + reason: generatedOutput); + var analyzeResult = await Process.run( + 'dart', + ['analyze'], + workingDirectory: workingDirectory, + ); + expect(analyzeResult.exitCode, 0, + reason: '${analyzeResult.stdout}\n\n${analyzeResult.stderr}'); + cleanup(workingDirectory); + }); + }); + group('#114', () { var issueNumber = '114'; var parentFolder = path.join(testSpecPath, 'issue', issueNumber); diff --git a/openapi-generator/test/specs/issue/91/github_issue_#91.json b/openapi-generator/test/specs/issue/91/github_issue_#91.json new file mode 100644 index 0000000..0ffa130 --- /dev/null +++ b/openapi-generator/test/specs/issue/91/github_issue_#91.json @@ -0,0 +1,208 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Tic Tac Toe", + "description": "This API allows writing down marks on a Tic Tac Toe board\nand requesting the state of the board or of individual squares.\n", + "version": "1.0.0" + }, + "tags": [ + { + "name": "Gameplay" + } + ], + "paths": { + "/board": { + "get": { + "summary": "Get the whole board", + "description": "Retrieves the current state of the board and the winner.", + "tags": [ + "Gameplay" + ], + "operationId": "get-board", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/status" + } + } + } + } + } + } + }, + "/board/{row}/{column}": { + "parameters": [ + { + "$ref": "#/components/parameters/rowParam" + }, + { + "$ref": "#/components/parameters/columnParam" + } + ], + "get": { + "summary": "Get a single board square", + "description": "Retrieves the requested square.", + "tags": [ + "Gameplay" + ], + "operationId": "get-square", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/mark" + } + } + } + }, + "400": { + "description": "The provided parameters are incorrect", + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorMessage" + }, + "example": "Illegal coordinates" + } + } + } + } + }, + "put": { + "summary": "Set a single board square", + "description": "Places a mark on the board and retrieves the whole board and the winner (if any).", + "tags": [ + "Gameplay" + ], + "operationId": "put-square", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/mark" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/status" + } + } + } + }, + "400": { + "description": "The provided parameters are incorrect", + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorMessage" + }, + "examples": { + "illegalCoordinates": { + "value": "Illegal coordinates." + }, + "notEmpty": { + "value": "Square is not empty." + }, + "invalidMark": { + "value": "Invalid Mark (X or O)." + } + } + } + } + } + } + } + } + }, + "components": { + "parameters": { + "rowParam": { + "description": "Board row (vertical coordinate)", + "name": "row", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/coordinate" + } + }, + "columnParam": { + "description": "Board column (horizontal coordinate)", + "name": "column", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/coordinate" + } + } + }, + "schemas": { + "errorMessage": { + "type": "string", + "maxLength": 256, + "description": "A text message describing an error" + }, + "coordinate": { + "type": "integer", + "minimum": 1, + "maximum": 3, + "example": 1 + }, + "mark": { + "type": "string", + "enum": [ + ".", + "X", + "O" + ], + "description": "Possible values for a board square. `.` means empty square.", + "example": "." + }, + "board": { + "type": "array", + "maxItems": 3, + "minItems": 3, + "items": { + "type": "array", + "maxItems": 3, + "minItems": 3, + "items": { + "$ref": "#/components/schemas/mark" + } + } + }, + "winner": { + "type": "string", + "enum": [ + ".", + "X", + "O" + ], + "description": "Winner of the game. `.` means nobody has won yet.", + "example": "." + }, + "status": { + "type": "object", + "properties": { + "winner": { + "$ref": "#/components/schemas/winner" + }, + "board": { + "$ref": "#/components/schemas/board" + } + } + } + } + } +} diff --git a/openapi-generator/test/utils.dart b/openapi-generator/test/utils.dart index edfbc6c..8ad0c2f 100644 --- a/openapi-generator/test/utils.dart +++ b/openapi-generator/test/utils.dart @@ -10,6 +10,7 @@ import 'package:openapi_generator/src/process_runner.dart'; import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; import 'package:path/path.dart' as path; import 'package:source_gen/source_gen.dart'; +import 'package:test/expect.dart'; import 'package:test/scaffolding.dart'; @GenerateNiceMocks([MockSpec()]) @@ -72,8 +73,13 @@ Future generateFromPath( } Future generateFromAnnotation(Openapi openapi, - {MockProcessRunner? process, String path = 'lib/myapp.dart'}) { - return generateFromSource(openapi.toString(), process: process, path: path); + {ProcessRunner? process, String path = 'lib/myapp.dart'}) { + expect(openapi.inputSpec is RemoteSpec, isFalse, + reason: 'Please use a local spec for tests.'); + return generateFromSource(openapi.toString(), + process: process, + openapiSpecFilePath: openapi.inputSpec.path, + path: path); } /// Runs an in memory test variant of the generator with the given [source]. @@ -81,9 +87,12 @@ Future generateFromAnnotation(Openapi openapi, /// [path] available so an override for the adds generated comment test can /// compare the output. Future generateFromSource(String source, - {MockProcessRunner? process, String path = 'lib/myapp.dart'}) async { + {ProcessRunner? process, + String path = 'lib/myapp.dart', + String? openapiSpecFilePath}) async { process ??= MockProcessRunner(); - final spec = File('${testSpecPath}openapi.test.yaml').readAsStringSync(); + final spec = File(openapiSpecFilePath ?? '${testSpecPath}openapi.test.yaml') + .readAsStringSync(); var sources = { 'openapi_generator|$path': ''' import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; @@ -93,7 +102,7 @@ Future generateFromSource(String source, ''', 'openapi_generator|openapi-spec.yaml': spec }; - + printOnFailure('Generator sources =>\n${sources}'); // Capture any message from generation; if there is one, return that instead of // the generated output. String? logMessage;