Skip to content

Commit

Permalink
Update OpenAPI templates and generator (#796)
Browse files Browse the repository at this point in the history
## Type of change
```
- [ ] Bug fix
- [ ] New feature development
- [x] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
```

## Objective
Update OpenAPI templates and generator to the latest version available:
- Generator: 7.6.0
- Templates:
OpenAPITools/openapi-generator@638af0f

The first commit (5564407) contains the
changes from upstream as-is, the second commit
(1249b12) contains all the changes we
need to make. This way it should be easier to apply the changes in the
future.

The diff with upstream is a bit smaller now and also does less
unnecessary allocations. Also I've updated the Cargo.toml template to
more closely match ours. Once this is approved I'll make a separate PR
to regenerate the bindings.

The changes to the generated bindings are fairly minor:
- The bindings are now capable of base64 decoding values that are marked
as `[JsonConverter(typeof(Base64UrlConverter))]` on the server. We have
some API endpoints that make use of it on the server but at the moment
we're not using them.
- The model structs now implement Default
- The resulting code generates less warnings now, so we can be more
selective with what warnings we disable.
  • Loading branch information
dani-garcia committed May 23, 2024
1 parent 3c1e3c6 commit 5d8536b
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 117 deletions.
2 changes: 1 addition & 1 deletion openapitools.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.2.0"
"version": "7.6.0"
}
}
6 changes: 4 additions & 2 deletions support/build-api.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# Delete old directory to ensure all files are updated
rm -rf crates/bitwarden-api-api/src

VERSION=$(grep '^version = ".*"' Cargo.toml | cut -d '"' -f 2)

# Generate new API bindings
npx openapi-generator-cli generate \
-i ../server/api.json \
-g rust \
-o crates/bitwarden-api-api \
--package-name bitwarden-api-api \
-t ./support/openapi-template \
--additional-properties=packageVersion=1.0.0
--additional-properties=packageVersion=$VERSION,packageDescription=\"Api bindings for the Bitwarden API.\"

# Delete old directory to ensure all files are updated
rm -rf crates/bitwarden-api-identity/src
Expand All @@ -20,7 +22,7 @@ npx openapi-generator-cli generate \
-o crates/bitwarden-api-identity \
--package-name bitwarden-api-identity \
-t ./support/openapi-template \
--additional-properties=packageVersion=1.0.0
--additional-properties=packageVersion=$VERSION,packageDescription=\"Api bindings for the Bitwarden Identity API.\"

rustup toolchain install nightly
cargo +nightly fmt
Expand Down
64 changes: 22 additions & 42 deletions support/openapi-template/Cargo.mustache
Original file line number Diff line number Diff line change
@@ -1,46 +1,28 @@
[package]
name = "{{{packageName}}}"
version = "{{#lambdaVersion}}{{{packageVersion}}}{{/lambdaVersion}}"
{{#infoEmail}}
authors = ["{{{.}}}"]
{{/infoEmail}}
{{^infoEmail}}
authors = ["OpenAPI Generator team and contributors"]
{{/infoEmail}}
{{#appDescription}}
{{#packageDescription}}
description = "{{{.}}}"
{{/appDescription}}
{{#licenseInfo}}
license = "{{.}}"
{{/licenseInfo}}
{{^licenseInfo}}
# Override this license by providing a License Object in the OpenAPI.
license = "Unlicense"
{{/licenseInfo}}
edition = "2018"
{{#publishRustRegistry}}
publish = ["{{.}}"]
{{/publishRustRegistry}}
{{#repositoryUrl}}
repository = "{{.}}"
{{/repositoryUrl}}
{{#documentationUrl}}
documentation = "{{.}}"
{{/documentationUrl}}
{{#homePageUrl}}
homepage = "{{.}}
{{/homePageUrl}}
{{/packageDescription}}
categories = ["api-bindings"]

version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
homepage.workspace = true
repository.workspace = true
license-file.workspace = true
keywords.workspace = true

[dependencies]
serde = "^1.0"
serde_derive = "^1.0"
serde = { version = "^1.0", features = ["derive"] }
{{#serdeWith}}
serde_with = "^2.0"
serde_with = { version = "^3.8", default-features = false, features = ["base64", "std", "macros"] }
{{/serdeWith}}
serde_json = "^1.0"
serde_repr = "^0.1"
url = "^2.2"
uuid = { version = "^1.0", features = ["serde", "v4"] }
url = "^2.5"
uuid = { version = "^1.8", features = ["serde", "v4"] }
{{#hyper}}
hyper = { version = "~0.14", features = ["full"] }
hyper-tls = "~0.5"
Expand All @@ -55,17 +37,15 @@ secrecy = "0.8.0"
{{/withAWSV4Signature}}
{{#reqwest}}
{{^supportAsync}}
[dependencies.reqwest]
version = "^0.11"
features = ["json", "blocking", "multipart"]
reqwest = { version = "^0.12", features = ["json", "blocking", "multipart"] }
{{#supportMiddleware}}
reqwest-middleware = { version = "^0.3", features = ["json", "blocking", "multipart"] }
{{/supportMiddleware}}
{{/supportAsync}}
{{#supportAsync}}
reqwest = { version = "^0.12", features = ["json", "multipart", "http2"], default-features = false }
{{#supportMiddleware}}
reqwest-middleware = "0.2.0"
reqwest-middleware = { version = "^0.3", features = ["json", "multipart"] }
{{/supportMiddleware}}
[dependencies.reqwest]
version = "^0.11"
features = ["http2", "json", "multipart"]
default-features = false
{{/supportAsync}}
{{/reqwest}}
1 change: 1 addition & 0 deletions support/openapi-template/README.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This API client was generated by the [OpenAPI Generator](https://openapi-generat
{{^hideGenerationTimestamp}}
- Build date: {{{generatedDate}}}
{{/hideGenerationTimestamp}}
- Generator version: {{generatorVersion}}
- Build package: `{{{generatorClass}}}`

## Installation
Expand Down
5 changes: 3 additions & 2 deletions support/openapi-template/hyper/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::option::Option;
use hyper;
use futures::Future;

use crate::models;
use super::{Error, configuration};
use super::request as __internal_request;

Expand All @@ -28,7 +29,7 @@ impl<C: hyper::client::connect::Connect> {{{classname}}}Client<C>
pub trait {{{classname}}} {
{{#operations}}
{{#operation}}
fn {{{operationId}}}(&self, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{^isUuid}}&str{{/isUuid}}{{/isString}}{{#isUuid}}&str{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) -> Pin<Box<dyn Future<Output = Result<{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}, Error>>>>;
fn {{{operationId}}}(&self, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{^isUuid}}&str{{/isUuid}}{{/isString}}{{#isUuid}}&str{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) -> Pin<Box<dyn Future<Output = Result<{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}, Error>>>>;
{{/operation}}
{{/operations}}
}
Expand All @@ -38,7 +39,7 @@ impl<C: hyper::client::connect::Connect>{{{classname}}} for {{{classname}}}Clien
{{#operations}}
{{#operation}}
#[allow(unused_mut)]
fn {{{operationId}}}(&self, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{^isUuid}}&str{{/isUuid}}{{/isString}}{{#isUuid}}&str{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) -> Pin<Box<dyn Future<Output = Result<{{^returnType}}(){{/returnType}}{{#returnType}}{{{.}}}{{/returnType}}, Error>>>> {
fn {{{operationId}}}(&self, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}{{^isUuid}}&str{{/isUuid}}{{/isString}}{{#isUuid}}&str{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) -> Pin<Box<dyn Future<Output = Result<{{^returnType}}(){{/returnType}}{{#returnType}}{{{.}}}{{/returnType}}, Error>>>> {
let mut req = __internal_request::Request::new(hyper::Method::{{{httpMethod.toUpperCase}}}, "{{{path}}}".to_string())
{{#hasAuthMethods}}
{{#authMethods}}
Expand Down
6 changes: 3 additions & 3 deletions support/openapi-template/hyper/api_mod.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,19 @@ impl From<(hyper::StatusCode, hyper::body::Body)> for Error {

impl From<http::Error> for Error {
fn from(e: http::Error) -> Self {
return Error::Http(e)
Error::Http(e)
}
}

impl From<hyper::Error> for Error {
fn from(e: hyper::Error) -> Self {
return Error::Hyper(e)
Error::Hyper(e)
}
}

impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
return Error::Serde(e)
Error::Serde(e)
}
}

Expand Down
13 changes: 6 additions & 7 deletions support/openapi-template/lib.mustache
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
#![allow(warnings)]
#![allow(clippy::all)]

#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_repr;
#![allow(unused_imports, unused_variables, unused_mut, non_camel_case_types)]
#![allow(
clippy::too_many_arguments,
clippy::empty_docs,
clippy::to_string_in_format_args
)]

extern crate serde;
extern crate serde_json;
Expand Down
128 changes: 77 additions & 51 deletions support/openapi-template/model.mustache
Original file line number Diff line number Diff line change
@@ -1,63 +1,44 @@
{{>partial_header}}
use crate::models;
use serde::{Deserialize, Serialize};
{{#models}}
{{#model}}
{{^isEnum}}{{#vendorExtensions.x-rust-has-byte-array}}
use serde_with::serde_as;
{{/vendorExtensions.x-rust-has-byte-array}}{{/isEnum}}
{{#description}}
/// {{{classname}}} : {{{description}}}
{{/description}}

{{!-- for repr(int) enum schemas --}}
{{!-- for enum schemas --}}
{{#isEnum}}
{{#isInteger}}
/// {{{description}}}
#[repr(i64)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize_repr, Deserialize_repr)]
{{^isInteger}}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum {{{classname}}} {
{{#allowableValues}}
{{#enumVars}}
{{{name}}} = {{{value}}},
#[serde(rename = "{{{value}}}")]
{{{name}}},
{{/enumVars}}{{/allowableValues}}
}

impl ToString for {{{classname}}} {
fn to_string(&self) -> String {
match self {
{{#allowableValues}}
{{#enumVars}}
Self::{{{name}}} => String::from("{{{value}}}"),
{{/enumVars}}
{{/allowableValues}}
}
}
}

impl Default for {{{classname}}} {
fn default() -> {{{classname}}} {
{{#allowableValues}}
Self::{{ enumVars.0.name }}
{{/allowableValues}}
}
}
{{/isInteger}}
{{/isEnum}}
{{!-- for enum schemas --}}
{{#isEnum}}
{{^isInteger}}
/// {{{description}}}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
{{#isInteger}}
#[repr(i64)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
pub enum {{{classname}}} {
{{#allowableValues}}
{{#enumVars}}
#[serde(rename = "{{{value}}}")]
{{{name}}},
{{{name}}} = {{{value}}},
{{/enumVars}}{{/allowableValues}}
}
{{/isInteger}}

impl ToString for {{{classname}}} {
fn to_string(&self) -> String {
impl std::fmt::Display for {{{classname}}} {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
{{#allowableValues}}
{{#enumVars}}
Self::{{{name}}} => String::from("{{{value}}}"),
Self::{{{name}}} => write!(f, "{{{value}}}"),
{{/enumVars}}
{{/allowableValues}}
}
Expand All @@ -71,61 +52,106 @@ impl Default for {{{classname}}} {
{{/allowableValues}}
}
}
{{/isInteger}}
{{/isEnum}}

{{!-- for schemas that have a discriminator --}}
{{#discriminator}}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "{{{vendorExtensions.x-tag-name}}}")]
#[serde(tag = "{{{propertyBaseName}}}")]
pub enum {{{classname}}} {
{{#vendorExtensions}}
{{#x-mapped-models}}
{{^oneOf}}
{{#mappedModels}}
#[serde(rename="{{mappingName}}")]
{{{modelName}}} {
{{#vars}}
{{#description}}
/// {{{.}}}
{{/description}}
#[serde(rename = "{{{baseName}}}"{{^required}}, skip_serializing_if = "Option::is_none"{{/required}})]
{{{name}}}: {{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{^required}}Option<{{/required}}{{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{#isModel}}Box<{{{dataType}}}>{{/isModel}}{{^isModel}}{{{dataType}}}{{/isModel}}{{/isEnum}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^required}}>{{/required}},
{{{name}}}: {{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{^required}}Option<{{/required}}{{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{#isModel}}{{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}{{{dataType}}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}{{/isModel}}{{^isModel}}{{{dataType}}}{{/isModel}}{{/isEnum}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^required}}>{{/required}},
{{/vars}}
},
{{/x-mapped-models}}
{{/vendorExtensions}}
{{/mappedModels}}
{{/oneOf}}
{{^oneOf.isEmpty}}
{{#composedSchemas.oneOf}}
{{#description}}
/// {{{.}}}
{{/description}}
{{#baseName}}
#[serde(rename="{{{.}}}")]
{{/baseName}}
{{{name}}}({{#isModel}}{{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}{{/isModel}}{{{dataType}}}{{#isModel}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}{{/isModel}}),
{{/composedSchemas.oneOf}}
{{/oneOf.isEmpty}}
}

{{/discriminator}}
impl Default for {{classname}} {
fn default() -> Self {
{{^oneOf}}{{#mappedModels}}{{#-first}}Self::{{modelName}} {
{{#vars}}
{{{name}}}: Default::default(),
{{/vars}}
}{{/-first}}{{/mappedModels}}
{{/oneOf}}{{^oneOf.isEmpty}}{{#composedSchemas.oneOf}}{{#-first}}Self::{{{name}}}(Default::default()){{/-first}}{{/composedSchemas.oneOf}}{{/oneOf.isEmpty}}
}
}

{{/discriminator}}
{{!-- for non-enum schemas --}}
{{^isEnum}}
{{^discriminator}}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
{{#vendorExtensions.x-rust-has-byte-array}}#[serde_as]
{{/vendorExtensions.x-rust-has-byte-array}}{{#oneOf.isEmpty}}#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct {{{classname}}} {
{{#vars}}
{{#description}}
/// {{{.}}}
{{/description}}
{{#isByteArray}}
{{#required}}#[serde_as(as = "serde_with::base64::Base64")]{{/required}}{{^required}}#[serde_as(as = "Option<serde_with::base64::Base64>")]{{/required}}
{{/isByteArray}}
#[serde(rename = "{{{baseName}}}"{{^required}}, skip_serializing_if = "Option::is_none"{{/required}})]
pub {{{name}}}: {{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{^required}}Option<{{/required}}{{#isEnum}}{{#isArray}}{{#uniqueItems}}std::collections::HashSet<{{/uniqueItems}}{{^uniqueItems}}Vec<{{/uniqueItems}}{{/isArray}}{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{#isModel}}Box<{{{dataType}}}>{{/isModel}}{{^isModel}}{{{dataType}}}{{/isModel}}{{/isEnum}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^required}}>{{/required}},
pub {{{name}}}: {{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{^required}}Option<{{/required}}{{#isEnum}}{{#isArray}}{{#uniqueItems}}std::collections::HashSet<{{/uniqueItems}}{{^uniqueItems}}Vec<{{/uniqueItems}}{{/isArray}}{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{#isModel}}{{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}{{{dataType}}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}{{/isModel}}{{^isModel}}{{#isByteArray}}Vec<u8>{{/isByteArray}}{{^isByteArray}}{{{dataType}}}{{/isByteArray}}{{/isModel}}{{/isEnum}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^required}}>{{/required}},
{{/vars}}
}

impl {{{classname}}} {
{{#description}}
/// {{{.}}}
{{/description}}
pub fn new({{#requiredVars}}{{{name}}}: {{#isNullable}}Option<{{/isNullable}}{{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}>{{/isNullable}}{{^-last}}, {{/-last}}{{/requiredVars}}) -> {{{classname}}} {
pub fn new({{#requiredVars}}{{{name}}}: {{#isNullable}}Option<{{/isNullable}}{{#isEnum}}{{#isArray}}{{#uniqueItems}}std::collections::HashSet<{{/uniqueItems}}{{^uniqueItems}}Vec<{{/uniqueItems}}{{/isArray}}{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{#isByteArray}}Vec<u8>{{/isByteArray}}{{^isByteArray}}{{{dataType}}}{{/isByteArray}}{{/isEnum}}{{#isNullable}}>{{/isNullable}}{{^-last}}, {{/-last}}{{/requiredVars}}) -> {{{classname}}} {
{{{classname}}} {
{{#vars}}
{{{name}}}{{^required}}{{#isArray}}: None{{/isArray}}{{#isMap}}: None{{/isMap}}{{^isContainer}}: None{{/isContainer}}{{/required}}{{#required}}{{#isModel}}: Box::new({{{name}}}){{/isModel}}{{/required}},
{{{name}}}{{^required}}: None{{/required}}{{#required}}{{#isModel}}{{^avoidBoxedModels}}: {{^isNullable}}Box::new({{{name}}}){{/isNullable}}{{#isNullable}}if let Some(x) = {{{name}}} {Some(Box::new(x))} else {None}{{/isNullable}}{{/avoidBoxedModels}}{{/isModel}}{{/required}},
{{/vars}}
}
}
}
{{/oneOf.isEmpty}}
{{^oneOf.isEmpty}}
{{! TODO: add other vars that are not part of the oneOf}}
{{#description}}
/// {{{.}}}
{{/description}}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum {{classname}} {
{{#composedSchemas.oneOf}}
{{#description}}
/// {{{.}}}
{{/description}}
{{{name}}}({{#isModel}}{{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}{{/isModel}}{{{dataType}}}{{#isModel}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}{{/isModel}}),
{{/composedSchemas.oneOf}}
}

impl Default for {{classname}} {
fn default() -> Self {
{{#composedSchemas.oneOf}}{{#-first}}Self::{{{name}}}(Default::default()){{/-first}}{{/composedSchemas.oneOf}}
}
}
{{/oneOf.isEmpty}}
{{/discriminator}}
{{/isEnum}}

{{!-- for properties that are of enum type --}}
{{#vars}}
{{#isEnum}}
Expand Down
Loading

0 comments on commit 5d8536b

Please sign in to comment.