[HTTP/OAS] Sort OpenAPI tags by x-displayName when it is set (#191732)

**Relates to:** https://github.com/elastic/kibana/issues/186356

## Summary

This PR implements proper tags sorting when a custom tag's `x-displayName` property is set. It allows to display tags properly at the API reference documentation page where `x-displayName` instead of tag's name when it's presented.
This commit is contained in:
Maxim Palenov 2024-09-05 18:48:01 +03:00 committed by GitHub
parent fd084d97f7
commit 3c4e50249d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 216 additions and 3 deletions

View file

@ -725,6 +725,65 @@ paths:
type: object
```
### Root level tags with `x-displayName`
OpenAPI documents may have root level tags referenced by name in operations. Some platforms including Bump.sh used for API reference documentation support `x-displayName`. Value specified in that custom property used instead of `tag.name` to display a name.
OpenAPI bundler supports `x-displayName` as well.
#### Examples
To specify a custom tag with `x-displayName` to assign that tag to all operations in the document the following configuration should be specified
```bash
const { bundle } = require('@kbn/openapi-bundler');
const { join, resolve } = require('path');
const ROOT = resolve(__dirname, '../..');
(async () => {
await bundle({
// ...
options: {
prototypeDocument: {
tags: [
{
name: 'My tag name',
description: 'My tag description',
x-displayName: 'My Custom Name',
},
],
},
},
});
})();
```
It will produce a document containing the specified tag assigned to all operations like below
```yaml
openapi: 3.0.3
info: ...
servers: ...
paths:
/api/some/operation:
delete:
operationId: SomeOperation
...
tags:
- My tag name
- Tag existing before bundling
components:
schemas: ...
security: ...
tags:
- description: My tag description
name: My tag name
x-displayName: My Custom Name
```
When merging OpenAPI specs together tags will be sorted by `x-displayName` or `name` in ascending order depending on whether `x-displayName` is specified.
## Contribution
In case you decide to contribute to the `kbn-openapi-bundler` package please make sure to add and/or update existing e2e test in `kbn-openapi-bundler/tests` folder.

View file

@ -25,7 +25,13 @@ export function mergeTags(
// To streamline API endpoints categorization it's expected that
// tags are sorted alphabetically by name
merged.sort((a, b) => a.name.localeCompare(b.name));
merged.sort((a, b) => getTagName(a).localeCompare(getTagName(b)));
return merged;
}
function getTagName(tag: OpenAPIV3.TagObject): string {
return 'x-displayName' in tag && typeof tag['x-displayName'] === 'string'
? tag['x-displayName']
: tag.name;
}

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import 'openapi-types';
// Override the OpenAPI types to add the x-displayName property to the
// tag object.
declare module 'openapi-types' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace OpenAPIV3 {
interface TagObject {
'x-displayName'?: string;
}
}
}

View file

@ -179,4 +179,77 @@ describe('OpenAPI Bundler - assign a tag', () => {
{ name: 'Global tag', description: 'Global tag description' },
]);
});
it('supports x-displayName', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
});
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
});
const [bundledSpec] = Object.values(
await bundleSpecs(
{
1: spec1,
2: spec2,
},
{
prototypeDocument: {
tags: [
{
name: 'Some Tag',
description: 'Some tag description',
'x-displayName': 'Custom name',
},
],
},
}
)
);
expect(bundledSpec.paths['/api/some_api']?.get?.tags).toEqual(['Some Tag']);
expect(bundledSpec.paths['/api/another_api']?.get?.tags).toEqual(['Some Tag']);
expect(bundledSpec.tags).toEqual([
{
name: 'Some Tag',
description: 'Some tag description',
'x-displayName': 'Custom name',
},
]);
});
});

View file

@ -9,8 +9,8 @@
import { mergeSpecs } from '../merge_specs';
import { createOASDocument } from '../../create_oas_document';
describe('OpenAPI Merger - sort tags', () => {
it('sorts tags in the result bundle', async () => {
describe('OpenAPI Merger - sort tags in the result bundle', () => {
it('sorts tags by name', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
@ -60,4 +60,59 @@ describe('OpenAPI Merger - sort tags', () => {
{ name: 'Spec3 tag name', description: 'Spec3 tag description' },
]);
});
it('sorts tags by x-displayName or name', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {},
},
},
},
tags: [
{ name: 'Some tag name', description: 'Some description' },
{ name: '1 tag', description: 'Some description' },
],
});
const spec2 = createOASDocument({
paths: {
'/api/some_api': {
post: {
responses: {},
},
},
},
tags: [
{ name: 'Another tag name', description: 'Another description', 'x-displayName': 'Y tag' },
],
});
const spec3 = createOASDocument({
paths: {
'/api/some_api': {
put: {
responses: {},
},
},
},
tags: [
{ name: 'Spec3 tag name', description: 'Spec3 tag description', 'x-displayName': 'X tag' },
],
});
const [mergedSpec] = Object.values(
await mergeSpecs({
1: spec1,
2: spec2,
3: spec3,
})
);
expect(mergedSpec.tags).toEqual([
{ name: '1 tag', description: 'Some description' },
{ name: 'Some tag name', description: 'Some description' },
{ name: 'Spec3 tag name', description: 'Spec3 tag description', 'x-displayName': 'X tag' },
{ name: 'Another tag name', description: 'Another description', 'x-displayName': 'Y tag' },
]);
});
});