⚠️ This is the instructions for the latest SNAPSHOT version (main branch). Please, see the latest released documentation if you are looking for instructions.
⚠️ Version 2.x.x of this extension (main
branch) supports Quarkus 3, and version 1.x.x (quarkus2
branch) supports Quarkus 2.
Quarkus' extension for generation of Rest Clients based on OpenAPI specification files.
This extension is based on the OpenAPI Generator Tool. Please consider donation to help them maintain the project: https://opencollective.com/openapi_generator/donate
This extension is for REST code generation for client side only. If you're looking for code generation for the server side, please take a look at the Quarkus Apicurio Extension.
Add the following dependency to your project's pom.xml
file:
⚠️ Version 2.x.x of this extension supports Quarkus 3, and version 1.x.x supports Quarkus 2.
<dependency>
<groupId>io.quarkiverse.openapi.generator</groupId>
<artifactId>quarkus-openapi-generator</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
You will also need to add or update the quarkus-maven-plugin
configuration with the following:
⚠️ You probably already have this configuration if you created your application with Code Quarkus. That said, double-check your configuration not to add anotherplugin
entry.
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
</plugin>
Now, create the directory openapi
under your src/main/
path and add the OpenAPI spec files there. We support JSON, YAML and YML extensions.
If you want to change the directory where OpenAPI files must be found, use the property quarkus.openapi-generator.codegen.input-base-dir
.
IMPORTANT: it is relative to the project base directory. For example, if you have a project called MyJavaProject
and decide to place them in MyJavaProject/openapi-definitions
, use the following property:
quarkus.openapi-generator.codegen.input-base-dir=openapi-definitions
To fine tune the configuration for each spec file, add the following entry to your properties file. In this example, our spec file is in src/main/openapi/petstore.json
:
quarkus.openapi-generator.codegen.spec.petstore_json.additional-model-type-annotations[email protected];@org.test.Bar
If a base package name is not provided, it will be used the default org.openapi.quarkus.<filename>
. For example, org.openapi.quarkus.petstore_json
.
Configuring additional-model-type-annotations
will add all annotations to the generated model files (extra details can be found in OpenApi Generator Doc).
⚠️ Note that the file namepetstore_json
is used to configure the specific information for each spec. We follow the Environment Variables Mapping Rules from Microprofile Configuration to sanitize the OpenAPI spec filename. Any non-alphabetic characters are replaced by an underscore_
.
Run mvn compile
to generate your classes in target/generated-sources/open-api-json
path:
- org.acme.openapi
- api
- PetApi.java
- StoreApi.java
- UserApi.java
- model
- Address.java
- Category.java
- Customer.java
- ModelApiResponse.java
- Order.java
- Pet.java
- Tag.java
- User.java
You can reference the generated code in your project, for example:
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.acme.openapi.api.PetApi;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.resteasy.annotations.jaxrs.PathParam;
@Produces(MediaType.APPLICATION_JSON)
@Path("/petstore")
public class PetResource {
@RestClient
@Inject
PetApi petApi;
}
See the integration-tests module for more information of how to use this extension. Please be advised that the extension is on experimental, early development stage.
You can use the quarkus-openapi-generator
with REST Client Classic or REST Client Reactive respectively. To do so add either the classic or reactive jackson dependency to your project's pom.xml
file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
⚠️ After Version 1.2.1 / 2.1.1 you need to declare the above dependency explicitly! Even if you stay with the REST Client Classic implementation!
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
</dependency>
For both implementations, the generated code is always blocking code.
When using RESTEasy Reactive:
- The client must not declare multiple MIME-TYPES with
@Consumes
- You might need to implement a
ParamConverter
for each complex type
By default, this extension generates the methods according to their returning models based on the OpenAPI specification Schema Object. If you want to return jakarta.ws.rs.core.Response
instead, you can set the return-response
property to true
.
Given you want to return jakarta.ws.rs.core.Response
for the my-openapi.yaml
file, you must add the following to your application.properties
file:
quarkus.openapi-generator.codegen.spec.my_openapi_yaml.return-response=true
Since the most part of this extension work is in the generate-code
execution phase of the Quarkus Maven's plugin, the log configuration must be set in the Maven context. When building your project, add -Dorg.slf4j.simpleLogger.log.org.openapitools=off
to the mvn
command to reduce the internal generator noise. For example:
mvn clean install -Dorg.slf4j.simpleLogger.log.org.openapitools=off
For more information, see the Maven Logging Configuration guide.
By default, the extension will process every OpenAPI specification file in the given path.
To limit code generation to only a specific set of OpenAPI specification files, you can set the quarkus.openapi-generator.codegen.include
property.
For instance, if you want to limit code generation for include-openapi.yaml
and include-openapi-2.yaml
files, you need to define the property like:
quarkus.openapi-generator.codegen.include=include-openapi.yaml,include-openapi-2.yaml
If you prefer to specify which files you want to skip, you can set the quarkus.openapi-generator.codegen.exclude
property.
For instance, if you want to skip code generation for exclude-openapi.yaml
and exclude-openapi-2.yaml
files, you need to define the property like:
quarkus.openapi-generator.codegen.exclude=exclude-openapi.yaml,exclude-openapi-2.yaml
IMPORTANT: exclude
supersedes include
, meaning that if a file is in both property it will NOT be analysed.
See the module ignore for an example of how to use this feature.
If your OpenAPI specification file has securitySchemes
definitions, the inner generator
will register ClientRequestFilter
providers for you to
implement the given authentication mechanism.
To provide the credentials for your application, you can use the Quarkus configuration support. The configuration key is composed using this
pattern: quarkus.openapi-generator.[filename].auth.[security_scheme_name].[auth_property_name]
. Where:
filename
is the sanitized name of file containing the OpenAPI spec, for examplepetstore_json
.security_scheme_name
is the sanitized name of the security scheme object definition in the OpenAPI file. Given the following excerpt, we haveapi_key
andbasic_auth
security schemes:
{
"securitySchemes": {
"api_key": {
"type": "apiKey",
"name": "api_key",
"in": "header"
},
"basic_auth": {
"type": "http",
"scheme": "basic"
}
}
}
⚠️ Note that the securityScheme name used to configure the specific information for each spec is sanitized using the same rules as for the file names.
auth_property_name
varies depending on the authentication provider. For example, for Basic Authentication we haveusername
andpassword
. See the following sections for more details.
Tip: on production environments you will likely to use HashiCorp Vault or Kubernetes Secrets to provide this information for your application.
If the OpenAPI specification file has securitySchemes
definitions, but no Security Requirement Object definitions, the generator can be configured to create these by default. In this case, for all operations without a security requirement the default one will be created. Note that the property value needs to match the name of a security scheme object definition, eg. api_key
or basic_auth
in the securitySchemes
list above.
Description | Property Key | Example |
---|---|---|
Create security for the referenced security scheme | quarkus.openapi-generator.codegen.default.security.scheme |
quarkus.openapi-generator.codegen.default.security.scheme=api_key |
See the module security for an example of how to use this feature.
For Basic HTTP Authentication, these are the supported configurations:
Description | Property Key | Example |
---|---|---|
Username credentials | quarkus.openapi-generator.[filename].auth.[security_scheme_name].username |
quarkus.openapi-generator.petstore_json.auth.basic_auth.username |
Password credentials | quarkus.openapi-generator.[filename].auth.[security_scheme_name].password |
quarkus.openapi-generator.petstore_json.auth.basic_auth-password |
For Bearer Token Authentication, these are the supported configurations:
Description | Property Key | Example |
---|---|---|
Bearer Token | quarkus.openapi-generator.[filename].auth.[security_scheme_name].bearer-token |
quarkus.openapi-generator.petstore_json.auth.bearer.bearer-token |
Similarly to bearer token, the API Key Authentication also has the token entry key property:
Description | Property Key | Example |
---|---|---|
API Key | quarkus.openapi-generator.[filename].auth.[security_scheme_name].api-key |
quarkus.openapi-generator.petstore_json.auth.api_key.api-key |
The API Key scheme has an additional property that requires where to add the API key in the request token: header, cookie or query. The inner provider takes care of that for you.
The extension will generate a ClientRequestFilter
capable to add OAuth2 authentication capabilities to the OpenAPI operations that require it. This means that you can use
the Quarkus OIDC Extension configuration to define your authentication flow.
The generated code creates a named OidcClient
for each Security Scheme listed in the OpenAPI specification files. For example, given
the following excerpt:
{
"securitySchemes": {
"petstore_auth": {
"type": "oauth2",
"flows": {
"implicit": {
"authorizationUrl": "https://petstore3.swagger.io/oauth/authorize",
"scopes": {
"write:pets": "modify pets in your account",
"read:pets": "read your pets"
}
}
}
}
}
}
You can configure this OidcClient
as:
quarkus.oidc-client.petstore_auth.auth-server-url=https://petstore3.swagger.io/oauth/authorize
quarkus.oidc-client.petstore_auth.discovery-enabled=false
quarkus.oidc-client.petstore_auth.token-path=/tokens
quarkus.oidc-client.petstore_auth.credentials.secret=secret
quarkus.oidc-client.petstore_auth.grant.type=password
quarkus.oidc-client.petstore_auth.grant-options.password.username=alice
quarkus.oidc-client.petstore_auth.grant-options.password.password=alice
quarkus.oidc-client.petstore_auth.client-id=petstore-app
The configuration suffix quarkus.oidc-client.petstore_auth
is exclusive for the schema defined in the specification file and the schemaName
is sanitized by applying the rules described above.
For this to work you must add Quarkus OIDC Client Filter Extension to your project:
RESTEasy Classic:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-filter</artifactId>
</dependency>
RESTEasy Reactive:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-reactive-filter</artifactId>
</dependency>
See the module generation-tests for an example of how to use this feature.
The authorization token propagation can be used with OpenApi operations secured with a security scheme of type "oauth2" or "bearer". When configured, you can propagate the authorization tokens passed to your service and the invocations to the REST clients generated by the quarkus-openapi-generator.
Let's see how it works by following a simple example:
Imagine that we have a updatePet
operation defined in the petstore.json
specification file and secured with the petstore_auth
security scheme.
The code below shows a simple example of the usage of this operation in a user-programmed service.
import org.acme.api.PetApi;
import org.acme.model.Pet;
import org.eclipse.microprofile.rest.client.inject.RestClient;
/**
* User programmed service.
*/
@Path("/petstore")
public class PetResource {
/**
* Inject the rest client generated by the quarkus-openapi-generator.
*/
@Inject
@RestClient
PetApi petApi;
/**
* User programmed operation.
*/
@Path("/pet/{id}")
@PATCH
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response customUpdatePet(@PathParam("id") long id, PetData petData) {
// Create a new instance of the Pet class generated by the quarkus-openapi-generator and
// populate accordingly.
Pet pet = new Pet();
pet.setId(id);
applyDataToPet(pet, petData);
// Execute the rest call using the generated client.
// The petstore.json open api spec stays that the "updatePet" operation is secured with the
// security scheme "petstore_auth".
petApi.updatePet(pet);
// Do other required things and finally return something.
return Response.ok().build();
}
public static class PetData {
// Represents the Pet modifiable data sent to the user programmed service.
}
private void applyDataToPet(Pet pet, PetData petData) {
// Set the corresponding values to the Pet instance.
}
}
Let's see what happens when the PetResource service customUpdatePet
operation is invoked by a third party.
- The
customUpdatePet
operation is invoked. - An authorization token is obtained using the corresponding
petstore_auth
OidcClient configuration. (for more information see OAuth2 Authentication) - The authorization token is automatically passed along the PetApi
updatePet
operation execution using an automatically generated request filter, etc.
However, there are scenarios where we want to propagate the authorization token that was initially passed to the PetResource service when the customUpdatePet
operation was invoked instead of having to obtain it by using the OidcClient
.
- The user service
customUpdatePet
operation is invoked, and an authorization token is passed by the third party typically by using the HTTPAuthorization
header. - The incoming authorization token is automatically passed along the PetApi
updatePet
operation execution according to the user-provided configuration.
⚠️ When configured, the token propagation applies to all the operations secured with the samesecurityScheme
in the same specification file.
The token propagation can be used with type "oauth2" or "bearer" security schemes. Finally, considering that a given security scheme might be configured on a set of operations in the same specification file when configured, it'll apply to all these operations.
Property Key | Example |
---|---|
quarkus.openapi-generator.[filename].auth.[security_scheme_name].token-propagation=[true,false] |
quarkus.openapi-generator.petstore_json.auth.petstore_auth.token-propagation=true Enables the token propagation for all the operations that are secured with the petstore_auth scheme in the petstore_json file. |
quarkus.openapi-generator.[filename].auth.[security_scheme_name].header-name=[http_header_name] |
quarkus.openapi-generator.petstore_json.auth.petstore_auth.header-name=MyHeaderName Says that the authorization token to propagate will be read from the HTTP header MyHeaderName instead of the standard HTTP Authorization header. |
You can define the CircuitBreaker annotation from MicroProfile Fault Tolerance
in your generated classes by setting the desired configuration in application.properties
.
Let's say you have the following OpenAPI definition:
{
"openapi": "3.0.3",
"info": {
"title": "Simple API",
"version": "1.0.0-SNAPSHOT"
},
"paths": {
"/hello": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/bye": {
"get": {
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}
And you want to configure Circuit Breaker for the /bye
endpoint, you can do it in the following way:
Add the SmallRye Fault Tolerance extension to your project's pom.xml
file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
</dependency>
Assuming your Open API spec file is in src/main/openapi/simple-openapi.json
, add the following configuration to your application.properties
file:
# Note that the file name must have only alphabetic characters or underscores (_).
quarkus.openapi-generator.codegen.spec.simple_openapi_json.base-package=org.acme.openapi.simple
# Enables the CircuitBreaker extension for the byeGet method from the DefaultApi class
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/enabled=true
With the above configuration, your Rest Clients will be created with a code similar to the following:
package org.acme.openapi.simple.api;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.MediaType;
@Path("")
@RegisterRestClient(configKey="simple-openapi_json")
public interface DefaultApi {
@GET
@Path("/bye")
@Produces({ "text/plain" })
@org.eclipse.microprofile.faulttolerance.CircuitBreaker
public String byeGet();
@GET
@Path("/hello")
@Produces({ "text/plain" })
public String helloGet();
}
You can also override the default Circuit Breaker configuration by setting the properties
in application.properties
just as you would for a traditional MicroProfile application:
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/failOn=java.lang.IllegalArgumentException,java.lang.NullPointerException
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/skipOn=java.lang.NumberFormatException
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/delay=33
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/delayUnit=MILLIS
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/requestVolumeThreshold=42
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/failureRatio=3.14
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/successThreshold=22
See the module circuit-breaker for an example of how to use this feature.
The rest client also supports request with mime-type multipart/form-data and, if the schema of the request body is known in advance, we can also automatically generate the models of the request bodies.
⚠️ Tip: RESTEasy Reactive supports multipart/form-data out of the box. Thus, no additional dependency is required.
If you're using RESTEasy Classic, you need to add the following additional dependency to your pom.xml
:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-multipart</artifactId>
</dependency>
For any multipart/form-data operation a model for the request body will be generated. Each part of the multipart is a field in this model that is annotated with the following annotations:
jakarta.ws.rs.FormParam
, where the value parameter denotes the part name,PartType
, where the parameter is the jax-rs MediaType of the part (see below for details),- and, if the part contains a file,
PartFilename
, with a generated default parameter that will be passed as the fileName sub-header in the Content-Disposition header of the part.
For example, the model for a request that requires a file, a string and some complex object will look like this:
public class MultipartBody {
@FormParam("file")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
@PartFilename("defaultFileName")
public File file;
@FormParam("fileName")
@PartType(MediaType.TEXT_PLAIN)
public String fileName;
@FormParam("someObject")
@PartType(MediaType.APPLICATION_JSON)
public MyComplexObject someObject;
}
Then in the client, when using RESTEasy Classic, the org.jboss.resteasy.annotations.providers.multipart.MultipartForm
annotation is added in front of the multipart parameter:
@Path("/echo")
@RegisterRestClient(baseUri="http://my.endpoint.com/api/v1", configKey="multipart-requests_yml")
public interface MultipartService {
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_PLAIN)
String sendMultipartData(@MultipartForm MultipartBody data);
}
When using RESTEasy Reactive, the jakarta.ws.rs.BeanParam
annotation is added in front of the multipart parameter:
@Path("/echo")
@RegisterRestClient(baseUri="http://my.endpoint.com/api/v1", configKey="multipart-requests_yml")
public interface MultipartService {
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_PLAIN)
String sendMultipartData(@jakarta.ws.rs.BeanParam MultipartBody data);
}
See Quarkus - Using the REST Client with Multipart and the RESTEasy JAX-RS specifications for more details.
⚠️ MultipartForm
is deprecated when using RESTEasy Reactive.
baseURI
value of RegisterRestClient
annotation is extracted from the servers
section of the file, if present. If not, it will be left empty and it is expected you set up the uri to be used in your configuration.
Importantly, if some multipart request bodies contain complex objects (i.e. non-primitives) you need to explicitly tell the Open API generator to create models for these objects by setting
the skip-form-model
property corresponding to your spec in the application.properties
to false
, e.g.:
quarkus.openapi-generator.codegen.spec.my_multipart_requests_yml.skip-form-model=false
See the module multipart-request for an example of how to use this feature.
The OAS 3.0 specifies the following default content-types for a multipart:
- If the property is a primitive, or an array of primitive values, the default Content-Type is
text/plain
- If the property is complex, or an array of complex values, the default Content-Type is
application/json
- If the property is a
type: string
withformat: binary
orformat: base64
(aka a file object), the default Content-Type isapplication/octet-stream
A different content-type may be defined in your api spec, but this is not yet supported in the code generation. Also, this "annotation-oriented" approach of RestEasy (i.e. using @MultipartForm
to
denote the multipart body parameter) does not seem to properly support the unmarshalling of arrays of the same type (e.g. array of files), in these cases it uses Content-Type equal
to application/json
.
Having the files in the src/main/openapi
directory will generate the REST stubs by default. Alternatively, you can implement
the io.quarkiverse.openapi.generator.deployment.codegen.OpenApiSpecInputProvider
interface to provide a list of InputStream
s of OpenAPI specification files. This is useful in scenarios where you want to dynamically generate the client code without having the target spec file
saved locally in your project.
See the example implementation here
The domain objects are classes generated in the model
package. These classes might have deprecated attributes in the Open API specification
file. By default, these attributes are generated. You can fine tune this behavior if the deprecated attributes should not be generated.
Use the property key <base_package>.model.MyClass.generateDeprecated=false
to disable the deprecated attributes in the given model. For example org.acme.weather.Country.generatedDeprecated=false
.
The client objects are classes generated in the api
package. These classes might have deprecated operations in the Open API specification
file. By default, these operations are generated. You can fine tune this behavior if the deprecated operations should not be generated.
Use the property key <base_package>.api.MyClass.generateDeprecated=false
to disable the deprecated operations in the given API. For example org.acme.openapi.simple.api.DefaultApi.generatedDeprecated=false
.
In some cases, we need custom RegisterProvider
for generated api, e.g. logging. You can define your own Providers in application.properties
:
Property Key | Example |
---|---|
quarkus.openapi-generator.codegen.spec.[filename].custom-register-providers |
quarkus.openapi-generator.codegen.spec.simple_openapi_json.custom-register-providers=org.test.Foo,org.test.Bar Provider classes are separated by commas |
With the above configuration, the extension generates your Rest Clients with a code similar to the following:
package org.acme.openapi.simple.api;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.MediaType;
@Path("")
@RegisterRestClient(configKey="simple-openapi_json")
@RegisterProvider(org.test.Foo.class)
@RegisterProvider(org.test.Bar.class)
public interface DefaultApi {
@GET
@Path("/bye")
@Produces({ "text/plain" })
@org.eclipse.microprofile.faulttolerance.CircuitBreaker
public String byeGet();
}
See the module register-provider for an example of how to use this feature.
Use the property key quarkus.openapi-generator.codegen.validateSpec=false
to disable validating the input specification file before code generation. By default, invalid specifications will result in an error.
It's possible to remap types in the generated files. For example, instead of a File
you can configure the code generator to use InputStream
for all file upload parts of multipart request, or you could change all UUID
types to String
. You can configure this in your application.properties
using the following configuration keys:
Description | Property Key | Example |
---|---|---|
Type Mapping | quarkus.openapi-generator.codegen.spec.[filename].type-mappings.[oas_type] |
quarkus.openapi-generator.codegen.spec.my_spec_yml.type-mappings.File=InputStream will use InputStream as type for all objects of the OAS File type. |
Import Mapping | quarkus.openapi-generator.codegen.spec.[filename].import-mappings.[type] |
quarkus.openapi-generator.codegen.spec.my_spec_yml.import-mappings.File=java.io.InputStream will replace the default import java.io.File with import java.io.InputStream |
Note that these configuration properties are maps. For the type-mapping the keys are OAS data types and the values are Java types.
Another common example is needing java.time.Instant
as type for date-time fields in your POJO classes. You can achieve with these settings:
quarkus.openapi-generator.codegen.spec.my_spec_yml.type-mappings.DateTime=Instant
quarkus.openapi-generator.codegen.spec.my_spec_yml.import-mappings.Instant=java.time.Instant
It's also possible to only use a type mapping with a fully qualified name, for instance quarkus.openapi-generator.codegen.spec.my_spec_yml.type-mappings.File=java.io.InputStream
. For more information and a list of all types see the OpenAPI generator documentation on Type Mappings and Import Mappings.
See the module type-mapping for an example of how to use this feature.
These are the known limitations of this pre-release version:
- Only Jackson support
We will work in the next few releases to address these use cases, until there please provide feedback for the current state of this extension. We also love contributions ❤️
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!