Skip to content

Commit

Permalink
feat(openapi): tooling upgrades (#1126)
Browse files Browse the repository at this point in the history
## 🧰 Changes

This upgrades our [oas](https://npm.im/oas) and
[oas-normalize](https://npm.im/oas-normalize) tooling to their latest
releases. Included in this work are a couple new features to and
refactors to some commands:

#### `openapi`

The `.validate()` call within `oas-normalize` has been refactored to no
longer do conversion and validation, the conversion side has been split
off to a new `.convert()` method.

As part of this change the previous `convertToLatest` parameter that
`.validate()` accepted for converting Swagger definitions to OpenAPI no
longer exists anywhere. We had this configurable within some internal
methods within rdme but it was never exposed, and converting Swagger to
OpenAPI has always been the default behavior throughout rdme.

#### `openapi inspect`

Our analyzer tool can now surface information on if an API definition
utilizes common parameters.[^1]

I have also removed our dependency on
[pluralize](https://npm.im/pluralize) because we were only loading it to
pluralize three strings: "media type", "operation", and "security type".
Because these can be easily pluralized with a one-liner we don't really
need to import a wholeass library to do this.

[^1]:
https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#path-item-object
  • Loading branch information
erunion authored Jan 6, 2025
1 parent 57fd346 commit 6678d68
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 35 deletions.
141 changes: 141 additions & 0 deletions __tests__/commands/openapi/__snapshots__/inspect.test.ts.snap

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions __tests__/commands/openapi/inspect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('rdme openapi inspect', () => {
'@readme/oas-examples/3.0/json/petstore.json',
'@readme/oas-examples/3.0/json/readme.json',
'@readme/oas-examples/3.0/json/readme-extensions.json',
'@readme/oas-examples/3.1/json/train-travel.json',
])('should generate a report for %s', spec => {
return expect(run([require.resolve(spec)])).resolves.toMatchSnapshot();
});
Expand All @@ -42,6 +43,10 @@ describe('rdme openapi inspect', () => {
spec: '@readme/oas-examples/3.0/json/schema-circular.json',
feature: ['additionalProperties', 'circularRefs'],
},
{
spec: '@readme/oas-examples/3.1/json/train-travel.json',
feature: ['commonParameters'],
},

// Soft error cases where we may or may not contain the features we're querying for.
{
Expand Down
15 changes: 15 additions & 0 deletions __tests__/lib/__snapshots__/analyzeOas.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ exports[`#analyzeOas > should analyze an OpenAPI definition 1`] = `
"locations": [],
"present": false,
},
"commonParameters": {
"description": "Common parameters allow you to define parameters that are shared across multiple operations within your API.",
"locations": [],
"present": false,
"url": {
"3.0": "https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.4.md#path-item-object",
"3.1": "https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#path-item-object",
},
},
"discriminators": {
"description": "With schemas that can be, or contain, different shapes, discriminators help you assist your users in identifying and determining the kind of shape they can supply or receive.",
"locations": [],
Expand Down Expand Up @@ -135,6 +144,12 @@ exports[`#analyzeOas > should analyze an OpenAPI definition 1`] = `
"present": false,
"url": "https://docs.readme.com/main/docs/openapi-extensions#authentication-defaults",
},
"x-readme-ref-name": {
"description": "x-readme-ref-name is added by our tooling after dereferencing in order to preserve original reference schema names.",
"hidden": true,
"locations": [],
"present": false,
},
"x-readme.code-samples": {
"description": "The x-readme.code-samples extension allows you to custom, create static code samples on your API documentation.",
"locations": [],
Expand Down
1 change: 1 addition & 0 deletions __tests__/lib/analyzeOas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('#getSupportedFeatures', () => {
'additionalProperties',
'callbacks',
'circularRefs',
'commonParameters',
'discriminators',
'links',
'style',
Expand Down
27 changes: 9 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,9 @@
"gray-matter": "^4.0.1",
"ignore": "^6.0.2",
"mime-types": "^2.1.35",
"oas": "^25.0.0",
"oas-normalize": "^11.1.2",
"oas": "^25.2.1",
"oas-normalize": "^12.0.0",
"ora": "^8.1.1",
"pluralize": "^8.0.0",
"prompts": "^2.4.2",
"semver": "^7.5.3",
"simple-git": "^3.19.1",
Expand Down Expand Up @@ -85,7 +84,6 @@
"@types/debug": "^4.1.7",
"@types/js-yaml": "^4.0.5",
"@types/mime-types": "^2.1.1",
"@types/pluralize": "^0.0.33",
"@types/prompts": "^2.4.2",
"@types/semver": "^7.3.12",
"@types/toposort": "^2.0.7",
Expand Down
1 change: 0 additions & 1 deletion src/commands/openapi/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export default class OpenAPIConvertCommand extends BaseCommand<typeof OpenAPICon
}

const { preparedSpec, specPath, specType } = await prepareOas(spec, 'openapi convert', {
convertToLatest: true,
title,
});
const parsedPreparedSpec: OASDocument = JSON.parse(preparedSpec);
Expand Down
15 changes: 13 additions & 2 deletions src/commands/openapi/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { OASDocument } from 'oas/types';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import ora from 'ora';
import pluralize from 'pluralize';
import { getBorderCharacters, table } from 'table';

import analyzeOas, { getSupportedFeatures } from '../../lib/analyzeOas.js';
Expand All @@ -14,6 +13,10 @@ import { oraOptions } from '../../lib/logger.js';
import prepareOas from '../../lib/prepareOas.js';
import SoftError from '../../lib/softError.js';

function pluralize(str: string, count: number) {
return count > 1 ? `${str}s` : str;
}

function getFeatureDocsURL(feature: AnalyzedFeature, definitionVersion: string): string | undefined {
if (!feature.url) {
return undefined;
Expand Down Expand Up @@ -68,6 +71,10 @@ function buildFeaturesReport(analysis: Analysis, features: string[]) {
}

Object.entries(analysis.readme).forEach(([feature, info]) => {
if (info.hidden) {
return;
}

if (!info.present) {
report.push(`${feature}: You do not use this.`);
hasUnusedFeature = true;
Expand Down Expand Up @@ -140,6 +147,10 @@ function buildFullReport(analysis: Analysis, definitionVersion: string, tableBor
];

Object.entries(analysis[component as 'openapi' | 'readme']).forEach(([feature, info]) => {
if (info.hidden) {
return;
}

const descriptions: string[] = [];
if (info.description) {
descriptions.push(info.description);
Expand Down Expand Up @@ -224,7 +235,7 @@ export default class OpenAPIInspectCommand extends BaseCommand<typeof OpenAPIIns
this.debug(`switching working directory from ${previousWorkingDirectory} to ${process.cwd()}`);
}

const { preparedSpec, definitionVersion } = await prepareOas(spec, 'openapi inspect', { convertToLatest: true });
const { preparedSpec, definitionVersion } = await prepareOas(spec, 'openapi inspect');
const parsedPreparedSpec: OASDocument = JSON.parse(preparedSpec);

const spinner = ora({ ...oraOptions() });
Expand Down
24 changes: 23 additions & 1 deletion src/lib/analyzeOas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import analyzer from 'oas/analyzer';

export interface AnalyzedFeature extends OASAnalysisFeature {
description: string;

/**
* The analyzed feature is not worth reporting within the inspector.
*/
hidden?: boolean;

url?:
| string
| {
Expand All @@ -21,6 +27,7 @@ export interface Analysis extends OASAnalysis {
additionalProperties: AnalyzedFeature;
callbacks: AnalyzedFeature;
circularRefs: AnalyzedFeature;
commonParameters: AnalyzedFeature;
discriminators: AnalyzedFeature;
links: AnalyzedFeature;
polymorphism: AnalyzedFeature;
Expand All @@ -37,6 +44,8 @@ export interface Analysis extends OASAnalysis {
raw_body?: AnalyzedFeature;

'x-default': AnalyzedFeature;
'x-readme-ref-name': AnalyzedFeature;

'x-readme.code-samples': AnalyzedFeature;
'x-readme.explorer-enabled': AnalyzedFeature;
'x-readme.headers': AnalyzedFeature;
Expand Down Expand Up @@ -69,6 +78,14 @@ const OPENAPI_FEATURE_DOCS: Record<keyof Analysis['openapi'], Pick<AnalyzedFeatu
circularRefs: {
description: 'Circular references are $ref pointers that at some point in their lineage reference themselves.',
},
commonParameters: {
description:
'Common parameters allow you to define parameters that are shared across multiple operations within your API.',
url: {
'3.0': 'https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.4.md#path-item-object',
'3.1': 'https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#path-item-object',
},
},
discriminators: {
description:
'With schemas that can be, or contain, different shapes, discriminators help you assist your users in identifying and determining the kind of shape they can supply or receive.',
Expand Down Expand Up @@ -121,12 +138,17 @@ const OPENAPI_FEATURE_DOCS: Record<keyof Analysis['openapi'], Pick<AnalyzedFeatu
},
};

const README_FEATURE_DOCS: Record<keyof Analysis['readme'], Pick<AnalyzedFeature, 'description' | 'url'>> = {
const README_FEATURE_DOCS: Record<keyof Analysis['readme'], Pick<AnalyzedFeature, 'description' | 'hidden' | 'url'>> = {
'x-default': {
description:
'The x-default extension allows you to define static authentication credential defaults for OAuth 2 and API Key security types.',
url: 'https://docs.readme.com/main/docs/openapi-extensions#authentication-defaults',
},
'x-readme-ref-name': {
description:
'x-readme-ref-name is added by our tooling after dereferencing in order to preserve original reference schema names.',
hidden: true,
},
'x-readme.code-samples': {
description:
'The x-readme.code-samples extension allows you to custom, create static code samples on your API documentation.',
Expand Down
21 changes: 12 additions & 9 deletions src/lib/prepareOas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,12 @@ export default async function prepareOas(
path: string | undefined,
command: 'openapi convert' | 'openapi inspect' | 'openapi reduce' | 'openapi validate' | 'openapi',
opts: {
/**
* Optionally convert the supplied or discovered API definition to the latest OpenAPI release.
*/
convertToLatest?: boolean;
/**
* An optional title to replace the value in the `info.title` field.
* @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#info-object}
*/
title?: string;
} = {
convertToLatest: false,
},
} = {},
) {
let specPath = path;

Expand Down Expand Up @@ -189,12 +183,21 @@ export default async function prepareOas(
throw err;
});

// If we were supplied a Postman collection this will **always** convert it to OpenAPI 3.0.
let api: OpenAPI.Document = await oas.validate({ convertToLatest: opts.convertToLatest }).catch((err: Error) => {
let api: OpenAPI.Document;
await oas.validate().catch((err: Error) => {
spinner.fail();
debug(`raw validation error object: ${JSON.stringify(err)}`);
throw err;
});

// If we were supplied a Postman collection this will **always** convert it to OpenAPI 3.0.
debug('converting the spec to OpenAPI 3.0 (if necessary)');
api = await oas.convert().catch((err: Error) => {
spinner.fail();
debug(`raw openapi conversion error object: ${JSON.stringify(err)}`);
throw err;
});

spinner.stop();

debug('👇👇👇👇👇 spec validated! logging spec below 👇👇👇👇👇');
Expand Down

0 comments on commit 6678d68

Please sign in to comment.