Skip to content

Commit

Permalink
fix(typings): update OpenAPI 3.0 and 3.1 typing declarations (#1795)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremyfiel authored Jan 20, 2025
1 parent 085aaf2 commit ef4b2e2
Show file tree
Hide file tree
Showing 16 changed files with 315 additions and 156 deletions.
5 changes: 5 additions & 0 deletions .changeset/selfish-pandas-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@redocly/openapi-core": patch
---

Updated typings for OAS 3.0 and OAS 3.1 Schemas.
18 changes: 12 additions & 6 deletions packages/cli/src/commands/join.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ import { isObject, isString, keysOf } from '../utils/js-utils';
import { COMPONENTS, OPENAPI3_METHOD } from './split/types';
import { crawl, startsWithComponents } from './split';

import type { Oas3Definition, Document, Oas3Tag, Referenced } from '@redocly/openapi-core';
import type { Document, Referenced } from '@redocly/openapi-core';
import type { BundleResult } from '@redocly/openapi-core/lib/bundle';
import type {
Oas3Definition,
Oas3_1Definition,
Oas3Parameter,
Oas3PathItem,
Oas3Server,
Oas3_1Definition,
Oas3Tag,
} from '@redocly/openapi-core/lib/typings/openapi';
import type { StrictObject } from '@redocly/openapi-core/lib/utils';
import type { CommandArgs } from '../wrapper';
import type { VerifyConfigOptions } from '../types';

Expand Down Expand Up @@ -311,7 +314,7 @@ export async function handleJoin({
}
}

function collectServers(openapi: Oas3Definition) {
function collectServers(openapi: Oas3Definition | Oas3_1Definition) {
const { servers } = openapi;
if (servers) {
if (!joinedDef.hasOwnProperty('servers')) {
Expand All @@ -325,7 +328,10 @@ export async function handleJoin({
}
}

function collectExternalDocs(openapi: Oas3Definition, { api }: JoinDocumentContext) {
function collectExternalDocs(
openapi: Oas3Definition | Oas3_1Definition,
{ api }: JoinDocumentContext
) {
const { externalDocs } = openapi;
if (externalDocs) {
if (joinedDef.hasOwnProperty('externalDocs')) {
Expand All @@ -339,7 +345,7 @@ export async function handleJoin({
}

function collectPaths(
openapi: Oas3Definition,
openapi: Oas3Definition | Oas3_1Definition,
{
apiFilename,
apiTitle,
Expand Down Expand Up @@ -567,7 +573,7 @@ export async function handleJoin({

function collectWebhooks(
oasVersion: SpecVersion,
openapi: Oas3_1Definition,
openapi: StrictObject<Oas3Definition | Oas3_1Definition>,
{
apiFilename,
apiTitle,
Expand Down
30 changes: 16 additions & 14 deletions packages/cli/src/commands/split/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,18 @@ import {
OPENAPI3_COMPONENT_NAMES,
} from './types';

import type { OasRef } from '@redocly/openapi-core';
import type { Oas3Definition, Oas3_1Definition, Oas2Definition } from '@redocly/openapi-core';
import type {
Definition,
Oas2Definition,
Oas3Schema,
Oas3Definition,
Oas3_1Definition,
Oas3_1Schema,
Oas3Components,
Oas3_1Components,
Oas3ComponentName,
ComponentsFiles,
RefObject,
Oas3PathItem,
OasRef,
Referenced,
} from './types';
} from '@redocly/openapi-core/lib/typings/openapi';
import type { ComponentsFiles, Definition, RefObject } from './types';
import type { CommandArgs } from '../../wrapper';
import type { VerifyConfigOptions } from '../../types';

Expand Down Expand Up @@ -239,7 +237,7 @@ function doesFileDiffer(filename: string, componentData: any) {

function removeEmptyComponents(
openapi: Oas3Definition | Oas3_1Definition,
componentType: Oas3ComponentName
componentType: Oas3ComponentName<Oas3Schema | Oas3_1Schema>
) {
if (openapi.components && isEmptyObject(openapi.components[componentType])) {
delete openapi.components[componentType];
Expand All @@ -264,15 +262,17 @@ function getFileNamePath(componentDirPath: string, componentName: string, ext: s
}

function gatherComponentsFiles(
components: Oas3Components,
components: Oas3Components | Oas3_1Components,
componentsFiles: ComponentsFiles,
componentType: Oas3ComponentName,
componentType: Oas3ComponentName<Oas3Schema | Oas3_1Schema>,
componentName: string,
filename: string
) {
let inherits: string[] = [];
if (componentType === OPENAPI3_COMPONENT.Schemas) {
inherits = ((components?.[componentType]?.[componentName] as Oas3Schema)?.allOf || [])
inherits = (
(components?.[componentType]?.[componentName] as Oas3Schema | Oas3_1Schema)?.allOf || []
)
.map(({ $ref }) => $ref)
.filter(isTruthy);
}
Expand Down Expand Up @@ -347,7 +347,9 @@ function iterateComponents(
componentTypes.forEach(iterateComponentTypes);

// eslint-disable-next-line no-inner-declarations
function iterateAndGatherComponentsFiles(componentType: Oas3ComponentName) {
function iterateAndGatherComponentsFiles(
componentType: Oas3ComponentName<Oas3Schema | Oas3_1Schema>
) {
const componentDirPath = path.join(componentsDir, componentType);
for (const componentName of Object.keys(components?.[componentType] || {})) {
const filename = getFileNamePath(componentDirPath, componentName, ext);
Expand All @@ -356,7 +358,7 @@ function iterateComponents(
}

// eslint-disable-next-line no-inner-declarations
function iterateComponentTypes(componentType: Oas3ComponentName) {
function iterateComponentTypes(componentType: Oas3ComponentName<Oas3Schema | Oas3_1Schema>) {
const componentDirPath = path.join(componentsDir, componentType);
createComponentDir(componentDirPath, componentType);
for (const componentName of Object.keys(components?.[componentType] || {})) {
Expand Down
29 changes: 3 additions & 26 deletions packages/cli/src/commands/split/types.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,6 @@
import {
Oas3Schema,
Oas3_1Schema,
Oas3Definition,
Oas3_1Definition,
Oas3Components,
Oas3PathItem,
Oas3Paths,
Oas3ComponentName,
Oas3_1Webhooks,
Oas2Definition,
Referenced,
} from '@redocly/openapi-core';
export {
Oas3_1Definition,
Oas3Definition,
Oas2Definition,
Oas3Components,
Oas3Paths,
Oas3PathItem,
Oas3ComponentName,
Oas3_1Schema,
Oas3Schema,
Oas3_1Webhooks,
Referenced,
};
import type { Oas2Definition } from '@redocly/openapi-core';
import type { Oas3_1Definition, Oas3Definition } from '@redocly/openapi-core/lib/typings/openapi';

export type Definition = Oas3_1Definition | Oas3Definition | Oas2Definition;
export interface ComponentsFiles {
[schemas: string]: any;
Expand Down
95 changes: 95 additions & 0 deletions packages/core/src/__tests__/lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1674,4 +1674,99 @@ describe('lint', () => {

expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});

it('should throw an error for $schema not expected here - OAS 3.0.x', async () => {
const document = parseYamlToDocument(
outdent`
openapi: 3.0.4
info:
title: test json schema validation keyword $schema - should use an OAS Schema, not JSON Schema
version: 1.0.0
paths:
'/thing':
get:
summary: a sample api
responses:
'200':
description: OK
content:
'application/json':
schema:
$schema: http://json-schema.org/draft-04/schema#
type: object
properties: {}
`,
''
);

const configFilePath = path.join(__dirname, '..', '..', '..', 'redocly.yaml');

const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: { spec: 'error' },
decorators: undefined,
configPath: configFilePath,
}),
});

expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"from": undefined,
"location": [
{
"pointer": "#/paths/~1thing/get/responses/200/content/application~1json/schema/$schema",
"reportOnKey": true,
"source": "",
},
],
"message": "Property \`$schema\` is not expected here.",
"ruleId": "struct",
"severity": "error",
"suggest": [],
},
]
`);
});

it('should allow for $schema to be defined - OAS 3.1.x', async () => {
const document = parseYamlToDocument(
outdent`
openapi: 3.1.1
info:
title: test json schema validation keyword $schema - should allow a JSON Schema
version: 1.0.0
paths:
'/thing':
get:
summary: a sample api
responses:
'200':
description: OK
content:
'application/json':
schema:
$schema: http://json-schema.org/draft-04/schema#
type: object
properties: {}
`,
''
);

const configFilePath = path.join(__dirname, '..', '..', '..', 'redocly.yaml');

const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: { spec: 'error' },
decorators: undefined,
configPath: configFilePath,
}),
});

expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});
});
20 changes: 16 additions & 4 deletions packages/core/src/decorators/oas3/remove-unused-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@ import { isEmptyObject } from '../../utils';

import type { Location } from '../../ref-utils';
import type { Oas3Decorator } from '../../visitors';
import type { Oas3Components, Oas3Definition } from '../../typings/openapi';
import type {
Oas3Definition,
Oas3_1Definition,
Oas3Components,
Oas3_1Components,
} from '../../typings/openapi';

export const RemoveUnusedComponents: Oas3Decorator = () => {
const components = new Map<
string,
{ usedIn: Location[]; componentType?: keyof Oas3Components; name: string }
{
usedIn: Location[];
componentType?: keyof (Oas3Components | Oas3_1Components);
name: string;
}
>();

function registerComponent(
location: Location,
componentType: keyof Oas3Components,
componentType: keyof (Oas3Components | Oas3_1Components),
name: string
): void {
components.set(location.absolutePointer, {
Expand All @@ -22,7 +31,10 @@ export const RemoveUnusedComponents: Oas3Decorator = () => {
});
}

function removeUnusedComponents(root: Oas3Definition, removedPaths: string[]): number {
function removeUnusedComponents(
root: Oas3Definition | Oas3_1Definition,
removedPaths: string[]
): number {
const removedLengthStart = removedPaths.length;

for (const [path, { usedIn, name, componentType }] of components) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ export type {
Oas3Definition,
Oas3_1Definition,
Oas3Components,
Oas3_1Components,
Oas3PathItem,
Oas3Paths,
Oas3ComponentName,
Oas3Schema,
Oas3_1Schema,
Oas3Tag,
Oas3_1Webhooks,
Referenced,
OasRef,
} from './typings/openapi';
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/rules/common/operation-tag-defined.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { Oas3Rule, Oas2Rule } from '../../visitors';
import type { Oas2Definition, Oas2Operation } from '../../typings/swagger';
import type { Oas3Definition, Oas3Operation } from '../../typings/openapi';
import type { Oas3Definition, Oas3_1Definition, Oas3Operation } from '../../typings/openapi';
import type { UserContext } from '../../walk';

export const OperationTagDefined: Oas3Rule | Oas2Rule = () => {
let definedTags: Set<string>;

return {
Root(root: Oas2Definition | Oas3Definition) {
Root(root: Oas2Definition | Oas3Definition | Oas3_1Definition) {
definedTags = new Set((root.tags ?? []).map((t) => t.name));
},
Operation(operation: Oas2Operation | Oas3Operation, { report, location }: UserContext) {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/rules/common/security-defined.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
} from '../../typings/swagger';
import type {
Oas3Definition,
Oas3_1Definition,
Oas3Operation,
Oas3PathItem,
Oas3SecurityScheme,
Expand All @@ -31,7 +32,7 @@ export const SecurityDefined: Oas3Rule | Oas2Rule = (opts: {

return {
Root: {
leave(root: Oas2Definition | Oas3Definition, { report }: UserContext) {
leave(root: Oas2Definition | Oas3Definition | Oas3_1Definition, { report }: UserContext) {
for (const [name, scheme] of referencedSchemes.entries()) {
if (scheme.defined) continue;
for (const reportedFromLocation of scheme.from) {
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/rules/common/tags-alphabetical.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { Oas3Rule, Oas2Rule } from '../../visitors';
import type { Oas2Definition, Oas2Tag } from '../../typings/swagger';
import type { Oas3Definition, Oas3Tag } from '../../typings/openapi';
import type { Oas3Definition, Oas3Tag, Oas3_1Definition } from '../../typings/openapi';
import type { UserContext } from '../../walk';

export const TagsAlphabetical: Oas3Rule | Oas2Rule = ({ ignoreCase = false }) => {
return {
Root(root: Oas2Definition | Oas3Definition, { report, location }: UserContext) {
Root(
root: Oas2Definition | Oas3Definition | Oas3_1Definition,
{ report, location }: UserContext
) {
if (!root.tags) return;
for (let i = 0; i < root.tags.length - 1; i++) {
if (getTagName(root.tags[i], ignoreCase) > getTagName(root.tags[i + 1], ignoreCase)) {
Expand Down
Loading

1 comment on commit ef4b2e2

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report

St.
Category Percentage Covered / Total
🟡 Statements 78.63% 5047/6419
🟡 Branches 67.28% 2056/3056
🟡 Functions 73.13% 833/1139
🟡 Lines 78.92% 4761/6033

Test suite run success

835 tests passing in 120 suites.

Report generated by 🧪jest coverage report action from ef4b2e2

Please sign in to comment.