Skip to content

dart-ogurets/dart-openapi-maven

Repository files navigation

Opinionated Dart OpenAPI v3 Plugin

This plugin was originally designed for use by the OpenAPi v3 Maven Plugin, but works with the command line generator and the Gradle plugin. it generates excellent Dart code and uses Dio.

Note
we are currently using 7.0.1 of the OpenAPI Maven Plugin.

Sponsors

This project is part of the Open Source project FeatureHub, please consider sponsoring that project to support this generator if you use it.

How to use

This tool can be used via Maven, Gradle or from the command line. There are important additional properties that you can configure especially around support for null safety, so read on!

Usage in Maven

To use it, do something like this:

      <plugin>
        <groupId>org.openapitools</groupId>
        <artifactId>openapi-generator-maven-plugin</artifactId>
        <version>6.4.0</version>
        <dependencies>
          <dependency>
            <groupId>com.bluetrainsoftware.maven</groupId>
            <artifactId>openapi-dart-generator</artifactId>
            <version>7.2</version>
          </dependency>
        </dependencies>
        <executions>
          <execution>
            <id>mr-api</id>
            <goals>
              <goal>generate</goal>
            </goals>
            <phase>generate-sources</phase>
            <configuration>
              <output>${project.basedir}</output>
              <inputSpec>${project.basedir}/target/mrapi/mr-api.yaml</inputSpec>
              <generatorName>dart2-api</generatorName>
              <enablePostProcessFile>true</enablePostProcessFile>
              <additionalProperties>
                <additionalProperty>
                  pubName=k8s_api
                </additionalProperty>
                <additionalProperty>
                  useDio5=true
                </additionalProperty>
              </additionalProperties>
            </configuration>
          </execution>
        </executions>
      </plugin>

We use the enablePostProcessFile because if it finds a FLUTTER environment variable, it will run dartfmt on the generated files.

Usage in Gradle

In Gradle, you have to make this extra library available to the buildscript, so at the top of your file before your plugin declaration you need a section similar to:

buildscript {
	repositories {
		mavenLocal()
		mavenCentral()
	}
	dependencies {
		classpath "com.bluetrainsoftware.maven:openapi-dart-generator:7.1"
	}
}

from there in your openApiGenerator definitions, you specify the server format in additional properties:

openApiGenerate {
    generatorName = "dart2-api"
    inputSpec = openApiSpec
    outputDir = openApiOutputDir
    apiPackage = "com.your-company.api"
    modelPackage = "com.your-company.api.model"
	  additionalProperties = [
	    'key': 'value'
	  ]
    configOptions = [:]
}

Usage from the command line

Please refer to the article on Medium.

Please note the versions of all the libraries have moved on since then.

Additional Properties

Additional properties allow you to customise how code is generated and we honour 2 of them above the normal ones.

  • useDio5: this will make the library use Dio5 and the associated OpenAPI common library associated with Dio5.

  • pubspec-dependencies: anything in this will be split on a command added to the dependencies section of your generated code.

  • pubspec-dev-dependencies: anything here will be added to the dev dependencies of your generated code.

  • nullSafe=true' and `nullSafe-array-default=true - these two normally go together and change the minimum version of Dart to 2.12 and generate null safe code. Using the nullSafe-array-default, it makes even arrays that are not listed as being required in your OpenAPI "required" but making them always generate a default value of []. This ends up being considerably easier to use.

  • listAnyOf=false - this will turn off AnyOf support. This would be a bit weird, but you can do it if you want.

  • disableCopyWith - if this is specified, then the copyWith functionality will be disabled. On complex OpenAPI definitions, the combinations of null safety and nested classes can cause incomplete or invalid code to be generated. We recommend disabling the generation of the copy-with code for that purpose. It is simply a convenience for coding and is not required as part of the API.

the normal ones include:

name: {{pubName}}
version: {{pubVersion}}
description: {{pubDescription}}

They are specified in the configuration above (Maven example here):

<configuration>
  <output>${project.basedir}</output>
  <inputSpec>${project.basedir}/target/mrapi/mr-api.yaml</inputSpec>
  <generatorName>dart2-api</generatorName>
  <enablePostProcessFile>true</enablePostProcessFile>
    <additionalProperties>
      <additionalProperty>pubName=myapi</additionalProperty>
    </additionalProperties>
</configuration>

See the src/it project for more examples.

Additional files

You may need to use additional files - just add them to the project or add them via a dependency. You can use the importMappings section of the configuration to bring in any packages (internal or external) into the library definition. For example

<typeMappings>int-or-string=IntOrString</typeMappings>
<importMappings>IntOrString=./int_or_string.dart</importMappings>

This will note anything that has format: int-or-string and map this to the class IntOrString and there will be an extra part import for it added to the library main. You must write this class to have the expected criteria from the rest of the library but it does allow you to support custom types. Again, an example of this is in src/it.

If you use something like this instead:

<typeMappings>int-or-string=IntOrString</typeMappings>
<importMappings>IntOrString=package:k8s-dart/int_or_string.dart</importMappings>

Then it will add it to the import section of your library allowing you to use external libraries.

Using dependencies to pull in apis from other artifacts

We typically use the Dependency Plugin to copy the actual OpenAPI yaml file from a different project - such as in this case "mr-api".

Note
you can also customise this using my MergeYaml plugin if you wish to merge apis together. If often do this for testing purposes.
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
        <executions>
          <execution>
            <id>unpack todo api</id>
            <phase>initialize</phase>
            <goals>
              <goal>unpack</goal>
            </goals>
            <configuration>
              <artifactItems>
                <artifactItem>
                  <groupId>io.yourapi.mr</groupId>
                  <artifactId>mr-api</artifactId>
                  <version>1.1-SNAPSHOT</version>
                  <type>jar</type>
                  <outputDirectory>${project.basedir}/target/mrapi/</outputDirectory>
                </artifactItem>
              </artifactItems>
              <includes>
                **/*.yaml
              </includes>
              <overWriteReleases>true</overWriteReleases>
              <overWriteSnapshots>true</overWriteSnapshots>
            </configuration>
          </execution>
        </executions>
      </plugin>

And we include a custom Clean plugin definition to ensure old artifacts aren’t left behind as you generate.

      <plugin>
        <artifactId>maven-clean-plugin</artifactId>
        <version>3.1.0</version>
        <configuration>
          <filesets>
            <fileset>
              <directory>lib</directory>
              <includes>
                <include>**/**</include>
              </includes>
            </fileset>
            <fileset>
              <directory>docs</directory>
              <includes>
                <include>**/**</include>
              </includes>
            </fileset>
            <fileset>
              <directory>test</directory>
              <includes>
                <include>**/**</include>
              </includes>
            </fileset>
            <fileset>
              <directory>.openapi-generator</directory>
              <includes>
                <include>**/**</include>
              </includes>
            </fileset>
            <fileset>
              <directory>.openapi-generator</directory>
              <includes>
                <include>**/**</include>
              </includes>
            </fileset>
          </filesets>
        </configuration>
      </plugin>

I need to do something special with the Dio layer!

The DioClientDelegate we provide can be fully overridden - in Dart all classes are also interfaces so if you wish to do special things in the underlying "guts" of the Dio library you can easily do so. Caching is an example, one of our users has an example:

Ensure you include 304 as a valid return type for cached APIs.

import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:openapi_dart_common/openapi.dart';
import 'package:xxxx/api_delegate.dart'; // The file below

final _cacheOptions = CacheOptions(
  store: MemCacheStore(),
  hitCacheOnErrorExcept: [401, 403],
  maxStale: const Duration(days: 7),
);

class API extends ApiClient {
    API._internal() : super(basePath: "https://xxxxxx/api") {
    apiClientDelegate = CustomDioClientDelegate();
    final dioDelegate = apiClientDelegate as CustomDioClientDelegate;
    dioDelegate.client.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
  }
}
import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:openapi_dart_common/openapi.dart';

class CustomDioClientDelegate implements DioClientDelegate {
  @override
  final Dio client;

  CustomDioClientDelegate([Dio? client]) : client = client ?? Dio();

  @override
  Future<ApiResponse> invokeAPI(
      String basePath, String path, Iterable<QueryParam> queryParams, Object? body, Options options,
      {bool passErrorsAsApiResponses = false}) async {
    final String url = basePath + path;

    // fill in query parameters
    final Map<String, String> qParams = _convertQueryParams(queryParams);

    options.responseType = ResponseType.plain;
    options.receiveDataWhenStatusError = true;

    // Dio can't cope with this in both places, it just adds them together in a stupid way
    if (options.headers != null && options.headers!['Content-Type'] != null) {
      options.contentType = options.headers!['Content-Type']?.toString();
      options.headers!.remove('Content-Type');
    }

    try {
      Response<String> response;

      if (['GET', 'HEAD', 'DELETE'].contains(options.method)) {
        response = await client.request<String>(url, options: options, queryParameters: qParams);
      } else {
        response = await client.request<String>(url, options: options, data: body, queryParameters: qParams);
      }

      final stream = _jsonToStream(response.data);
      return ApiResponse(response.statusCode ?? 500, _convertHeaders(response.headers), stream);
    } catch (ex, stack) {
      if (ex is! DioError) rethrow;

      if (passErrorsAsApiResponses) {
        if (ex.response == null) {
          return ApiResponse(500, {}, null)
            ..innerException = ex
            ..stackTrace = stack;
        }

        // if (e.response.data)
        if (ex.response!.data is String?) {
          final response = ex.response!;
          final json = response.data as String;

          return ApiResponse(response.statusCode ?? 500, _convertHeaders(response.headers), _jsonToStream(json));
        } else {
          print(
              "ex is not 'String?' ${ex.response.runtimeType.toString()} ${ex.response!.data?.runtimeType.toString() ?? ''}");
        }
      }

      if (ex.response == null) {
        throw ApiException.withInner(500, 'Connection error', ex, stack);
      } else {
        throw ApiException.withInner(ex.response?.statusCode ?? 500, ex.response?.data as String?, ex, stack);
      }
    }
  }

  Map<String, String> _convertQueryParams(Iterable<QueryParam> queryParams) {
    final Map<String, String> qp = {};
    for (final q in queryParams) {
      qp[q.name] = q.value;
    }
    return qp;
  }

  Map<String, List<String>> _convertHeaders(Headers headers) {
    final Map<String, List<String>> res = {};
    headers.forEach((k, v) => res[k] = v);
    return res;
  }

  Stream<List<int>> _jsonToStream(String? json) {
    return Stream<List<int>>.value(utf8.encode(json ?? ""));
  }
}

Testing

If you are trying to make changes to the repository, I recommend adding a new test to "SampleRunner" with your options - you can change it to point to your own yaml OpenAPI file and it will generate the project into target/SampleRunner. Also open this project (from target/SampleRunner) in the IDE and you will be able to run the test and regenerate the project (just don’t do a mvn clean).

If you run and debug the test in the IDE it means you can see what OpenAPI is putting in what places and see the breakdown of the structures and tagging that is going on.

If you add something, please make sure you provide integration tests - so add the parts of the yaml that don’t otherwise work to the projects in src/it/* projects and run the mvn verify command. Please make sure you add tests to the test subfolder to ensure the code is generating and working the way you want, especially if you add stuff to the deep copy mechanism, the hash or equals mechanisms.

To test locally you can run tests by invoking this command:

mvn clean verify

The source for the tests is located in src/k8s folders. The generated test output will be in target/it/k8s.

Changelog

  <globalProperties>
    <skipFormModels>false</skipFormModels>
  </globalProperties>
  • 3.10 - support extension methods for mapping between enum names and types

  • 3.9 - support for reserved words mappings in variable names

  • 3.8 - non complex lists were not being compared in equals or hash functions correctly

  • 3.7 - resolved an issue with inline enums - thanks to Robert of https://github.com/BlackBeltTechnology

  • 3.5 - resolved an issue where class level variables were being duplicated from the parent, causing equals to fail

  • 3.4 - backed out some experimental features and exposed the serialization capabilities. fixed a NPE on the copyWith.

  • 3.3 - introduces feedback from Jpi & Brian Janssen around making all the LocalApi serializer calls static, so toJson() can be called by jsonEncode without introducing non-Dart-like complexity. Further, we introduce an experimental vendor extension on an operation called x-dart-rich-operationId - this has to be another operation id, not the same as operationId: as Dart cannot have two functions with different return types. It will give you the same method signature, but return the deserialized object, the headers and the status code. It does not interfere with the existing code generation and was introduced to allow situations where session data is being returned outside of the body. It will be documented further once accepted.

  • 3.2 - introduces support for extra elements in the pubspec, import support, arbitrary part support and a fix for arrays of date times and dates

  • 3.1 - introduces support for the Kubernetes API in terms of compilation along with a considerable degree of support for the complexity of that API. It also reduces the code generated when no forms are used or forms are used but not json. It also supports the return of non-json data by just returning the string, allowing you to decode it.

  • 2.9 - support inheritance using allOf where it exists. If the model from the generator provides a parentClass we modify the output to now support the correct code generation to support inheritance. Resolve issue around headers not being merged if passed by user. Fix issue with form data generation where fields need to be json encoded.

  • 2.8 - resolves a number of Dart Analysis issues

  • 2.6/7 - add in a new copyWith() method that allows you to make deep copies of the model and replace specific parts

  • 2.5 - fixed a dangling } issue from pedantic, fixed additionalProperties support for k8s api generation, added integration tests

  • 2.4 - fixed an issue if no authNames were being provided, a List<dynamic> was created instead of a List<String>

  • 2.3 - this is a cleanup of the move to Dio based on pedantic feedback

  • 1.5 - fixed the pubspec.yaml

  • 1.4 - added in serialization of outgoing data because Dart cannot serialize an enum using json.

Roadmap

  • We intend to support the oneOf syntax for parameters for request and response types by using optional parameters. This won’t change method calls when you are only passing one type.

  • We will wrap exceptions that have generated models

  • We intend to be generating server side code for supporting Dart server side applications.

  • We are considering memoization

Known Issues

  • When you have a nullable Map type with a default value (e.g. {}), e.g. Map<String, Filter>? then the copyWith does not function correctly because {} as Map<String, Filter>? is not correct and Map<String, Filter>?.from({}) is not valid syntax. We will need to use the Dart 2.16 functionality to create a new type for this map and then we can resolve the issue.

  • nullable: true is being removed from OpenAPI is not part of the standard form 3.1 onwards. You can specify a field as being required and being nullable: true, but this generator expects that required fields are not nullable.