Skip to content

Commit

Permalink
Update .changeset/selfish-pandas-speak.md
Browse files Browse the repository at this point in the history
Added missing keywords for OAS 3.1.x (JSON Schema 2020-12).

Co-authored-by: Andrew Tatomyr <[email protected]>
  • Loading branch information
jeremyfiel and tatomyr committed Dec 3, 2024
1 parent 7463f90 commit 99dbb67
Show file tree
Hide file tree
Showing 30 changed files with 266 additions and 154 deletions.
5 changes: 2 additions & 3 deletions .changeset/selfish-pandas-speak.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
"@redocly/openapi-core": minor
"@redocly/cli": minor
"@redocly/openapi-core": patch
---

Update typings for OAS 3.0 and OAS 3.1 Schemas
Updated typings for OAS 3.0 and OAS 3.1 Schemas.
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 - allOf 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": "spec",
"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 - allOf 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(`[]`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { yamlAndJsonSyncReader } from '../../utils';
import { isRef } from '../../ref-utils';

import type { Oas3Decorator } from '../../visitors';
import type { Oas3Operation, Oas3RequestBody, Oas3Response } from '../../typings/openapi';
import type { Oas3_1Schema, Oas3Operation, Oas3RequestBody, Oas3Response, Oas3Schema } from '../../typings/openapi';
import type { NonUndefined, ResolveFn, UserContext } from '../../walk';

export const MediaTypeExamplesOverride: Oas3Decorator = ({ operationIds }) => {
return {
Operation: {
enter(operation: Oas3Operation, ctx: UserContext) {
enter(operation: Oas3Operation<Oas3Schema | Oas3_1Schema>, ctx: UserContext) {
const operationId = operation.operationId;

if (!operationId) {
Expand All @@ -23,7 +23,7 @@ export const MediaTypeExamplesOverride: Oas3Decorator = ({ operationIds }) => {

if (properties.responses && operation.responses) {
for (const responseCode of Object.keys(properties.responses)) {
const resolvedResponse = checkAndResolveRef<Oas3Response>(
const resolvedResponse = checkAndResolveRef<Oas3Response<Oas3Schema | Oas3_1Schema>>(
operation.responses[responseCode],
ctx.resolve
);
Expand All @@ -46,7 +46,7 @@ export const MediaTypeExamplesOverride: Oas3Decorator = ({ operationIds }) => {
}

if (properties.request && operation.requestBody) {
const resolvedRequest = checkAndResolveRef<Oas3RequestBody>(
const resolvedRequest = checkAndResolveRef<Oas3RequestBody<Oas3Schema | Oas3_1Schema>>(
operation.requestBody,
ctx.resolve
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { readFileAsStringSync } from '../../utils';

import type { Oas3Decorator, Oas2Decorator } from '../../visitors';
import type { Oas2Operation } from '../../typings/swagger';
import type { Oas3Operation } from '../../typings/openapi';
import type { Oas3Schema, Oas3_1Schema, Oas3Operation } from '../../typings/openapi';
import type { UserContext } from '../../walk';

export const OperationDescriptionOverride: Oas3Decorator | Oas2Decorator = ({ operationIds }) => {
return {
Operation: {
leave(operation: Oas2Operation | Oas3Operation, { report, location }: UserContext) {
leave(operation: Oas2Operation | Oas3Operation<Oas3Schema | Oas3_1Schema>, { report, location }: UserContext) {
if (!operation.operationId) return;
if (!operationIds)
throw new Error(
Expand Down
8 changes: 4 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,17 @@ import { isEmptyObject } from '../../utils';

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

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

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

function removeUnusedComponents(root: Oas3Definition, removedPaths: string[]): number {
function removeUnusedComponents(root: Oas3Definition<Oas3Schema | Oas3_1Schema>, 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/rules/common/no-ambiguous-paths.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Oas3Rule, Oas2Rule } from '../../visitors';
import type { UserContext } from '../../walk';
import type { Oas3Paths } from '../../typings/openapi';
import type { Oas3_1Schema, Oas3Paths, Oas3Schema } from '../../typings/openapi';
import type { Oas2Paths } from '../../typings/swagger';

export const NoAmbiguousPaths: Oas3Rule | Oas2Rule = () => {
return {
Paths(pathMap: Oas3Paths | Oas2Paths, { report, location }: UserContext) {
Paths(pathMap: Oas3Paths<Oas3Schema | Oas3_1Schema> | Oas2Paths, { report, location }: UserContext) {
const seenPaths: string[] = [];

for (const currentPath of Object.keys(pathMap)) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/rules/common/no-http-verbs-in-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { isPathParameter, splitCamelCaseIntoWords } from '../../utils';

import type { Oas3Rule, Oas2Rule } from '../../visitors';
import type { Oas2PathItem } from '../../typings/swagger';
import type { Oas3PathItem } from '../../typings/openapi';
import type { Oas3Schema, Oas3_1Schema, Oas3PathItem } from '../../typings/openapi';
import type { UserContext } from '../../walk';

const httpMethods = ['get', 'head', 'post', 'put', 'patch', 'delete', 'options', 'trace'];

export const NoHttpVerbsInPaths: Oas3Rule | Oas2Rule = ({ splitIntoWords }) => {
return {
PathItem(_path: Oas2PathItem | Oas3PathItem, { key, report, location }: UserContext) {
PathItem(_path: Oas2PathItem | Oas3PathItem<Oas3Schema | Oas3_1Schema>, { key, report, location }: UserContext) {
const pathKey = key.toString();
if (!pathKey.startsWith('/')) return;
const pathSegments = pathKey.split('/');
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/rules/common/no-identical-paths.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Oas3Rule, Oas2Rule } from '../../visitors';
import type { UserContext } from '../../walk';
import type { Oas3Paths } from '../../typings/openapi';
import type { Oas3Schema, Oas3_1Schema, Oas3Paths } from '../../typings/openapi';
import type { Oas2Paths } from '../../typings/swagger';

export const NoIdenticalPaths: Oas3Rule | Oas2Rule = () => {
return {
Paths(pathMap: Oas3Paths | Oas2Paths, { report, location }: UserContext) {
Paths(pathMap: Oas3Paths<Oas3Schema | Oas3_1Schema> | Oas2Paths, { report, location }: UserContext) {
const Paths = new Map<string, string>();
for (const pathName of Object.keys(pathMap)) {
const id = pathName.replace(/{.+?}/g, '{VARIABLE}');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { getAdditionalPropertiesOption, validateExample } from '../utils';

import type { UserContext } from '../../walk';
import type { Oas3Parameter } from '../../typings/openapi';
import type { Oas3Schema, Oas3_1Schema, Oas3Parameter } from '../../typings/openapi';

export const NoInvalidParameterExamples: any = (opts: any) => {
const allowAdditionalProperties = getAdditionalPropertiesOption(opts) ?? false;
return {
Parameter: {
leave(parameter: Oas3Parameter, ctx: UserContext) {
leave(parameter: Oas3Parameter<Oas3Schema | Oas3_1Schema>, ctx: UserContext) {
if (parameter.example !== undefined) {
validateExample(
parameter.example,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/rules/common/operation-description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { validateDefinedAndNonEmpty } from '../utils';
import type { Oas3Rule, Oas2Rule } from '../../visitors';
import type { UserContext } from '../../walk';
import type { Oas2Operation } from '../../typings/swagger';
import type { Oas3Operation } from '../../typings/openapi';
import type { Oas3Operation, Oas3Schema, Oas3_1Schema } from '../../typings/openapi';

export const OperationDescription: Oas3Rule | Oas2Rule = () => {
return {
Operation(operation: Oas2Operation | Oas3Operation, ctx: UserContext) {
Operation(operation: Oas2Operation | Oas3Operation<Oas3Schema | Oas3_1Schema>, ctx: UserContext) {
validateDefinedAndNonEmpty('description', operation, ctx);
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { Oas3Rule, Oas2Rule } from '../../visitors';
import type { Oas2Operation } from '../../typings/swagger';
import type { Oas3Operation } from '../../typings/openapi';
import type { Oas3Operation, Oas3Schema, Oas3_1Schema } from '../../typings/openapi';
import type { UserContext } from '../../walk';

export const OperationIdUnique: Oas3Rule | Oas2Rule = () => {
const seenOperations = new Set();

return {
Operation(operation: Oas2Operation | Oas3Operation, { report, location }: UserContext) {
Operation(operation: Oas2Operation | Oas3Operation<Oas3Schema | Oas3_1Schema>, { report, location }: UserContext) {
if (!operation.operationId) return;
if (seenOperations.has(operation.operationId)) {
report({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { Oas3Rule, Oas2Rule } from '../../visitors';
import type { Oas2Operation } from '../../typings/swagger';
import type { Oas3Operation } from '../../typings/openapi';
import type { Oas3Operation, Oas3Schema, Oas3_1Schema } from '../../typings/openapi';
import type { UserContext } from '../../walk';

// eslint-disable-next-line no-useless-escape
const validUrlSymbols = /^[A-Za-z0-9-._~:/?#\[\]@!\$&'()*+,;=]*$/;

export const OperationIdUrlSafe: Oas3Rule | Oas2Rule = () => {
return {
Operation(operation: Oas2Operation | Oas3Operation, { report, location }: UserContext) {
Operation(operation: Oas2Operation | Oas3Operation<Oas3Schema | Oas3_1Schema>, { report, location }: UserContext) {
if (operation.operationId && !validUrlSymbols.test(operation.operationId)) {
report({
message: 'Operation `operationId` should not have URL invalid characters.',
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/rules/common/operation-operationId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { validateDefinedAndNonEmpty } from '../utils';
import type { Oas3Rule, Oas2Rule } from '../../visitors';
import type { UserContext } from '../../walk';
import type { Oas2Operation } from '../../typings/swagger';
import type { Oas3Operation } from '../../typings/openapi';
import type { Oas3Operation, Oas3Schema, Oas3_1Schema } from '../../typings/openapi';

export const OperationOperationId: Oas3Rule | Oas2Rule = () => {
return {
Root: {
PathItem: {
Operation(operation: Oas2Operation | Oas3Operation, ctx: UserContext) {
Operation(operation: Oas2Operation | Oas3Operation<Oas3Schema | Oas3_1Schema>, ctx: UserContext) {
validateDefinedAndNonEmpty('operationId', operation, ctx);
},
},
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/rules/common/operation-parameters-unique.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Oas3Rule, Oas2Rule } from '../../visitors';
import type { Oas2Parameter } from '../../typings/swagger';
import type { Oas3Parameter } from '../../typings/openapi';
import type { Oas3Schema, Oas3_1Schema, Oas3Parameter } from '../../typings/openapi';
import type { UserContext } from '../../walk';

export const OperationParametersUnique: Oas3Rule | Oas2Rule = () => {
Expand All @@ -13,7 +13,7 @@ export const OperationParametersUnique: Oas3Rule | Oas2Rule = () => {
seenPathParams = new Set();
},
Parameter(
parameter: Oas2Parameter | Oas3Parameter,
parameter: Oas2Parameter | Oas3Parameter<Oas3Schema | Oas3_1Schema>,
{ report, key, parentLocations }: UserContext
) {
const paramId = `${parameter.in}___${parameter.name}`;
Expand All @@ -30,7 +30,7 @@ export const OperationParametersUnique: Oas3Rule | Oas2Rule = () => {
seenOperationParams = new Set();
},
Parameter(
parameter: Oas2Parameter | Oas3Parameter,
parameter: Oas2Parameter | Oas3Parameter<Oas3Schema | Oas3_1Schema>,
{ report, key, parentLocations }: UserContext
) {
const paramId = `${parameter.in}___${parameter.name}`;
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/rules/common/operation-singular-tag.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Oas3Rule, Oas2Rule } from '../../visitors';
import type { Oas2Operation } from '../../typings/swagger';
import type { Oas3Operation } from '../../typings/openapi';
import type { Oas3Operation, Oas3Schema, Oas3_1Schema } from '../../typings/openapi';
import type { UserContext } from '../../walk';

export const OperationSingularTag: Oas3Rule | Oas2Rule = () => {
return {
Operation(operation: Oas2Operation | Oas3Operation, { report, location }: UserContext) {
Operation(operation: Oas2Operation | Oas3Operation<Oas3Schema | Oas3_1Schema>, { report, location }: UserContext) {
if (operation.tags && operation.tags.length > 1) {
report({
message: 'Operation `tags` object should have only one tag.',
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/rules/common/operation-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { validateDefinedAndNonEmpty } from '../utils';
import type { Oas3Rule, Oas2Rule } from '../../visitors';
import type { UserContext } from '../../walk';
import type { Oas2Operation } from '../../typings/swagger';
import type { Oas3Operation } from '../../typings/openapi';
import type { Oas3Operation, Oas3Schema, Oas3_1Schema } from '../../typings/openapi';

export const OperationSummary: Oas3Rule | Oas2Rule = () => {
return {
Operation(operation: Oas2Operation | Oas3Operation, ctx: UserContext) {
Operation(operation: Oas2Operation | Oas3Operation<Oas3Schema | Oas3_1Schema>, ctx: UserContext) {
validateDefinedAndNonEmpty('summary', operation, ctx);
},
};
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/rules/common/operation-tag-defined.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { Oas3Rule, Oas2Rule } from '../../visitors';
import type { Oas2Definition, Oas2Operation } from '../../typings/swagger';
import type { Oas3Definition, Oas3Operation } from '../../typings/openapi';
import type { Oas3Definition, Oas3Operation, Oas3Schema, Oas3_1Schema } 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<Oas3Schema | Oas3_1Schema>) {
definedTags = new Set((root.tags ?? []).map((t) => t.name));
},
Operation(operation: Oas2Operation | Oas3Operation, { report, location }: UserContext) {
Operation(operation: Oas2Operation | Oas3Operation<Oas3Schema | Oas3_1Schema>, { report, location }: UserContext) {
if (operation.tags) {
for (let i = 0; i < operation.tags.length; i++) {
if (!definedTags.has(operation.tags[i])) {
Expand Down
Loading

0 comments on commit 99dbb67

Please sign in to comment.