Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(typings): update OpenAPI 3.0 and 3.1 typing declarations #1795

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { 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 All @@ -43,7 +46,7 @@
apiFilename: string;
apiTitle?: string;
tags: Oas3Tag[];
potentialConflicts: any;

Check warning on line 49 in packages/cli/src/commands/join.ts

View workflow job for this annotation

GitHub Actions / code-style-check

Unexpected any. Specify a different type
tagsPrefix: string;
componentsPrefix: string | undefined;
};
Expand Down Expand Up @@ -161,7 +164,7 @@
}
}

const joinedDef: any = {};

Check warning on line 167 in packages/cli/src/commands/join.ts

View workflow job for this annotation

GitHub Actions / code-style-check

Unexpected any. Specify a different type
const potentialConflicts = {
tags: {},
paths: {},
Expand Down Expand Up @@ -284,7 +287,7 @@
}

function getIndexGroup(name: string): number {
return joinedDef[xTagGroups].findIndex((item: any) => item.name === name);

Check warning on line 290 in packages/cli/src/commands/join.ts

View workflow job for this annotation

GitHub Actions / code-style-check

Unexpected any. Specify a different type
}

function createXTagGroups(name: string) {
Expand All @@ -292,7 +295,7 @@
joinedDef[xTagGroups] = [];
}

if (!joinedDef[xTagGroups].some((g: any) => g.name === name)) {

Check warning on line 298 in packages/cli/src/commands/join.ts

View workflow job for this annotation

GitHub Actions / code-style-check

Unexpected any. Specify a different type
joinedDef[xTagGroups].push({ name, tags: [] });
}

Expand All @@ -311,21 +314,24 @@
}
}

function collectServers(openapi: Oas3Definition) {
function collectServers(openapi: Oas3Definition | Oas3_1Definition) {
const { servers } = openapi;
if (servers) {
if (!joinedDef.hasOwnProperty('servers')) {
joinedDef['servers'] = [];
}
for (const server of servers) {
if (!joinedDef.servers.some((s: any) => s.url === server.url)) {

Check warning on line 324 in packages/cli/src/commands/join.ts

View workflow job for this annotation

GitHub Actions / code-style-check

Unexpected any. Specify a different type
joinedDef.servers.push(server);
}
}
}
}

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 @@
}

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

function collectWebhooks(
oasVersion: SpecVersion,
openapi: Oas3_1Definition,
openapi: StrictObject<Oas3Definition | Oas3_1Definition>,
{
apiFilename,
apiTitle,
Expand Down Expand Up @@ -617,7 +623,7 @@
}

function addInfoSectionAndSpecVersion(
documents: any,

Check warning on line 626 in packages/cli/src/commands/join.ts

View workflow job for this annotation

GitHub Actions / code-style-check

Unexpected any. Specify a different type
prefixComponentsWithInfoProp: string | undefined
) {
const firstApi = documents[0];
Expand Down
32 changes: 17 additions & 15 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,
Oas3Components,
Oas3_1Schema,
Oas3ComponentsBase,
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: Oas3ComponentsBase<Oas3Schema | Oas3_1Schema> | Oas3_1Components,
jeremyfiel marked this conversation as resolved.
Show resolved Hide resolved
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(`[]`);
});
});
22 changes: 18 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,28 @@ 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,
Oas3ComponentsBase,
Oas3_1Components,
Oas3Schema,
Oas3_1Schema,
} from '../../typings/openapi';

export const RemoveUnusedComponents: Oas3Decorator = () => {
const components = new Map<
string,
{ usedIn: Location[]; componentType?: keyof Oas3Components; name: string }
{
usedIn: Location[];
componentType?: keyof (Oas3ComponentsBase<Oas3Schema | Oas3_1Schema> | Oas3_1Components);
jeremyfiel marked this conversation as resolved.
Show resolved Hide resolved
name: string;
}
>();

function registerComponent(
location: Location,
componentType: keyof Oas3Components,
componentType: keyof (Oas3ComponentsBase<Oas3Schema | Oas3_1Schema> | Oas3_1Components),
jeremyfiel marked this conversation as resolved.
Show resolved Hide resolved
name: string
): void {
components.set(location.absolutePointer, {
Expand All @@ -22,7 +33,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
4 changes: 2 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ export { ConfigTypes } from './types/redocly-yaml';
export type {
Oas3Definition,
Oas3_1Definition,
Oas3Components,
Oas3ComponentsBase,
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
Loading