Skip to content

Commit

Permalink
feat(open-api-gateway): support for multiple tags on operations
Browse files Browse the repository at this point in the history
This change adds support for tagging operations with multiple tags. This is achieved by using a new
feature of openapi-generator which will only consider the "first" (if any) tag for an operation
during code generation, ensuring that no duplicated code is generated. Note that the "first" tag for
OpenAPI-based projects is the one defined first in the list of tags associated with an operation.
Unfortunately Smithy 1.27.2 and below will sort tags alphabetically in the generated OpenAPI spec,
so the "first" tag is the lowest in the alphabet. This will be addressed in the next Smithy release.

fix #269
  • Loading branch information
cogwirrel committed Feb 15, 2023
1 parent 8f773c1 commit 9a59105
Show file tree
Hide file tree
Showing 37 changed files with 55,487 additions and 18,174 deletions.
34 changes: 33 additions & 1 deletion packages/open-api-gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1311,9 +1311,41 @@ The Smithy-based projects are compatible with the [Smithy IntelliJ Plugin](https
* Right-click on the `smithy/build.gradle` file in your Smithy API project
* Select "Link Gradle Project"

### Tagging Operations

Operations can be grouped together into logical collections via tags. This can be achieved in Smithy with the `@tags` trait:

```smithy
@tags(["pets", "users"])
operation PurchasePet {
...
}
```

Or in OpenAPI using the `tags` property:

```yaml
paths:
/pets/purchase:
post:
operationId: purchasePet
tags:
- pets
- users
...
```

When multiple tags are used, the "first" tag is considered to be the API that the operation belongs to, so in the generated client, the above example operation would be included in the `PetsApi` client but not the `UsersApi` client.

Multiple tags are still useful for documentation generation, for example `DocumentationFormat.HTML_REDOC` will group operations by tag in the side navigation bar.

If you would like to introduce tags without breaking existing clients, we recommend first adding a tag named `default` to all operations.

⚠️ __Important Note__: Smithy version 1.27.2 and below sorts tags in alphabetical order and so the "first" tag will be the earliest in the alphabet. Therefore, if using tags in Smithy, we currently recommend prefixing your desired first tag with an underscore (`_`). This will be rectified in the next Smithy release, where tag order from the `@tags` trait will be preserved.

### Breaking Changes

* `v0.14.0` - see https://github.com/aws/aws-prototyping-sdk/pull/280
* Moved smithy model files from `model` directory to `smithy/src/main/smithy` - please move these manually as part of upgrading to `0.14.0`, and delete your `model` directory when done.
* Moved smithy gradle files from `smithy-build` directory to `smithy` - if you have added any dependencies to your `smithy-build/build.gradle` file you will need to copy them across into `smithy/build.gradle` (note dependencies in the new gradle file start with `implementation` rather than `smithy`).
* Deprecated `gradleWrapperPath` option on SmithApiGateway projects in favour of `ignoreGradleWrapper: false` - the gradle wrapper in `smithy` directory is always used (and generated automatically if not found). If you used a custom gradle wrapper, copy it into the `smithy` directory, set `ignoreGradleWrapper: false` and check it in to your repository.
* Deprecated `gradleWrapperPath` option on SmithApiGateway projects in favour of `ignoreGradleWrapper: false` - the gradle wrapper in `smithy` directory is always used (and generated automatically if not found). If you used a custom gradle wrapper, copy it into the `smithy` directory, set `ignoreGradleWrapper: false` and check it in to your repository.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "6.0.0",
"version": "6.3.0",
"storageDir": "~/.open-api-generator-cli"
}
}
3 changes: 3 additions & 0 deletions packages/open-api-gateway/scripts/generators/generate
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ output_path=''
generator=''
generator_dir=''
additional_properties=''
openapi_normalizer=''
src_dir='src'
while [[ "$#" -gt 0 ]]; do case $1 in
--spec-path) spec_path="$2"; shift;;
--output-path) output_path="$2"; shift;;
--generator) generator="$2"; shift;;
--generator-dir) generator_dir="$2"; shift;;
--additional-properties) additional_properties="$2"; shift;;
--openapi-normalizer) openapi_normalizer="$2"; shift;;
--src-dir) src_dir="$2"; shift;;
esac; shift; done

Expand Down Expand Up @@ -52,6 +54,7 @@ run_command @openapitools/openapi-generator-cli generate \
--template-dir templates \
--config config.final.yaml \
--additional-properties="$additional_properties" \
${openapi_normalizer:+"--openapi-normalizer=$openapi_normalizer"} \
--input-spec $spec_path \
--output $output_path

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "6.0.1",
"version": "6.3.0",
"storageDir": "~/.open-api-generator-cli"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "6.1.0",
"version": "6.3.0",
"storageDir": "~/.open-api-generator-cli"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "6.0.0",
"version": "6.3.0",
"storageDir": "~/.open-api-generator-cli"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export class GeneratedJavaClientSourceCode extends Component {
].join("\\ "),
},
srcDir: path.join("src", "main", "java", ...invokerPackage.split(".")),
normalizers: {
KEEP_ONLY_FIRST_TAG_IN_OPERATION: true,
},
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class GeneratedPythonClientSourceCode extends Component {
// Generate the python client
logger.debug("Generating python client...");
invokeOpenApiGenerator({
generator: "python-experimental",
generator: "python",
specPath: this.options.specPath,
outputPath: this.project.outdir,
generatorDirectory: ClientLanguage.PYTHON,
Expand All @@ -57,6 +57,9 @@ export class GeneratedPythonClientSourceCode extends Component {
},
// Tell the generator where python source files live
srcDir: (this.project as PythonProject).moduleName,
normalizers: {
KEEP_ONLY_FIRST_TAG_IN_OPERATION: true,
},
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export class GeneratedTypescriptClientSourceCode extends Component {
supportsES6: "true",
},
srcDir: (this.project as TypeScriptProject).srcdir,
normalizers: {
KEEP_ONLY_FIRST_TAG_IN_OPERATION: true,
},
});

// Write an index.ts which exposes the additional generated file OperationConfig.ts, which contains handler wrappers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export enum NonClientGeneratorDirectory {
*/
export type GeneratorDirectory = ClientLanguage | NonClientGeneratorDirectory;

/**
* Types of normalizers supported by openapi-generator
* @see https://openapi-generator.tech/docs/customization/#openapi-normalizer
*/
export type OpenApiNormalizer = "KEEP_ONLY_FIRST_TAG_IN_OPERATION";

/**
* Options for generating client code or docs using OpenAPI Generator CLI
*/
Expand Down Expand Up @@ -48,6 +54,11 @@ export interface GenerationOptions {
* (eg. operation config) should be placed.
*/
readonly srcDir?: string;
/**
* Normalizers to apply to the spec prior to generation, if any
* @see https://openapi-generator.tech/docs/customization/#openapi-normalizer
*/
readonly normalizers?: Partial<Record<OpenApiNormalizer, boolean>>;
}

const serializeProperties = (properties: { [key: string]: string }) =>
Expand Down Expand Up @@ -95,8 +106,17 @@ export const invokeOpenApiGenerator = (options: GenerationOptions) => {
options.additionalProperties
)}"`
: "";

const normalizers = options.normalizers
? ` --openapi-normalizer "${serializeProperties(
Object.fromEntries(
Object.entries(options.normalizers).map(([k, v]) => [k, `${v}`])
)
)}"`
: "";

exec(
`./generate --generator ${options.generator} --spec-path ${options.specPath} --output-path ${options.outputPath} --generator-dir ${options.generatorDirectory} --src-dir ${srcDir}${additionalProperties}`,
`./generate --generator ${options.generator} --spec-path ${options.specPath} --output-path ${options.outputPath} --generator-dir ${options.generatorDirectory} --src-dir ${srcDir}${additionalProperties}${normalizers}`,
{
cwd: path.resolve(
__dirname,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ export interface GeneratedJavaClientProjectOptions extends JavaProjectOptions {

const DEPENDENCIES: string[] = [
// Required for open api generated client
"io.swagger/swagger-annotations@1.6.5",
"io.swagger/swagger-annotations@1.6.8",
"com.google.code.findbugs/jsr305@3.0.2",
"com.squareup.okhttp3/okhttp@4.9.3",
"com.squareup.okhttp3/logging-interceptor@4.9.3",
"com.google.code.gson/gson@2.9.0",
"com.squareup.okhttp3/okhttp@4.10.0",
"com.squareup.okhttp3/logging-interceptor@4.10.0",
"com.google.code.gson/gson@2.9.1",
"io.gsonfire/gson-fire@1.8.5",
"org.apache.commons/commons-lang3@3.12.0",
"jakarta.annotation/jakarta.annotation-api@1.3.5",
"org.openapitools/jackson-databind-nullable@0.2.2",
"org.openapitools/jackson-databind-nullable@0.2.4",
"javax.ws.rs/jsr311-api@1.1.1",
"javax.ws.rs/javax.ws.rs-api@2.1.1",
// For handler wrappers
Expand All @@ -41,7 +41,7 @@ const DEPENDENCIES: string[] = [
];

const TEST_DEPENDENCIES: string[] = [
"org.junit.jupiter/junit-jupiter-api@5.8.2",
"org.junit.jupiter/junit-jupiter-api@5.9.1",
"org.mockito/mockito-core@3.12.4",
];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
/*! Copyright [Amazon.com](http://amazon.com/), Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0 */
import * as path from "path";
import { getLogger } from "log4js";
import type { OpenAPIV3 } from "openapi-types";
import { Component, Project } from "projen";
import { exec, tryReadFileSync } from "projen/lib/util";

const logger = getLogger();

/**
* Configuration for the ParsedSpec component
*/
Expand Down Expand Up @@ -62,9 +65,10 @@ export class ParsedSpec extends Component {

const parsedSpec: OpenAPIV3.Document = JSON.parse(singleSpecFile);

// TODO: Remove this validation and update mustache templates as appropriate when the following has been addressed:
// https://github.com/OpenAPITools/openapi-generator/pull/14568
// Check that each operation has zero or one tags
// To avoid duplicating custom generated code (eg. OperationConfig or handler wrappers) and causing build errors, we
// will apply the OpenAPI Normalizer to KEEP_ONLY_FIRST_TAG_IN_OPERATION when generating code. Tags are still
// preserved in the specification to allow for better documentation.
// See: https://github.com/OpenAPITools/openapi-generator/pull/14465
const operationsWithMultipleTags = Object.entries(parsedSpec.paths).flatMap(
([urlPath, methods]) =>
Object.entries(methods ?? {})
Expand All @@ -79,10 +83,10 @@ export class ParsedSpec extends Component {
);

if (operationsWithMultipleTags.length > 0) {
throw new Error(
`Operations with multiple tags are not yet supported, please tag operations with at most one tag. The following operations have multiple tags: ${operationsWithMultipleTags.join(
logger.warn(
`The following operations had multiple tags: ${operationsWithMultipleTags.join(
", "
)}`
)}. Code will only be generated for each operation's first tag.`
);
}
}
Expand Down
Loading

0 comments on commit 9a59105

Please sign in to comment.