From 1cc34ec2fe778a423811d49a8112ce7becf541d8 Mon Sep 17 00:00:00 2001 From: "Daniel A. White" Date: Tue, 12 Dec 2023 10:13:49 -0500 Subject: [PATCH] feat(oas3): adds webhook support for oas 3.1 (#258) BREAKING CHANGE: http operation parsing changed to introduce webhooks --- package.json | 2 +- src/context.ts | 2 + src/generators.ts | 8 ++ src/oas/__tests__/operation.test.ts | 2 +- src/oas/index.ts | 2 +- src/oas/operation.ts | 101 +++++++++++------- src/oas/service.ts | 3 +- src/oas/types.ts | 15 +-- src/oas2/__tests__/__fixtures__/id/bundled.ts | 1 + src/oas2/__tests__/bundle.test.ts | 2 + src/oas2/__tests__/operation.test.ts | 18 ++-- src/oas2/operation.ts | 23 ++-- src/oas2/service.ts | 1 + .../__fixtures__/examples/bundled.ts | 1 + src/oas3/__tests__/__fixtures__/id/bundled.ts | 44 ++++++++ src/oas3/__tests__/__fixtures__/id/input.json | 16 +++ .../__fixtures__/shared-components/bundled.ts | 1 + src/oas3/__tests__/accessors.test.ts | 2 +- src/oas3/__tests__/bundle.test.ts | 6 ++ src/oas3/__tests__/operation.test.ts | 90 ++++++++++------ src/oas3/__tests__/service.test.ts | 3 +- src/oas3/operation.ts | 49 ++++++--- src/oas3/service.ts | 6 +- .../transformers/__tests__/callbacks.test.ts | 2 +- .../transformers/__tests__/responses.test.ts | 2 +- .../transformers/__tests__/securities.test.ts | 2 +- .../transformers/__tests__/servers.test.ts | 2 +- src/oas3/transformers/callbacks.ts | 6 +- src/oas3/types.ts | 12 ++- src/postman/types.ts | 16 +-- src/types.ts | 23 +++- yarn.lock | 8 +- 32 files changed, 335 insertions(+), 136 deletions(-) diff --git a/package.json b/package.json index 88158799..6e517d71 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "dependencies": { "@stoplight/json": "^3.18.1", "@stoplight/json-schema-generator": "1.0.2", - "@stoplight/types": "14.0.0", + "@stoplight/types": "14.1.0", "@types/json-schema": "7.0.11", "@types/swagger-schema-official": "~2.0.22", "@types/type-is": "^1.6.3", diff --git a/src/context.ts b/src/context.ts index d3927ab9..fd86dfa9 100644 --- a/src/context.ts +++ b/src/context.ts @@ -66,6 +66,8 @@ export function createContext>( service: '', path: '', operation: '', + webhookName: '', + webhook: '', }, references: {}, parentId: '', diff --git a/src/generators.ts b/src/generators.ts index 3c8a5989..94b430a8 100644 --- a/src/generators.ts +++ b/src/generators.ts @@ -36,10 +36,18 @@ export const idGenerators = { return join(['http_path', props.parentId, sanitizePath(props.path)]); }, + httpWebhookName: (props: Context & { name: string }) => { + return join(['http_webhook_name', props.parentId, sanitizePath(props.name)]); + }, + httpOperation: (props: Context & { method: string; path: string }) => { return join(['http_operation', props.parentId, props.method, sanitizePath(props.path)]); }, + httpWebhookOperation: (props: Context & { method: string; name: string }) => { + return join(['http_webhook_operation', props.parentId, props.method, sanitizePath(props.name)]); + }, + httpCallbackOperation: (props: Context & { method: string; path: string }) => { return join(['http_callback', props.parentId, props.method, props.path]); }, diff --git a/src/oas/__tests__/operation.test.ts b/src/oas/__tests__/operation.test.ts index 5ca74c39..50eb251c 100644 --- a/src/oas/__tests__/operation.test.ts +++ b/src/oas/__tests__/operation.test.ts @@ -1,10 +1,10 @@ import { bundleTarget } from '@stoplight/json'; -import { OpenAPIObject } from 'openapi3-ts'; import { Spec } from 'swagger-schema-official'; import { setSkipHashing } from '../../hash'; import { transformOas2Operations } from '../../oas2/operation'; import { transformOas3Operations } from '../../oas3/operation'; +import { OpenAPIObject } from '../../oas3/types'; setSkipHashing(true); diff --git a/src/oas/index.ts b/src/oas/index.ts index 9eff1ada..f6b64175 100644 --- a/src/oas/index.ts +++ b/src/oas/index.ts @@ -7,7 +7,7 @@ export type { Oas2HttpServiceTransformer, Oas2TransformOperationOpts, Oas2TransformServiceOpts, - Oas3HttpOperationTransformer, + Oas3HttpEndpointOperationTransformer, Oas3HttpServiceBundle, Oas3HttpServiceTransformer, Oas3TransformOperationOpts, diff --git a/src/oas/operation.ts b/src/oas/operation.ts index be263876..4c84150f 100644 --- a/src/oas/operation.ts +++ b/src/oas/operation.ts @@ -1,11 +1,11 @@ import { isPlainObject } from '@stoplight/json'; -import type { DeepPartial, IHttpOperation } from '@stoplight/types'; +import type { DeepPartial, IHttpEndpointOperation } from '@stoplight/types'; import type { OpenAPIObject, OperationObject, PathsObject } from 'openapi3-ts'; import type { Spec } from 'swagger-schema-official'; import pickBy = require('lodash.pickby'); import { isBoolean, isString } from '../guards'; -import type { Fragment, HttpOperationTransformer } from '../types'; +import type { EndpointOperationConfig, Fragment, HttpEndpointOperationTransformer } from '../types'; import { TransformerContext, TranslateFunction } from '../types'; import { extractId } from '../utils'; import { getExtensions } from './accessors'; @@ -15,16 +15,32 @@ import { translateToSecurityDeclarationType } from './transformers'; const DEFAULT_METHODS = ['get', 'post', 'put', 'delete', 'options', 'head', 'patch', 'trace']; -export function transformOasOperations>( +export const OPERATION_CONFIG: EndpointOperationConfig = { + type: 'operation', + documentProp: 'paths', + nameProp: 'path', +}; + +export const WEBHOOK_CONFIG: EndpointOperationConfig = { + type: 'webhook', + documentProp: 'webhooks', + nameProp: 'name', +}; + +export function transformOasEndpointOperations< + T extends Fragment & DeepPartial, + TEndpoint extends IHttpEndpointOperation, +>( document: T, - transformer: HttpOperationTransformer, + transformer: HttpEndpointOperationTransformer, + config: EndpointOperationConfig, methods: string[] | null = DEFAULT_METHODS, + ctx?: TransformerContext, -): IHttpOperation[] { - const paths = isPlainObject(document.paths) ? Object.keys(document.paths) : []; +): TEndpoint[] { + const entries = isPlainObject(document[config.documentProp]) ? Object.entries(document[config.documentProp]) : []; - return paths.flatMap(path => { - const value = document.paths![path]; + return entries.flatMap(([name, value]) => { if (!isPlainObject(value)) return []; let operations = Object.keys(value); @@ -35,75 +51,84 @@ export function transformOasOperations transformer({ document, - path, + name, method, + config, ctx, }), ); }); } -export const transformOasOperation: TranslateFunction< +export const transformOasEndpointOperation: TranslateFunction< DeepPartial | DeepPartial, - [path: string, method: string], - Omit -> = function (path: string, method: string) { - const pathObj = this.maybeResolveLocalRef(this.document?.paths?.[path]) as PathsObject; + [config: EndpointOperationConfig, name: string, method: string], + Omit +> = function ({ type, documentProp, nameProp }: EndpointOperationConfig, name: string, method: string) { + const pathObj = this.maybeResolveLocalRef(this.document?.[documentProp]?.[name]) as PathsObject; if (typeof pathObj !== 'object' || pathObj === null) { - throw new Error(`Could not find ${['paths', path].join('/')} in the provided spec.`); + throw new Error(`Could not find ${[documentProp, name].join('/')} in the provided spec.`); } - const operation = this.maybeResolveLocalRef(pathObj[method]) as OperationObject; - if (!operation) { - throw new Error(`Could not find ${['paths', path, method].join('/')} in the provided spec.`); + const obj = this.maybeResolveLocalRef(pathObj[method]) as OperationObject; + if (!obj) { + throw new Error(`Could not find ${[documentProp, name, method].join('/')} in the provided spec.`); } const serviceId = (this.ids.service = String(this.document['x-stoplight']?.id)); - this.ids.path = this.generateId.httpPath({ parentId: serviceId, path }); - let operationId: string; + if (type === 'operation') { + this.ids.path = this.generateId.httpPath({ parentId: serviceId, path: name }); + } else { + this.ids.webhookName = this.generateId.httpWebhookName({ parentId: serviceId, name }); + } + let id: string; if (this.context === 'callback') { - operationId = this.ids.operation = - extractId(operation) ?? + id = this.ids.operation = + extractId(obj) ?? this.generateId.httpCallbackOperation({ parentId: serviceId, method, - path, + path: name, }); + } else if (type === 'operation') { + id = this.ids.operation = + extractId(obj) ?? this.generateId.httpOperation({ parentId: serviceId, method, path: name }); } else { - operationId = this.ids.operation = - extractId(operation) ?? this.generateId.httpOperation({ parentId: serviceId, method, path }); + id = this.ids.webhook = + extractId(obj) ?? this.generateId.httpWebhookOperation({ parentId: serviceId, method, name }); } - this.parentId = operationId; - this.context = 'operation'; + this.parentId = id; + + this.context = type; return { - id: operationId, + id, method, - path, + [nameProp]: name, - tags: translateToTags.call(this, operation.tags), - extensions: getExtensions(operation), + tags: translateToTags.call(this, obj.tags), + extensions: getExtensions(obj), ...pickBy( { - deprecated: operation.deprecated, - internal: operation['x-internal'], + deprecated: obj.deprecated, + internal: obj['x-internal'], }, isBoolean, ), ...pickBy( { - iid: operation.operationId, - description: operation.description, - summary: operation.summary, + iid: obj.operationId, + description: obj.description, + summary: obj.summary, }, isString, ), - securityDeclarationType: translateToSecurityDeclarationType(operation), - ...toExternalDocs(operation.externalDocs), + securityDeclarationType: translateToSecurityDeclarationType(obj), + ...toExternalDocs(obj.externalDocs), }; }; diff --git a/src/oas/service.ts b/src/oas/service.ts index 6acb9d0a..eea8feeb 100644 --- a/src/oas/service.ts +++ b/src/oas/service.ts @@ -1,7 +1,8 @@ import { isPlainObject } from '@stoplight/json'; import { DeepPartial, IHttpService } from '@stoplight/types'; -import { OpenAPIObject } from 'openapi3-ts'; import { Spec } from 'swagger-schema-official'; + +import { OpenAPIObject } from '../oas3/types'; import pickBy = require('lodash.pickby'); import { isBoolean, isNonNullable, isString } from '../guards'; diff --git a/src/oas/types.ts b/src/oas/types.ts index cc653617..8c8dbda5 100644 --- a/src/oas/types.ts +++ b/src/oas/types.ts @@ -1,12 +1,12 @@ -import { DeepPartial } from '@stoplight/types'; +import { DeepPartial, IHttpEndpointOperation, IHttpOperation } from '@stoplight/types'; import type * as OAS3 from 'openapi3-ts'; import type * as OAS2 from 'swagger-schema-official'; import type { - HttpOperationTransformer, + HttpEndpointOperationTransformer, HttpServiceBundle, HttpServiceTransformer, - ITransformOperationOpts, + ITransformEndpointOperationOpts, ITransformServiceOpts, } from '../types'; @@ -30,10 +30,11 @@ export type Oas3HttpServiceBundle = HttpServiceBundle; /** * Operation */ -export type Oas2TransformOperationOpts = ITransformOperationOpts>; -export type Oas3TransformOperationOpts = ITransformOperationOpts>; -export type Oas2HttpOperationTransformer = HttpOperationTransformer; -export type Oas3HttpOperationTransformer = HttpOperationTransformer; +export type Oas2TransformOperationOpts = ITransformEndpointOperationOpts>; +export type Oas3TransformOperationOpts = ITransformEndpointOperationOpts>; +export type Oas2HttpOperationTransformer = HttpEndpointOperationTransformer; +export type Oas3HttpEndpointOperationTransformer = + HttpEndpointOperationTransformer; export type Oas3ParamBase = ParamBase & { in: OAS3.ParameterLocation }; export type Oas2ParamBase = ParamBase & { in: OAS2.BaseParameter['in'] }; diff --git a/src/oas2/__tests__/__fixtures__/id/bundled.ts b/src/oas2/__tests__/__fixtures__/id/bundled.ts index 976016ef..64da28ed 100644 --- a/src/oas2/__tests__/__fixtures__/id/bundled.ts +++ b/src/oas2/__tests__/__fixtures__/id/bundled.ts @@ -426,4 +426,5 @@ export default { ], }, ], + webhooks: [], }; diff --git a/src/oas2/__tests__/bundle.test.ts b/src/oas2/__tests__/bundle.test.ts index 0ad76022..f3af30af 100644 --- a/src/oas2/__tests__/bundle.test.ts +++ b/src/oas2/__tests__/bundle.test.ts @@ -159,6 +159,7 @@ describe('bundleOas2Service', () => { tags: [], }, ], + webhooks: [], components: { callbacks: [], cookie: [], @@ -281,6 +282,7 @@ describe('bundleOas2Service', () => { tags: [], }, ], + webhooks: [], components: { callbacks: [], cookie: [], diff --git a/src/oas2/__tests__/operation.test.ts b/src/oas2/__tests__/operation.test.ts index 0b8f6c2b..cc25b97f 100644 --- a/src/oas2/__tests__/operation.test.ts +++ b/src/oas2/__tests__/operation.test.ts @@ -1,8 +1,8 @@ import { DeepPartial } from '@stoplight/types'; -import { OpenAPIObject } from 'openapi3-ts'; import { Spec } from 'swagger-schema-official'; import { setSkipHashing } from '../../hash'; +import { OPERATION_CONFIG } from '../../oas/operation'; import { transformOas2Operation, transformOas2Operations } from '../operation'; setSkipHashing(true); @@ -69,9 +69,10 @@ describe('transformOas2Operation', () => { expect( transformOas2Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', document, + config: OPERATION_CONFIG, }), ).toMatchSnapshot({ id: expect.any(String), @@ -147,9 +148,10 @@ describe('transformOas2Operation', () => { }; const result = transformOas2Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'delete', document, + config: OPERATION_CONFIG, }); expect(result.responses[0].contents).toHaveLength(0); @@ -169,9 +171,10 @@ describe('transformOas2Operation', () => { expect( transformOas2Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', document, + config: OPERATION_CONFIG, }), ).toHaveProperty('deprecated', true); }); @@ -226,7 +229,7 @@ describe('transformOas2Operation', () => { }); it('given malformed parameters should translate operation with those parameters', () => { - const document: Partial = { + const document: DeepPartial = { swagger: '2.0', paths: { '/users/{userId}': { @@ -236,7 +239,7 @@ describe('transformOas2Operation', () => { in: 'header', name: 'name', }, - null, + null as any, ], }, }, @@ -245,9 +248,10 @@ describe('transformOas2Operation', () => { expect( transformOas2Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', document, + config: OPERATION_CONFIG, }), ).toStrictEqual({ id: expect.any(String), diff --git a/src/oas2/operation.ts b/src/oas2/operation.ts index d1fd44ef..ebc0c913 100644 --- a/src/oas2/operation.ts +++ b/src/oas2/operation.ts @@ -2,7 +2,7 @@ import type { DeepPartial, IHttpOperation } from '@stoplight/types'; import type { Spec } from 'swagger-schema-official'; import { createContext } from '../oas/context'; -import { transformOasOperation, transformOasOperations } from '../oas/operation'; +import { OPERATION_CONFIG, transformOasEndpointOperation, transformOasEndpointOperations } from '../oas/operation'; import { Oas2HttpOperationTransformer } from '../oas/types'; import type { Fragment } from '../types'; import { TransformerContext } from '../types'; @@ -15,25 +15,26 @@ export function transformOas2Operations = DeepPartia document: T, ctx?: TransformerContext, ): IHttpOperation[] { - return transformOasOperations(document, transformOas2Operation, void 0, ctx); + return transformOasEndpointOperations(document, transformOas2Operation, OPERATION_CONFIG, void 0, ctx); } export const transformOas2Operation: Oas2HttpOperationTransformer = ({ document: _document, - path, + name, method, + config, ctx = createContext(_document), }) => { - const httpOperation = transformOasOperation.call(ctx, path, method); - const pathObj = ctx.maybeResolveLocalRef(ctx.document.paths![path]) as Fragment; - const operation = ctx.maybeResolveLocalRef(pathObj[method]) as Fragment; + const httpEndpointOperation = transformOasEndpointOperation.call(ctx, config, name, method); + const parentObj = ctx.maybeResolveLocalRef(ctx.document[config.documentProp]![name]) as Fragment; + const obj = ctx.maybeResolveLocalRef(parentObj[method]) as Fragment; return { - ...httpOperation, + ...httpEndpointOperation, - responses: translateToResponses.call(ctx, operation), - servers: translateToServers.call(ctx, operation), - request: translateToRequest.call(ctx, pathObj, operation), - security: translateToSecurities.call(ctx, operation.security, 'requirement'), + responses: translateToResponses.call(ctx, obj), + servers: translateToServers.call(ctx, obj), + request: translateToRequest.call(ctx, parentObj, obj), + security: translateToSecurities.call(ctx, obj.security, 'requirement'), } as any; }; diff --git a/src/oas2/service.ts b/src/oas2/service.ts index 910c9bf0..8e13805f 100644 --- a/src/oas2/service.ts +++ b/src/oas2/service.ts @@ -53,6 +53,7 @@ export const bundleOas2Service: Oas2HttpServiceBundle = ({ document: _document } return { ...service, operations, + webhooks: [], components, }; }; diff --git a/src/oas3/__tests__/__fixtures__/examples/bundled.ts b/src/oas3/__tests__/__fixtures__/examples/bundled.ts index e5aa6331..129bed8a 100644 --- a/src/oas3/__tests__/__fixtures__/examples/bundled.ts +++ b/src/oas3/__tests__/__fixtures__/examples/bundled.ts @@ -66,6 +66,7 @@ export default { tags: [], }, ], + webhooks: [], components: { callbacks: [], cookie: [], diff --git a/src/oas3/__tests__/__fixtures__/id/bundled.ts b/src/oas3/__tests__/__fixtures__/id/bundled.ts index b641a6a0..fdf2c685 100644 --- a/src/oas3/__tests__/__fixtures__/id/bundled.ts +++ b/src/oas3/__tests__/__fixtures__/id/bundled.ts @@ -391,6 +391,50 @@ export default { ], }, ], + webhooks: [ + { + extensions: {}, + id: 'http_webhook_operation-service_abc-post-git.push', + iid: 'post-git.push', + method: 'post', + name: 'git.push', + request: { + body: { + contents: [ + { + encodings: [], + examples: [], + id: 'http_media-http_request_body-http_webhook_operation-service_abc-post-git.push-application/json', + mediaType: 'application/json', + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + 'x-stoplight': { + id: 'schema-http_media-http_request_body-http_webhook_operation-service_abc-post-git.push-application/json-', + }, + }, + }, + ], + id: 'http_request_body-http_webhook_operation-service_abc-post-git.push', + }, + cookie: [], + headers: [], + path: [], + query: [], + }, + responses: [], + security: [], + securityDeclarationType: 'inheritedFromService', + servers: [ + { + id: 'http_server-service_abc-http://localhost:3000', + name: 'Users API', + url: 'http://localhost:3000', + }, + ], + tags: [], + }, + ], extensions: { 'x-stoplight': { id: 'service_abc', diff --git a/src/oas3/__tests__/__fixtures__/id/input.json b/src/oas3/__tests__/__fixtures__/id/input.json index d382ebbe..42aae2b3 100644 --- a/src/oas3/__tests__/__fixtures__/id/input.json +++ b/src/oas3/__tests__/__fixtures__/id/input.json @@ -118,6 +118,22 @@ } } }, + "webhooks": { + "git.push": { + "post": { + "operationId": "post-git.push", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, "components": { "schemas": { "User": { diff --git a/src/oas3/__tests__/__fixtures__/shared-components/bundled.ts b/src/oas3/__tests__/__fixtures__/shared-components/bundled.ts index e9a39895..9f103e50 100644 --- a/src/oas3/__tests__/__fixtures__/shared-components/bundled.ts +++ b/src/oas3/__tests__/__fixtures__/shared-components/bundled.ts @@ -62,6 +62,7 @@ export default { tags: [], }, ], + webhooks: [], components: { callbacks: [], cookie: [], diff --git a/src/oas3/__tests__/accessors.test.ts b/src/oas3/__tests__/accessors.test.ts index 533bf32e..ad60575e 100644 --- a/src/oas3/__tests__/accessors.test.ts +++ b/src/oas3/__tests__/accessors.test.ts @@ -1,8 +1,8 @@ import { DeepPartial } from '@stoplight/types'; -import { OpenAPIObject } from 'openapi3-ts'; import { setSkipHashing } from '../../hash'; import { getSecurities as _getSecurities, OperationSecurities } from '../accessors'; +import { OpenAPIObject } from '../types'; setSkipHashing(true); diff --git a/src/oas3/__tests__/bundle.test.ts b/src/oas3/__tests__/bundle.test.ts index 48dd2cc6..2919321d 100644 --- a/src/oas3/__tests__/bundle.test.ts +++ b/src/oas3/__tests__/bundle.test.ts @@ -113,6 +113,7 @@ describe('bundleOas3Service', () => { servers: [], }, ], + webhooks: [], components: { callbacks: [], responses: [], @@ -401,6 +402,7 @@ describe('bundleOas3Service', () => { id: 'undefined', name: 'no-title', operations: [], + webhooks: [], version: '', extensions: {}, infoExtensions: {}, @@ -462,6 +464,7 @@ describe('bundleOas3Service', () => { id: 'undefined', name: 'no-title', operations: [], + webhooks: [], version: '', extensions: {}, infoExtensions: {}, @@ -707,6 +710,7 @@ describe('bundleOas3Service', () => { tags: [], }, ], + webhooks: [], }); }); @@ -770,6 +774,7 @@ describe('bundleOas3Service', () => { tags: [], }, ], + webhooks: [], }); }); @@ -878,6 +883,7 @@ describe('bundleOas3Service', () => { tags: [], }, ], + webhooks: [], components: { callbacks: [], cookie: [], diff --git a/src/oas3/__tests__/operation.test.ts b/src/oas3/__tests__/operation.test.ts index 0742d7ea..54634529 100644 --- a/src/oas3/__tests__/operation.test.ts +++ b/src/oas3/__tests__/operation.test.ts @@ -1,10 +1,10 @@ -import { OpenAPIObject } from 'openapi3-ts'; - import { setSkipHashing } from '../../hash'; +import { OPERATION_CONFIG } from '../../oas/operation'; import { transformOas3Operation as _transformOas3Operation, transformOas3Operations as _transformOas3Operations, } from '../operation'; +import { OpenAPIObject } from '../types'; setSkipHashing(true); @@ -32,8 +32,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', + config: OPERATION_CONFIG, document, }), ).toHaveProperty('deprecated', true); @@ -62,7 +63,8 @@ describe('transformOas3Operation', () => { }; const result = transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', + config: OPERATION_CONFIG, method: 'delete', document, }); @@ -143,7 +145,8 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', + config: OPERATION_CONFIG, method: 'get', document, }), @@ -181,7 +184,8 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', + config: OPERATION_CONFIG, method: 'get', document, }), @@ -218,7 +222,8 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', + config: OPERATION_CONFIG, method: 'get', document, }), @@ -248,7 +253,8 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', + config: OPERATION_CONFIG, method: 'get', document, }), @@ -291,7 +297,8 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', + config: OPERATION_CONFIG, method: 'get', document, }), @@ -330,8 +337,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', + config: OPERATION_CONFIG, document, }), ).toHaveProperty('servers', []); @@ -361,8 +369,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', + config: OPERATION_CONFIG, document, }), ).toHaveProperty('servers', [ @@ -397,8 +406,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', + config: OPERATION_CONFIG, document, }), ).toMatchSnapshot({ @@ -436,8 +446,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', + config: OPERATION_CONFIG, document, }), ).toHaveProperty('servers', []); @@ -467,8 +478,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', + config: OPERATION_CONFIG, document, }), ).toHaveProperty('servers', [ @@ -503,8 +515,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', + config: OPERATION_CONFIG, document, }), ).toMatchSnapshot({ @@ -542,8 +555,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', + config: OPERATION_CONFIG, document, }), ).toHaveProperty('servers', []); @@ -573,8 +587,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', + config: OPERATION_CONFIG, document, }), ).toHaveProperty('servers', [ @@ -629,8 +644,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/subscribe', + name: '/subscribe', method: 'post', + config: OPERATION_CONFIG, document, }), ).toMatchSnapshot({ @@ -687,8 +703,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', + config: OPERATION_CONFIG, document, }), ).toStrictEqual({ @@ -770,8 +787,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', + config: OPERATION_CONFIG, document, }), ).toStrictEqual({ @@ -858,7 +876,9 @@ describe('transformOas3Operation', () => { ], }; - expect(transformOas3Operation({ document, path: '/pets', method: 'get' }).servers).toEqual([ + expect( + transformOas3Operation({ document, name: '/pets', method: 'get', config: OPERATION_CONFIG }).servers, + ).toEqual([ { id: expect.any(String), description: 'Sample Petstore Server Https', @@ -960,8 +980,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/pet', + name: '/pet', method: 'get', + config: OPERATION_CONFIG, document, }), ).toStrictEqual({ @@ -1102,8 +1123,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/pets', + name: '/pets', method: 'post', + config: OPERATION_CONFIG, document, }), ).toHaveProperty('request.body', { @@ -1169,8 +1191,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/pets', + name: '/pets', method: 'post', + config: OPERATION_CONFIG, document, }), ).toHaveProperty('request.body', { @@ -1223,8 +1246,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation.bind(null, { - path: '/pets', + name: '/pets', method: 'post', + config: OPERATION_CONFIG, document, }), ).not.toThrow(); @@ -1253,7 +1277,9 @@ describe('transformOas3Operation', () => { }, }; - expect(transformOas3Operation({ document, path: '/hello/test', method: 'get' })).toStrictEqual({ + expect( + transformOas3Operation({ document, name: '/hello/test', method: 'get', config: OPERATION_CONFIG }), + ).toStrictEqual({ id: 'http_operation-undefined-get-/hello/test', iid: 'get-test', method: 'get', @@ -1365,8 +1391,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', + config: OPERATION_CONFIG, document, }), ).toEqual( @@ -1394,8 +1421,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/pet', + name: '/pet', method: 'get', + config: OPERATION_CONFIG, document, }), ).toEqual( @@ -1485,8 +1513,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/users/{userId}', + name: '/users/{userId}', method: 'get', + config: OPERATION_CONFIG, document, }), ).toHaveProperty('responses', [ @@ -1529,8 +1558,9 @@ describe('transformOas3Operation', () => { expect( transformOas3Operation({ - path: '/subscribe', + name: '/subscribe', method: 'connect', + config: OPERATION_CONFIG, document, }), ).toHaveProperty('request.body', { diff --git a/src/oas3/__tests__/service.test.ts b/src/oas3/__tests__/service.test.ts index 0508f6ac..c9a0652a 100644 --- a/src/oas3/__tests__/service.test.ts +++ b/src/oas3/__tests__/service.test.ts @@ -1,7 +1,6 @@ -import { OpenAPIObject } from 'openapi3-ts'; - import { setSkipHashing } from '../../hash'; import { transformOas3Service as _transformOas3Service } from '../service'; +import { OpenAPIObject } from '../types'; setSkipHashing(true); diff --git a/src/oas3/operation.ts b/src/oas3/operation.ts index 0f41d772..df8b2b06 100644 --- a/src/oas3/operation.ts +++ b/src/oas3/operation.ts @@ -1,11 +1,16 @@ -import { DeepPartial, IHttpOperation } from '@stoplight/types'; +import { DeepPartial, IHttpEndpointOperation, IHttpOperation, IHttpWebhookOperation } from '@stoplight/types'; import pickBy = require('lodash.pickby'); import type { OpenAPIObject } from 'openapi3-ts'; import { isNonNullable } from '../guards'; -import { transformOasOperation, transformOasOperations } from '../oas'; +import { + OPERATION_CONFIG, + transformOasEndpointOperation, + transformOasEndpointOperations, + WEBHOOK_CONFIG, +} from '../oas'; import { createContext } from '../oas/context'; -import type { Oas3HttpOperationTransformer } from '../oas/types'; +import type { Oas3HttpEndpointOperationTransformer } from '../oas/types'; import { Fragment, TransformerContext } from '../types'; import { translateToCallbacks } from './transformers/callbacks'; import { translateToRequest } from './transformers/request'; @@ -17,26 +22,46 @@ export function transformOas3Operations, ): IHttpOperation[] { - return transformOasOperations(document, transformOas3Operation, void 0, ctx); + return transformOasEndpointOperations( + document, + transformOas3Operation, + OPERATION_CONFIG, + void 0, + ctx, + ) as unknown as IHttpOperation[]; } -export const transformOas3Operation: Oas3HttpOperationTransformer = ({ +export function transformOas3WebhookOperations>( + document: T, + ctx?: TransformerContext, +): IHttpWebhookOperation[] { + return transformOasEndpointOperations( + document, + transformOas3Operation, + WEBHOOK_CONFIG, + void 0, + ctx, + ) as unknown as IHttpWebhookOperation[]; +} + +export const transformOas3Operation: Oas3HttpEndpointOperationTransformer = ({ document: _document, - path, + name, method, + config, ctx = createContext(_document), }) => { - const httpOperation = transformOasOperation.call(ctx, path, method); - const pathObj = ctx.maybeResolveLocalRef(ctx.document.paths![path]) as Fragment; - const operation = ctx.maybeResolveLocalRef(pathObj[method]) as Fragment; + const httpOperation = transformOasEndpointOperation.call(ctx, config, name, method); + const parentObj = ctx.maybeResolveLocalRef(ctx.document[config.documentProp]![name]) as Fragment; + const operation = ctx.maybeResolveLocalRef(parentObj[method]) as Fragment; return { ...httpOperation, responses: translateToResponses.call(ctx, operation.responses), - request: translateToRequest.call(ctx, pathObj, operation), + request: translateToRequest.call(ctx, parentObj, operation), security: translateToSecurities.call(ctx, operation.security, 'requirement'), - servers: translateToServers.call(ctx, pathObj, operation), + servers: translateToServers.call(ctx, parentObj, operation), ...pickBy( { @@ -44,5 +69,5 @@ export const transformOas3Operation: Oas3HttpOperationTransformer = ({ }, isNonNullable, ), - } as unknown as IHttpOperation; + } as unknown as IHttpEndpointOperation; }; diff --git a/src/oas3/service.ts b/src/oas3/service.ts index 5e65bbbf..6e853479 100644 --- a/src/oas3/service.ts +++ b/src/oas3/service.ts @@ -1,5 +1,5 @@ import { isPlainObject } from '@stoplight/json'; -import type { HttpSecurityScheme, IHttpOperation, Optional } from '@stoplight/types'; +import type { HttpSecurityScheme, IHttpOperation, IHttpWebhookOperation, Optional } from '@stoplight/types'; import pickBy = require('lodash.pickby'); import { withContext } from '../context'; @@ -14,7 +14,7 @@ import { OasVersion } from '../oas/types'; import type { ArrayCallbackParameters } from '../types'; import { entries } from '../utils'; import { isSecurityScheme } from './guards'; -import { transformOas3Operations } from './operation'; +import { transformOas3Operations, transformOas3WebhookOperations } from './operation'; import { translateToCallbacks } from './transformers/callbacks'; import { translateToExample } from './transformers/examples'; import { translateToSharedParameters } from './transformers/parameters'; @@ -42,10 +42,12 @@ export const bundleOas3Service: Oas3HttpServiceBundle = ({ document: _document } }; const operations = transformOas3Operations(document, ctx) as unknown as IHttpOperation[]; + const webhooks = transformOas3WebhookOperations(document, ctx) as unknown as IHttpWebhookOperation[]; return { ...service, operations, + webhooks, components, }; }; diff --git a/src/oas3/transformers/__tests__/callbacks.test.ts b/src/oas3/transformers/__tests__/callbacks.test.ts index 6d3264bc..99b4ec16 100644 --- a/src/oas3/transformers/__tests__/callbacks.test.ts +++ b/src/oas3/transformers/__tests__/callbacks.test.ts @@ -1,7 +1,7 @@ import { DeepPartial } from '@stoplight/types'; -import { OpenAPIObject } from 'openapi3-ts'; import { createContext } from '../../../oas/context'; +import { OpenAPIObject } from '../../types'; import { translateToCallbacks as _translateToCallbacks } from '../callbacks'; const translateToCallbacks = (document: DeepPartial, callbacks: unknown) => diff --git a/src/oas3/transformers/__tests__/responses.test.ts b/src/oas3/transformers/__tests__/responses.test.ts index 707152a7..6c58b7d5 100644 --- a/src/oas3/transformers/__tests__/responses.test.ts +++ b/src/oas3/transformers/__tests__/responses.test.ts @@ -1,7 +1,7 @@ import type { DeepPartial } from '@stoplight/types'; -import { OpenAPIObject } from 'openapi3-ts'; import { createContext } from '../../../oas/context'; +import { OpenAPIObject } from '../../types'; import { translateToResponses as _translateToResponses } from '../responses'; const translateToResponses = (document: DeepPartial, responses: unknown) => diff --git a/src/oas3/transformers/__tests__/securities.test.ts b/src/oas3/transformers/__tests__/securities.test.ts index b9e660ed..604e6e7a 100644 --- a/src/oas3/transformers/__tests__/securities.test.ts +++ b/src/oas3/transformers/__tests__/securities.test.ts @@ -1,8 +1,8 @@ import { DeepPartial } from '@stoplight/types'; -import { OpenAPIObject } from 'openapi3-ts'; import { createContext } from '../../../oas/context'; import { OperationSecurities } from '../../accessors'; +import { OpenAPIObject } from '../../types'; import { translateToSecurities as _translateToSecurities } from '../securities'; const translateToSecurities = (document: DeepPartial, operationSecurities: OperationSecurities) => diff --git a/src/oas3/transformers/__tests__/servers.test.ts b/src/oas3/transformers/__tests__/servers.test.ts index 1d854e91..3438f0cb 100644 --- a/src/oas3/transformers/__tests__/servers.test.ts +++ b/src/oas3/transformers/__tests__/servers.test.ts @@ -1,7 +1,7 @@ import { DeepPartial } from '@stoplight/types'; -import { OpenAPIObject } from 'openapi3-ts'; import { createContext } from '../../../oas/context'; +import { OpenAPIObject } from '../../types'; import { translateToServers as _translateToServers } from '../servers'; const translateToServers = ( diff --git a/src/oas3/transformers/callbacks.ts b/src/oas3/transformers/callbacks.ts index cf6f9072..6a9c64f3 100644 --- a/src/oas3/transformers/callbacks.ts +++ b/src/oas3/transformers/callbacks.ts @@ -3,6 +3,7 @@ import type { OpenAPIObject } from 'openapi3-ts'; import { createContext } from '../../oas/context'; import { isReferenceObject } from '../../oas/guards'; +import { OPERATION_CONFIG } from '../../oas/operation'; import { entries } from '../../utils'; import { transformOas3Operation } from '../operation'; import type { Oas3TranslateFunction } from '../types'; @@ -35,11 +36,12 @@ export const translateToCallbacks: Oas3TranslateFunction< ...transformOas3Operation({ document, method, - path, + name: path, + config: OPERATION_CONFIG, ctx, }), key: callbackName, - }); + } as IHttpCallbackOperation); } } diff --git a/src/oas3/types.ts b/src/oas3/types.ts index c452b6b3..02cd1f95 100644 --- a/src/oas3/types.ts +++ b/src/oas3/types.ts @@ -1,5 +1,7 @@ import { DeepPartial } from '@stoplight/types'; -import { OpenAPIObject } from 'openapi3-ts'; +import { OpenAPIObject as _OpenAPIObject } from 'openapi3-ts'; +import { PathItemObject } from 'openapi3-ts/src/model/OpenApi'; +import { ISpecificationExtension } from 'openapi3-ts/src/model/SpecificationExtension'; import { TranslateFunction } from '../types'; @@ -8,3 +10,11 @@ export type Oas3TranslateFunction

; + +export interface OpenAPIObject extends _OpenAPIObject { + webhooks?: WebhooksObject; +} + +export interface WebhooksObject extends ISpecificationExtension { + [name: string]: PathItemObject; +} diff --git a/src/postman/types.ts b/src/postman/types.ts index 350c7555..7861cb6c 100644 --- a/src/postman/types.ts +++ b/src/postman/types.ts @@ -1,9 +1,13 @@ +import { IHttpOperation } from '@stoplight/types'; import type { CollectionDefinition } from 'postman-collection'; -import type { HttpOperationTransformer } from '../types'; +import type { HttpEndpointOperationTransformer } from '../types'; -export type PostmanCollectionHttpOperationTransformer = HttpOperationTransformer<{ - document: CollectionDefinition; - path: string; - method: string; -}>; +export type PostmanCollectionHttpOperationTransformer = HttpEndpointOperationTransformer< + { + document: CollectionDefinition; + path: string; + method: string; + }, + IHttpOperation +>; diff --git a/src/types.ts b/src/types.ts index 56581841..ac3a5999 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import type { IBundledHttpService, IHttpOperation, IHttpService } from '@stoplight/types'; +import type { IBundledHttpService, IHttpEndpointOperation, IHttpService } from '@stoplight/types'; import type { idGenerators } from './generators'; @@ -20,20 +20,33 @@ export type HttpServiceTransformer = (opts: T) => IHttpService; export type HttpServiceBundle = (opts: T) => IBundledHttpService; -export interface ITransformOperationOpts { +export type EndpointOperationConfig = + | { + type: 'operation'; + documentProp: 'paths'; + nameProp: 'path'; + } + | { + type: 'webhook'; + documentProp: 'webhooks'; + nameProp: 'name'; + }; + +export interface ITransformEndpointOperationOpts { document: T; - path: string; + name: string; method: string; + config: EndpointOperationConfig; ctx?: TransformerContext; } -export type HttpOperationTransformer = (opts: T) => IHttpOperation; +export type HttpEndpointOperationTransformer = (opts: T) => TEndpoint; export type HttpSecurityKind = 'requirement' | 'scheme'; export type ArrayCallbackParameters = [T, number, T[]]; -export type AvailableContext = 'service' | 'path' | 'operation' | 'callback'; +export type AvailableContext = 'service' | 'path' | 'operation' | 'callback' | 'webhook' | 'webhookName'; export type References = Record; diff --git a/yarn.lock b/yarn.lock index 4fbced09..34d32066 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1483,10 +1483,10 @@ resolved "https://registry.yarnpkg.com/@stoplight/test-utils/-/test-utils-0.0.1.tgz#b0d38c8a0abebda2dacbc2aa6a4f9bec44878e7b" integrity sha512-Cj3waLFR9bYLG8yvgkjXMxOfiVdewRyrKdH5RQpttVNfKj5UF+mElofPcHn/UfiOuxK3t5rbsdMfS26LK5tdQg== -"@stoplight/types@14.0.0": - version "14.0.0" - resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-14.0.0.tgz#f444490664c2c16d5f06265fcbac8d94a33481e8" - integrity sha512-w7Ejau6TaB7RqR0vWzGJSdmgLEYD2frjgbHPZoxgGQwAq/R8Qh/D9p9Bl9JFdii+YTL5xoDjyX0c1WDRlbMV8g== +"@stoplight/types@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-14.1.0.tgz#36b04488acc1d8ab5bb712416f50d3bc99610b34" + integrity sha512-fL8Nzw03+diALw91xHEHA5Q0WCGeW9WpPgZQjodNUWogAgJ56aJs03P9YzsQ1J6fT7/XjDqHMgn7/RlsBzB/SQ== dependencies: "@types/json-schema" "^7.0.4" utility-types "^3.10.0"