From 087a11f149059ccd80ab50a997727399be9a8a97 Mon Sep 17 00:00:00 2001 From: Viacheslav Turovskyi Date: Wed, 3 Jul 2024 02:51:41 +0000 Subject: [PATCH 1/3] fix: remove forceful normalization of YAML to JSON --- .changeset/smooth-pumas-deliver.md | 5 +++++ packages/parser/src/parse.ts | 18 ++++-------------- 2 files changed, 9 insertions(+), 14 deletions(-) create mode 100644 .changeset/smooth-pumas-deliver.md diff --git a/.changeset/smooth-pumas-deliver.md b/.changeset/smooth-pumas-deliver.md new file mode 100644 index 000000000..58c83ea24 --- /dev/null +++ b/.changeset/smooth-pumas-deliver.md @@ -0,0 +1,5 @@ +--- +"@asyncapi/parser": patch +--- + +fix: remove forceful normalization of YAML to JSON diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts index 9688a4314..0d3c40c05 100644 --- a/packages/parser/src/parse.ts +++ b/packages/parser/src/parse.ts @@ -38,24 +38,14 @@ const defaultOptions: ParseOptions = { validateOptions: {}, __unstable: {}, }; -import yaml from 'js-yaml'; + export async function parse(parser: Parser, spectral: Spectral, asyncapi: Input, options: ParseOptions = {}): Promise { let spectralDocument: Document | undefined; try { options = mergePatch(defaultOptions, options); - // Normalize input to always be JSON - let loadedObj; - if (typeof asyncapi === 'string') { - try { - loadedObj = yaml.load(asyncapi); - } catch (e) { - loadedObj = JSON.parse(asyncapi); - } - } else { - loadedObj = asyncapi; - } - const { validated, diagnostics, extras } = await validate(parser, spectral, loadedObj, { ...options.validateOptions, source: options.source, __unstable: options.__unstable }); + + const { validated, diagnostics, extras } = await validate(parser, spectral, asyncapi, { ...options.validateOptions, source: options.source, __unstable: options.__unstable }); if (validated === undefined) { return { document: undefined, @@ -72,7 +62,7 @@ export async function parse(parser: Parser, spectral: Spectral, asyncapi: Input, // Apply unique ids which are used as part of iterating between channels <-> operations <-> messages applyUniqueIds(validatedDoc); - const detailed = createDetailedAsyncAPI(validatedDoc, loadedObj as DetailedAsyncAPI['input'], options.source); + const detailed = createDetailedAsyncAPI(validatedDoc, asyncapi as DetailedAsyncAPI['input'], options.source); const document = createAsyncAPIDocument(detailed); setExtension(xParserSpecParsed, true, document); setExtension(xParserApiVersion, ParserAPIVersion, document); From ef477746654e67b77970c9a36b8e5913b0055944 Mon Sep 17 00:00:00 2001 From: Viacheslav Turovskyi Date: Wed, 3 Jul 2024 02:51:41 +0000 Subject: [PATCH 2/3] add comment and unit tests --- packages/parser/src/parse.ts | 9 +- packages/parser/test/parse.spec.ts | 943 +++++++++++++++++++++++++++++ 2 files changed, 951 insertions(+), 1 deletion(-) diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts index 0d3c40c05..f966a0ff7 100644 --- a/packages/parser/src/parse.ts +++ b/packages/parser/src/parse.ts @@ -44,7 +44,14 @@ export async function parse(parser: Parser, spectral: Spectral, asyncapi: Input, try { options = mergePatch(defaultOptions, options); - + + // `./src/validate.ts` enforces 'string' type on both YAML and JSON later in + // code, and parses them both using the same `@stoplight/yaml`, so forceful + // normalization of YAML to JSON here has no practical application. It only + // causes `range` to be reported incorrectly in `diagnostics` by misleading + // `Parser` into thinking it's dealing with JSON instead of YAML, creating + // the bug described in https://github.com/asyncapi/parser-js/issues/936 + const { validated, diagnostics, extras } = await validate(parser, spectral, asyncapi, { ...options.validateOptions, source: options.source, __unstable: options.__unstable }); if (validated === undefined) { return { diff --git a/packages/parser/test/parse.spec.ts b/packages/parser/test/parse.spec.ts index d7cf4fde7..037698b09 100644 --- a/packages/parser/test/parse.spec.ts +++ b/packages/parser/test/parse.spec.ts @@ -277,4 +277,947 @@ describe('parse()', function() { expect(filteredDiagnostics.length).toEqual(1); expect(filteredDiagnostics[0].message).toEqual('\'#components/messages/message1\' JSON pointer is invalid'); }); + + it("should parse valid v3 YAML document in 'string' format", async function () { + const documentRaw = ` + asyncapi: 3.0.0 + info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups + channels: + userSignedup: + address: user/signedup + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' + operations: + sendUserSignedup: + action: send + channel: + $ref: '#/channels/userSignedup' + messages: + - $ref: '#/channels/userSignedup/messages/UserSignedUp' + components: + messages: + UserSignedUp: + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user` + + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeInstanceOf(AsyncAPIDocumentV3); + expect(diagnostics.length === 0).toEqual(true); + }); + + it("should parse valid v3 JSON document in JSON format", async function () { + const documentRaw = { + "asyncapi": "3.0.0", + "info": { + "title": "Account Service", + "version": "1.0.0", + "description": "This service is in charge of processing user signups" + }, + "channels": { + "userSignedup": { + "address": "user/signedup", + "messages": { + "UserSignedUp": { + "$ref": "#/components/messages/UserSignedUp" + } + } + } + }, + "operations": { + "sendUserSignedup": { + "action": "send", + "channel": { + "$ref": "#/channels/userSignedup" + }, + "messages": [ + { + "$ref": "#/channels/userSignedup/messages/UserSignedUp" + } + ] + } + }, + "components": { + "messages": { + "UserSignedUp": { + "payload": { + "type": "object", + "properties": { + "displayName": { + "type": "string", + "description": "Name of the user" + }, + "email": { + "type": "string", + "format": "email", + "description": "Email of the user" + } + } + } + } + } + } + } + + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeInstanceOf(AsyncAPIDocumentV3); + expect(diagnostics.length === 0).toEqual(true); + }); + + it("should parse valid v3 JSON document after JSON.stringify()", async function () { + const documentRaw = "{\n \"asyncapi\": \"3.0.0\",\n \"info\": {\n \"title\": \"Account Service\",\n \"version\": \"1.0.0\",\n \"description\": \"This service is in charge of processing user signups\"\n },\n \"channels\": {\n \"userSignedup\": {\n \"address\": \"user/signedup\",\n \"messages\": {\n \"UserSignedUp\": {\n \"$ref\": \"#/components/messages/UserSignedUp\"\n }\n }\n }\n },\n \"operations\": {\n \"sendUserSignedup\": {\n \"action\": \"send\",\n \"channel\": {\n \"$ref\": \"#/channels/userSignedup\"\n },\n \"messages\": [\n {\n \"$ref\": \"#/channels/userSignedup/messages/UserSignedUp\"\n }\n ]\n }\n },\n \"components\": {\n \"messages\": {\n \"UserSignedUp\": {\n \"payload\": {\n \"type\": \"object\",\n \"properties\": {\n \"displayName\": {\n \"type\": \"string\",\n \"description\": \"Name of the user\"\n },\n \"email\": {\n \"type\": \"string\",\n \"format\": \"email\",\n \"description\": \"Email of the user\"\n }\n }\n }\n }\n }\n }\n}\n" + + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeInstanceOf(AsyncAPIDocumentV3); + expect(diagnostics.length === 0).toEqual(true); + }); + + it("should not parse invalid v3 YAML document and give error in line 153 (#936)", async function () { + const documentRaw = `asyncapi: 3.0.0 +info: + title: My Event-Driven API + version: 1.0.0 + description: This API provides real-time event streaming capabilities. + termsOfService: https://example.com/terms-of-service + contact: + name: Rohit + email: rohitwashere@asyncapi.com + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + tags: + - name: Events + description: APIs related to event streaming + - name: Authentication + description: APIs for authentication and authorization + externalDocs: + description: Additional documentation + url: https://example.com/docs +servers: + production: + host: rabbitmq.in.mycompany.com:5672 + pathname: /v1 + protocol: amqp + protocolVersion: "1.0" + description: Production RabbitMQ broker (uses the 'production' vhost). + title: Production Server + summary: Production environment server + security: + - type: http + scheme: bearer + tags: + - name: production + description: Production environment + externalDocs: + description: Additional documentation for the production server + url: https://example.com/docs/production + bindings: + amqp: + exchange: my-exchange + queue: my-queue + staging: + host: rabbitmq.in.mycompany.com:5672 + pathname: /v1 + protocol: amqp + protocolVersion: "1.0" + description: Staging RabbitMQ broker (uses the 'staging' vhost). + title: Staging Server + summary: Staging environment server + security: + - type: apiKey + in: user + description: Provide your API key as the user and leave the password empty. + tags: + - name: staging + description: Staging environment + externalDocs: + description: Additional documentation for the staging server + url: https://example.com/docs/staging + bindings: + amqp: + exchange: my-exchange + queue: my-queue +channels: + user: + address: 'users.{userId}' + title: Users channel + description: This channel is used to exchange messages about user events. + messages: + userSignedUp: + $ref: '#/components/messages/userSignedUp' + userCompletedOrder: + $ref: '#/components/messages/userCompletedOrder' + parameters: + userId: + $ref: '#/components/parameters/userId' + servers: + - $ref: '#/servers/production' + bindings: + amqp: + is: queue + queue: + exclusive: true + tags: + - name: user + description: User-related messages + externalDocs: + description: 'Find more info here' + url: 'https://example.com' + userSignupReply: + address: 'users.signup.reply' + description: Channel for user signup replies + messages: + userSignedUpReply: + summary: User signup reply message + payload: + type: object + properties: + status: + type: string + description: Status of the signup process + message: + type: string + description: Additional information + + +operations: + sendUserSignUp: + action: send + title: User sign up + summary: Action to sign a user up. + description: A longer description + channel: + $ref: '#/channels/user' + security: + - type: oauth2 + description: The oauth security descriptions + flows: + clientCredentials: + tokenUrl: 'https://example.com/api/oauth/dialog' + availableScopes: + 'subscribe:auth_revocations': Scope required for authorization revocation topic + scopes: + - 'subscribe:auth_revocations' + tags: + - name: user + - name: signup + - name: register + bindings: + amqp: + ack: false + messages: + - $ref: '#/channels/user/messages/userSignedUp' + reply: + address: + location: '$message.header#/replyTo' + channel: + $ref: '#/channels/userSignupReply' + messages: + - $ref: '#/channels/userSignupReply/messages/userSignedUpReply' + +components: + schemas: + Category: + type: object + properties: + id: + type: integer + format: int64 + AvroExample: + schemaFormat: application/vnd.apache.avro+json;version=1.9.0 + schema: + $ref: 'path/to/user-create.avsc/#UserCreate' + + servers: + development: + host: '{stage}.in.mycompany.com' + protocol: amqp + description: RabbitMQ broker + bindings: + $ref: '#/components/serverBindings/devAmqp' + variables: + stage: + $ref: '#/components/serverVariables/stage' + security: + - $ref: '#/components/securitySchemes/oauth' + + serverVariables: + stage: + default: demo + description: This value is assigned by the service provider in this example of 'mycompany.com' + + channels: + user: + address: 'users.{userId}' + title: Users channel + description: This channel is used to exchange messages about user events. + messages: + userSignedUp: + $ref: '#/components/messages/userSignUp' + parameters: + userId: + $ref: '#/components/parameters/userId' + servers: + - $ref: '#/components/servers/development' + bindings: + $ref: '#/components/channelBindings/user' + tags: + - $ref: '#/components/tags/user' + externalDocs: + $ref: '#/components/externalDocs/infoDocs' + + messages: + userSignUp: + summary: Action to sign a user up. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/Category' + correlationId: + $ref: '#/components/correlationIds/default' + bindings: + $ref: '#/components/messageBindings/user' + userSignedUp: + summary: User signed up event + contentType: application/json + payload: + type: object + properties: + userId: + type: string + description: The ID of the user + email: + type: string + description: The email of the user + userCompletedOrder: + summary: User completed order event + contentType: application/json + payload: + type: object + properties: + orderId: + type: string + description: The ID of the order + userId: + type: string + description: The ID of the user + amount: + type: number + description: The total amount of the order + + + parameters: + userId: + description: Id of the user. + + correlationIds: + default: + description: Default Correlation ID + location: $message.header#/correlationId + + operations: + sendUserSignUp: + action: send + title: User sign up + channel: + $ref: '#/channels/user' + bindings: + $ref: '#/components/operationBindings/sendUser' + traits: + - $ref: '#/components/operationTraits/binding' + reply: + $ref: '#/components/replies/signupReply' + + replies: + signupReply: + address: + $ref: '#/components/replyAddresses/signupReply' + channel: + $ref: '#/channels/userSignupReply' + + replyAddresses: + signupReply: + location: '$message.header#/replyTo' + + + securitySchemes: + oauth: + type: oauth2 + description: The oauth security descriptions + flows: + clientCredentials: + tokenUrl: 'https://example.com/api/oauth/dialog' + availableScopes: + 'subscribe:auth_revocations': Scope required for authorization revocation topic + scopes: + - 'subscribe:auth_revocations' + + operationTraits: + binding: + bindings: + amqp: + ack: false + + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + + tags: + user: + name: user + description: User-related messages + + externalDocs: + infoDocs: + url: https://example.com/docs + description: 'Find more info here' + + serverBindings: + devAmqp: + amqp: + exchange: my-exchange + queue: my-queue + + channelBindings: + user: + amqp: + is: queue + queue: + exclusive: true + + operationBindings: + sendUser: + amqp: + ack: false + + messageBindings: + user: + amqp: + contentEncoding: gzip + messageType: 'user.signup' + bindingVersion: '0.3.0'` + + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).not.toBeInstanceOf(AsyncAPIDocumentV3); + expect(diagnostics[0].range.start.line === 153).toEqual(true); + }); + + it("should not parse invalid v3 JSON document and give error in line 236 (#936)", async function () { + const documentRaw = { + "asyncapi": "3.0.0", + "info": { + "title": "My Event-Driven API", + "version": "1.0.0", + "description": "This API provides real-time event streaming capabilities.", + "termsOfService": "https://example.com/terms-of-service", + "contact": { + "name": "Rohit", + "email": "rohitwashere@asyncapi.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "tags": [ + { + "name": "Events", + "description": "APIs related to event streaming" + }, + { + "name": "Authentication", + "description": "APIs for authentication and authorization" + } + ], + "externalDocs": { + "description": "Additional documentation", + "url": "https://example.com/docs" + } + }, + "servers": { + "production": { + "host": "rabbitmq.in.mycompany.com:5672", + "pathname": "/v1", + "protocol": "amqp", + "protocolVersion": "1.0", + "description": "Production RabbitMQ broker (uses the `production` vhost).", + "title": "Production Server", + "summary": "Production environment server", + "security": [ + { + "type": "http", + "scheme": "bearer" + } + ], + "tags": [ + { + "name": "production", + "description": "Production environment" + } + ], + "externalDocs": { + "description": "Additional documentation for the production server", + "url": "https://example.com/docs/production" + }, + "bindings": { + "amqp": { + "exchange": "my-exchange", + "queue": "my-queue" + } + } + }, + "staging": { + "host": "rabbitmq.in.mycompany.com:5672", + "pathname": "/v1", + "protocol": "amqp", + "protocolVersion": "1.0", + "description": "Staging RabbitMQ broker (uses the `staging` vhost).", + "title": "Staging Server", + "summary": "Staging environment server", + "security": [ + { + "type": "apiKey", + "in": "user", + "description": "Provide your API key as the user and leave the password empty." + } + ], + "tags": [ + { + "name": "staging", + "description": "Staging environment" + } + ], + "externalDocs": { + "description": "Additional documentation for the staging server", + "url": "https://example.com/docs/staging" + }, + "bindings": { + "amqp": { + "exchange": "my-exchange", + "queue": "my-queue" + } + } + } + }, + "channels": { + "user": { + "address": "users.{userId}", + "title": "Users channel", + "description": "This channel is used to exchange messages about user events.", + "messages": { + "userSignedUp": { + "$ref": "#/components/messages/userSignedUp" + }, + "userCompletedOrder": { + "$ref": "#/components/messages/userCompletedOrder" + } + }, + "parameters": { + "userId": { + "$ref": "#/components/parameters/userId" + } + }, + "servers": [ + { + "$ref": "#/servers/production" + } + ], + "bindings": { + "amqp": { + "is": "queue", + "queue": { + "exclusive": true + } + } + }, + "tags": [ + { + "name": "user", + "description": "User-related messages" + } + ], + "externalDocs": { + "description": "Find more info here", + "url": "https://example.com" + } + }, + "userSignupReply": { + "address": "users.signup.reply", + "description": "Channel for user signup replies", + "messages": { + "userSignedUpReply": { + "summary": "User signup reply message", + "payload": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Status of the signup process" + }, + "message": { + "type": "string", + "description": "Additional information" + } + } + } + } + } + } + }, + "operations": { + "sendUserSignUp": { + "action": "send", + "title": "User sign up", + "summary": "Action to sign a user up.", + "description": "A longer description", + "channel": { + "$ref": "#/channels/user" + }, + "security": [ + { + "type": "oauth2", + "description": "The oauth security descriptions", + "flows": { + "clientCredentials": { + "tokenUrl": "https://example.com/api/oauth/dialog", + "availableScopes": { + "subscribe:auth_revocations": "Scope required for authorization revocation topic" + } + } + }, + "scopes": [ + "subscribe:auth_revocations" + ] + } + ], + "tags": [ + { + "name": "user" + }, + { + "name": "signup" + }, + { + "name": "register" + } + ], + "bindings": { + "amqp": { + "ack": false + } + }, + "messages": [ + { + "$ref": "#/channels/user/messages/userSignedUp" + } + ], + "reply": { + "address": { + "location": "$message.header#/replyTo" + }, + "channel": { + "$ref": "#/channels/userSignupReply" + }, + "messages": [ + { + "$ref": "#/channels/userSignupReply/messages/userSignedUpReply" + } + ] + } + } + }, + "components": { + "schemas": { + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + } + }, + "AvroExample": { + "schemaFormat": "application/vnd.apache.avro+json;version=1.9.0", + "schema": { + "$ref": "path/to/user-create.avsc/#UserCreate" + } + } + }, + "servers": { + "development": { + "host": "{stage}.in.mycompany.com", + "protocol": "amqp", + "description": "RabbitMQ broker", + "bindings": { + "$ref": "#/components/serverBindings/devAmqp" + }, + "variables": { + "stage": { + "$ref": "#/components/serverVariables/stage" + } + }, + "security": [ + { + "$ref": "#/components/securitySchemes/oauth" + } + ] + } + }, + "serverVariables": { + "stage": { + "default": "demo", + "description": "This value is assigned by the service provider in this example of `mycompany.com`" + } + }, + "channels": { + "user": { + "address": "users.{userId}", + "title": "Users channel", + "description": "This channel is used to exchange messages about user events.", + "messages": { + "userSignedUp": { + "$ref": "#/components/messages/userSignUp" + } + }, + "parameters": { + "userId": { + "$ref": "#/components/parameters/userId" + } + }, + "servers": [ + { + "$ref": "#/components/servers/development" + } + ], + "bindings": { + "$ref": "#/components/channelBindings/user" + }, + "tags": [ + { + "$ref": "#/components/tags/user" + } + ], + "externalDocs": { + "$ref": "#/components/externalDocs/infoDocs" + } + } + }, + "messages": { + "userSignUp": { + "summary": "Action to sign a user up.", + "traits": [ + { + "$ref": "#/components/messageTraits/commonHeaders" + } + ], + "payload": { + "$ref": "#/components/schemas/Category" + }, + "correlationId": { + "$ref": "#/components/correlationIds/default" + }, + "bindings": { + "$ref": "#/components/messageBindings/user" + } + }, + "userSignedUp": { + "summary": "User signed up event", + "contentType": "application/json", + "payload": { + "type": "object", + "properties": { + "userId": { + "type": "string", + "description": "The ID of the user" + }, + "email": { + "type": "string", + "description": "The email of the user" + } + } + } + }, + "userCompletedOrder": { + "summary": "User completed order event", + "contentType": "application/json", + "payload": { + "type": "object", + "properties": { + "orderId": { + "type": "string", + "description": "The ID of the order" + }, + "userId": { + "type": "string", + "description": "The ID of the user" + }, + "amount": { + "type": "number", + "description": "The total amount of the order" + } + } + } + } + }, + "parameters": { + "userId": { + "description": "Id of the user." + } + }, + "correlationIds": { + "default": { + "description": "Default Correlation ID", + "location": "$message.header#/correlationId" + } + }, + "operations": { + "sendUserSignUp": { + "action": "send", + "title": "User sign up", + "channel": { + "$ref": "#/channels/user" + }, + "bindings": { + "$ref": "#/components/operationBindings/sendUser" + }, + "traits": [ + { + "$ref": "#/components/operationTraits/binding" + } + ], + "reply": { + "$ref": "#/components/replies/signupReply" + } + } + }, + "replies": { + "signupReply": { + "address": { + "$ref": "#/components/replyAddresses/signupReply" + }, + "channel": { + "$ref": "#/channels/userSignupReply" + } + } + }, + "replyAddresses": { + "signupReply": { + "location": "$message.header#/replyTo" + } + }, + "securitySchemes": { + "oauth": { + "type": "oauth2", + "description": "The oauth security descriptions", + "flows": { + "clientCredentials": { + "tokenUrl": "https://example.com/api/oauth/dialog", + "availableScopes": { + "subscribe:auth_revocations": "Scope required for authorization revocation topic" + } + } + }, + "scopes": [ + "subscribe:auth_revocations" + ] + } + }, + "operationTraits": { + "binding": { + "bindings": { + "amqp": { + "ack": false + } + } + } + }, + "messageTraits": { + "commonHeaders": { + "headers": { + "type": "object", + "properties": { + "my-app-header": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + } + } + } + }, + "tags": { + "user": { + "name": "user", + "description": "User-related messages" + } + }, + "externalDocs": { + "infoDocs": { + "url": "https://example.com/docs", + "description": "Find more info here" + } + }, + "serverBindings": { + "devAmqp": { + "amqp": { + "exchange": "my-exchange", + "queue": "my-queue" + } + } + }, + "channelBindings": { + "user": { + "amqp": { + "is": "queue", + "queue": { + "exclusive": true + } + } + } + }, + "operationBindings": { + "sendUser": { + "amqp": { + "ack": false + } + } + }, + "messageBindings": { + "user": { + "amqp": { + "contentEncoding": "gzip", + "messageType": "user.signup", + "bindingVersion": "0.3.0" + } + } + } + } + } + + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).not.toBeInstanceOf(AsyncAPIDocumentV3); + expect(diagnostics[0].range.start.line === 236).toEqual(true); + }); }); From 22bd2e509e250dd3e893edf7719d761b0a75f977 Mon Sep 17 00:00:00 2001 From: Viacheslav Turovskyi Date: Wed, 3 Jul 2024 02:51:41 +0000 Subject: [PATCH 3/3] fix linting errors --- packages/parser/test/parse.spec.ts | 1127 +++++++++++++++------------- 1 file changed, 590 insertions(+), 537 deletions(-) diff --git a/packages/parser/test/parse.spec.ts b/packages/parser/test/parse.spec.ts index 037698b09..32b0b93b2 100644 --- a/packages/parser/test/parse.spec.ts +++ b/packages/parser/test/parse.spec.ts @@ -1,42 +1,46 @@ import { Document } from '@stoplight/spectral-core'; -import { AsyncAPIDocumentV2, AsyncAPIDocumentV3, ParserAPIVersion } from '../src/models'; +import { + AsyncAPIDocumentV2, + AsyncAPIDocumentV3, + ParserAPIVersion, +} from '../src/models'; import { Parser } from '../src/parser'; import { xParserApiVersion } from '../src/constants'; -describe('parse()', function() { +describe('parse()', function () { const parser = new Parser(); - it('should parse valid document', async function() { + it('should parse valid document', async function () { const documentRaw = { asyncapi: '2.0.0', info: { title: 'Valid AsyncApi document', version: '1.0', }, - channels: {} + channels: {}, }; const { document, diagnostics } = await parser.parse(documentRaw); - + expect(document).toBeInstanceOf(AsyncAPIDocumentV2); expect(diagnostics.length > 0).toEqual(true); }); - it('should not parse valid v3 document', async function() { + it('should not parse valid v3 document', async function () { const documentRaw = { asyncapi: '3.0.0', info: { title: 'Valid AsyncApi document', version: '1.0', }, - channels: {} + channels: {}, }; const { document, diagnostics } = await parser.parse(documentRaw); expect(document).toBeInstanceOf(AsyncAPIDocumentV3); expect(diagnostics.length === 0).toEqual(true); }); - it('should parse invalid document', async function() { + it('should parse invalid document', async function () { const documentRaw = { asyncapi: '2.0.0', info: { @@ -45,12 +49,12 @@ describe('parse()', function() { }, }; const { document, diagnostics } = await parser.parse(documentRaw); - + expect(document).toEqual(undefined); expect(diagnostics.length > 0).toEqual(true); }); - it('should parse invalid v3 document', async function() { + it('should parse invalid v3 document', async function () { const documentRaw = { asyncapi: '3.0.0', not_a_valid_info_object: { @@ -59,43 +63,45 @@ describe('parse()', function() { }, }; const { document, diagnostics } = await parser.parse(documentRaw); - + expect(document === undefined).toEqual(true); expect(diagnostics.length > 0).toEqual(true); }); - it('should return extras', async function() { + it('should return extras', async function () { const documentRaw = { asyncapi: '2.0.0', info: { title: 'Valid AsyncApi document', version: '1.0', }, - channels: {} + channels: {}, }; const { document, diagnostics, extras } = await parser.parse(documentRaw); - + expect(document).toBeInstanceOf(AsyncAPIDocumentV2); expect(extras?.document).toBeInstanceOf(Document); expect(diagnostics.length > 0).toEqual(true); }); - it('should assign x-parser-api-version extension to the 1 value', async function() { + it('should assign x-parser-api-version extension to the 1 value', async function () { const documentRaw = { asyncapi: '2.0.0', info: { title: 'Valid AsyncApi document', version: '1.0', }, - channels: {} + channels: {}, }; const { document } = await parser.parse(documentRaw); - + expect(document).toBeInstanceOf(AsyncAPIDocumentV2); - expect(document?.extensions().get(xParserApiVersion)?.value()).toEqual(ParserAPIVersion); + expect(document?.extensions().get(xParserApiVersion)?.value()).toEqual( + ParserAPIVersion, + ); }); - it('should preserve references', async function() { + it('should preserve references', async function () { const documentRaw = { asyncapi: '2.0.0', info: { @@ -107,41 +113,56 @@ describe('parse()', function() { publish: { operationId: 'publishOperation', message: { - $ref: '#/components/messages/message' - } + $ref: '#/components/messages/message', + }, }, subscribe: { operationId: 'subscribeOperation', message: { - $ref: '#/components/messages/message' - } - } - } + $ref: '#/components/messages/message', + }, + }, + }, }, components: { messages: { message: { payload: { type: 'object', - } - } - } - } + }, + }, + }, + }, }; const { document } = await parser.parse(documentRaw); - - const publishMessage = document?.channels().get('channel')?.operations().get('publishOperation')?.messages()[0]; - const subscribeMessage = document?.channels().get('channel')?.operations().get('subscribeOperation')?.messages()[0]; - const componentsMessage = document?.components()?.messages()?.get('message'); + + const publishMessage = document + ?.channels() + .get('channel') + ?.operations() + .get('publishOperation') + ?.messages()[0]; + const subscribeMessage = document + ?.channels() + .get('channel') + ?.operations() + .get('subscribeOperation') + ?.messages()[0]; + const componentsMessage = document + ?.components() + ?.messages() + ?.get('message'); expect(publishMessage?.json() !== undefined).toEqual(true); expect(subscribeMessage?.json() !== undefined).toEqual(true); expect(componentsMessage?.json() !== undefined).toEqual(true); expect(componentsMessage?.json() === publishMessage?.json()).toEqual(true); - expect(componentsMessage?.json() === subscribeMessage?.json()).toEqual(true); + expect(componentsMessage?.json() === subscribeMessage?.json()).toEqual( + true, + ); expect(publishMessage?.json() === subscribeMessage?.json()).toEqual(true); }); - it('should parse circular references (in this same document)', async function() { + it('should parse circular references (in this same document)', async function () { const documentRaw = { asyncapi: '2.0.0', info: { @@ -159,24 +180,35 @@ describe('parse()', function() { type: 'string', }, circular: { - $ref: '#/channels/channel/publish/message/payload' - } - } - } - } - } - } - } + $ref: '#/channels/channel/publish/message/payload', + }, + }, + }, + }, + }, + }, + }, }; const { document } = await parser.parse(documentRaw); - const messagePayload = document?.channels().get('channel')?.operations().get('someId')?.messages()[0].payload(); + const messagePayload = document + ?.channels() + .get('channel') + ?.operations() + .get('someId') + ?.messages()[0] + .payload(); expect(messagePayload?.json() !== undefined).toEqual(true); - expect(messagePayload?.properties()?.['circular'].json() !== undefined).toEqual(true); - expect(messagePayload?.properties()?.['circular'].json() === messagePayload?.json()).toEqual(true); // expect that same reference + expect( + messagePayload?.properties()?.['circular'].json() !== undefined, + ).toEqual(true); + expect( + messagePayload?.properties()?.['circular'].json() === + messagePayload?.json(), + ).toEqual(true); // expect that same reference }); - it('should parse circular references (in external file)', async function() { + it('should parse circular references (in external file)', async function () { const documentRaw = { asyncapi: '2.0.0', info: { @@ -194,18 +226,26 @@ describe('parse()', function() { type: 'string', }, circular: { - $ref: './mocks/parse/circular-ref.yaml' - } - } - } - } - } - } - } + $ref: './mocks/parse/circular-ref.yaml', + }, + }, + }, + }, + }, + }, + }, }; - const { document } = await parser.parse(documentRaw, { source: __filename }); + const { document } = await parser.parse(documentRaw, { + source: __filename, + }); - const messagePayload = document?.channels().get('channel')?.operations().get('someId')?.messages()[0].payload(); + const messagePayload = document + ?.channels() + .get('channel') + ?.operations() + .get('someId') + ?.messages()[0] + .payload(); const circular = messagePayload?.properties()?.['circular']; const deepProperty = circular?.properties()?.['deepProperty']; const deepCircular = deepProperty?.properties()?.['circular']; @@ -214,7 +254,7 @@ describe('parse()', function() { expect(deepProperty?.json() === deepCircular?.json()).toEqual(true); // expect that same reference }); - it('should throw errors when references url does not exist (#224 issue)', async function() { + it('should throw errors when references url does not exist (#224 issue)', async function () { const documentRaw = { asyncapi: '2.0.0', info: { @@ -227,23 +267,27 @@ describe('parse()', function() { operationId: 'someId', message: { payload: { - $ref: 'http://hopefullu-it-does-not-exist.com/some-file.yaml#/components/schemas/schema' - } - } - } - } - } + $ref: 'http://hopefullu-it-does-not-exist.com/some-file.yaml#/components/schemas/schema', + }, + }, + }, + }, + }, }; const { document, diagnostics } = await parser.parse(documentRaw); - const filteredDiagnostics = diagnostics.filter(d => d.code === 'invalid-ref'); - + const filteredDiagnostics = diagnostics.filter( + (d) => d.code === 'invalid-ref', + ); + expect(document).toBeUndefined(); expect(diagnostics.length > 0).toEqual(true); expect(filteredDiagnostics.length).toEqual(1); - expect(filteredDiagnostics[0].message).toEqual('FetchError: request to http://hopefullu-it-does-not-exist.com/some-file.yaml failed, reason: getaddrinfo ENOTFOUND hopefullu-it-does-not-exist.com'); + expect(filteredDiagnostics[0].message).toEqual( + 'FetchError: request to http://hopefullu-it-does-not-exist.com/some-file.yaml failed, reason: getaddrinfo ENOTFOUND hopefullu-it-does-not-exist.com', + ); }); - it('should throw errors when local reference does not exist (#360 issue)', async function() { + it('should throw errors when local reference does not exist (#360 issue)', async function () { const documentRaw = { asyncapi: '2.0.0', info: { @@ -256,29 +300,33 @@ describe('parse()', function() { operationId: 'someId', message: { $ref: '#components/messages/message1', - } - } - } + }, + }, + }, }, components: { messages: { message1: { name: 'unusedMessage', title: 'This is most definityly a message.', - } - } - } + }, + }, + }, }; const { document, diagnostics } = await parser.parse(documentRaw); - const filteredDiagnostics = diagnostics.filter(d => d.code === 'invalid-ref'); - + const filteredDiagnostics = diagnostics.filter( + (d) => d.code === 'invalid-ref', + ); + expect(document).toBeUndefined(); expect(diagnostics.length > 0).toEqual(true); expect(filteredDiagnostics.length).toEqual(1); - expect(filteredDiagnostics[0].message).toEqual('\'#components/messages/message1\' JSON pointer is invalid'); + expect(filteredDiagnostics[0].message).toEqual( + '\'#components/messages/message1\' JSON pointer is invalid', + ); }); - - it("should parse valid v3 YAML document in 'string' format", async function () { + + it('should parse valid v3 YAML document in \'string\' format', async function () { const documentRaw = ` asyncapi: 3.0.0 info: @@ -310,83 +358,84 @@ describe('parse()', function() { email: type: string format: email - description: Email of the user` - + description: Email of the user`; + const { document, diagnostics } = await parser.parse(documentRaw); expect(document).toBeInstanceOf(AsyncAPIDocumentV3); expect(diagnostics.length === 0).toEqual(true); }); - - it("should parse valid v3 JSON document in JSON format", async function () { + + it('should parse valid v3 JSON document in JSON format', async function () { const documentRaw = { - "asyncapi": "3.0.0", - "info": { - "title": "Account Service", - "version": "1.0.0", - "description": "This service is in charge of processing user signups" + asyncapi: '3.0.0', + info: { + title: 'Account Service', + version: '1.0.0', + description: 'This service is in charge of processing user signups', }, - "channels": { - "userSignedup": { - "address": "user/signedup", - "messages": { - "UserSignedUp": { - "$ref": "#/components/messages/UserSignedUp" - } - } - } + channels: { + userSignedup: { + address: 'user/signedup', + messages: { + UserSignedUp: { + $ref: '#/components/messages/UserSignedUp', + }, + }, + }, }, - "operations": { - "sendUserSignedup": { - "action": "send", - "channel": { - "$ref": "#/channels/userSignedup" + operations: { + sendUserSignedup: { + action: 'send', + channel: { + $ref: '#/channels/userSignedup', }, - "messages": [ + messages: [ { - "$ref": "#/channels/userSignedup/messages/UserSignedUp" - } - ] - } + $ref: '#/channels/userSignedup/messages/UserSignedUp', + }, + ], + }, }, - "components": { - "messages": { - "UserSignedUp": { - "payload": { - "type": "object", - "properties": { - "displayName": { - "type": "string", - "description": "Name of the user" + components: { + messages: { + UserSignedUp: { + payload: { + type: 'object', + properties: { + displayName: { + type: 'string', + description: 'Name of the user', + }, + email: { + type: 'string', + format: 'email', + description: 'Email of the user', }, - "email": { - "type": "string", - "format": "email", - "description": "Email of the user" - } - } - } - } - } - } - } - + }, + }, + }, + }, + }, + }; + const { document, diagnostics } = await parser.parse(documentRaw); expect(document).toBeInstanceOf(AsyncAPIDocumentV3); expect(diagnostics.length === 0).toEqual(true); }); - - it("should parse valid v3 JSON document after JSON.stringify()", async function () { - const documentRaw = "{\n \"asyncapi\": \"3.0.0\",\n \"info\": {\n \"title\": \"Account Service\",\n \"version\": \"1.0.0\",\n \"description\": \"This service is in charge of processing user signups\"\n },\n \"channels\": {\n \"userSignedup\": {\n \"address\": \"user/signedup\",\n \"messages\": {\n \"UserSignedUp\": {\n \"$ref\": \"#/components/messages/UserSignedUp\"\n }\n }\n }\n },\n \"operations\": {\n \"sendUserSignedup\": {\n \"action\": \"send\",\n \"channel\": {\n \"$ref\": \"#/channels/userSignedup\"\n },\n \"messages\": [\n {\n \"$ref\": \"#/channels/userSignedup/messages/UserSignedUp\"\n }\n ]\n }\n },\n \"components\": {\n \"messages\": {\n \"UserSignedUp\": {\n \"payload\": {\n \"type\": \"object\",\n \"properties\": {\n \"displayName\": {\n \"type\": \"string\",\n \"description\": \"Name of the user\"\n },\n \"email\": {\n \"type\": \"string\",\n \"format\": \"email\",\n \"description\": \"Email of the user\"\n }\n }\n }\n }\n }\n }\n}\n" - + + it('should parse valid v3 JSON document after JSON.stringify()', async function () { + const documentRaw = + '{\n "asyncapi": "3.0.0",\n "info": {\n "title": "Account Service",\n "version": "1.0.0",\n "description": "This service is in charge of processing user signups"\n },\n "channels": {\n "userSignedup": {\n "address": "user/signedup",\n "messages": {\n "UserSignedUp": {\n "$ref": "#/components/messages/UserSignedUp"\n }\n }\n }\n },\n "operations": {\n "sendUserSignedup": {\n "action": "send",\n "channel": {\n "$ref": "#/channels/userSignedup"\n },\n "messages": [\n {\n "$ref": "#/channels/userSignedup/messages/UserSignedUp"\n }\n ]\n }\n },\n "components": {\n "messages": {\n "UserSignedUp": {\n "payload": {\n "type": "object",\n "properties": {\n "displayName": {\n "type": "string",\n "description": "Name of the user"\n },\n "email": {\n "type": "string",\n "format": "email",\n "description": "Email of the user"\n }\n }\n }\n }\n }\n }\n}\n'; + const { document, diagnostics } = await parser.parse(documentRaw); expect(document).toBeInstanceOf(AsyncAPIDocumentV3); expect(diagnostics.length === 0).toEqual(true); }); - - it("should not parse invalid v3 YAML document and give error in line 153 (#936)", async function () { + + it('should not parse invalid v3 YAML document and give error in line 153 (#936)', async function () { const documentRaw = `asyncapi: 3.0.0 info: title: My Event-Driven API @@ -715,506 +764,510 @@ components: amqp: contentEncoding: gzip messageType: 'user.signup' - bindingVersion: '0.3.0'` - + bindingVersion: '0.3.0'`; + const { document, diagnostics } = await parser.parse(documentRaw); expect(document).not.toBeInstanceOf(AsyncAPIDocumentV3); expect(diagnostics[0].range.start.line === 153).toEqual(true); }); - - it("should not parse invalid v3 JSON document and give error in line 236 (#936)", async function () { + + it('should not parse invalid v3 JSON document and give error in line 236 (#936)', async function () { const documentRaw = { - "asyncapi": "3.0.0", - "info": { - "title": "My Event-Driven API", - "version": "1.0.0", - "description": "This API provides real-time event streaming capabilities.", - "termsOfService": "https://example.com/terms-of-service", - "contact": { - "name": "Rohit", - "email": "rohitwashere@asyncapi.com" + asyncapi: '3.0.0', + info: { + title: 'My Event-Driven API', + version: '1.0.0', + description: + 'This API provides real-time event streaming capabilities.', + termsOfService: 'https://example.com/terms-of-service', + contact: { + name: 'Rohit', + email: 'rohitwashere@asyncapi.com', }, - "license": { - "name": "Apache 2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + license: { + name: 'Apache 2.0', + url: 'https://www.apache.org/licenses/LICENSE-2.0.html', }, - "tags": [ + tags: [ { - "name": "Events", - "description": "APIs related to event streaming" + name: 'Events', + description: 'APIs related to event streaming', }, { - "name": "Authentication", - "description": "APIs for authentication and authorization" - } + name: 'Authentication', + description: 'APIs for authentication and authorization', + }, ], - "externalDocs": { - "description": "Additional documentation", - "url": "https://example.com/docs" - } + externalDocs: { + description: 'Additional documentation', + url: 'https://example.com/docs', + }, }, - "servers": { - "production": { - "host": "rabbitmq.in.mycompany.com:5672", - "pathname": "/v1", - "protocol": "amqp", - "protocolVersion": "1.0", - "description": "Production RabbitMQ broker (uses the `production` vhost).", - "title": "Production Server", - "summary": "Production environment server", - "security": [ + servers: { + production: { + host: 'rabbitmq.in.mycompany.com:5672', + pathname: '/v1', + protocol: 'amqp', + protocolVersion: '1.0', + description: + 'Production RabbitMQ broker (uses the `production` vhost).', + title: 'Production Server', + summary: 'Production environment server', + security: [ { - "type": "http", - "scheme": "bearer" - } + type: 'http', + scheme: 'bearer', + }, ], - "tags": [ + tags: [ { - "name": "production", - "description": "Production environment" - } + name: 'production', + description: 'Production environment', + }, ], - "externalDocs": { - "description": "Additional documentation for the production server", - "url": "https://example.com/docs/production" + externalDocs: { + description: 'Additional documentation for the production server', + url: 'https://example.com/docs/production', + }, + bindings: { + amqp: { + exchange: 'my-exchange', + queue: 'my-queue', + }, }, - "bindings": { - "amqp": { - "exchange": "my-exchange", - "queue": "my-queue" - } - } }, - "staging": { - "host": "rabbitmq.in.mycompany.com:5672", - "pathname": "/v1", - "protocol": "amqp", - "protocolVersion": "1.0", - "description": "Staging RabbitMQ broker (uses the `staging` vhost).", - "title": "Staging Server", - "summary": "Staging environment server", - "security": [ + staging: { + host: 'rabbitmq.in.mycompany.com:5672', + pathname: '/v1', + protocol: 'amqp', + protocolVersion: '1.0', + description: 'Staging RabbitMQ broker (uses the `staging` vhost).', + title: 'Staging Server', + summary: 'Staging environment server', + security: [ { - "type": "apiKey", - "in": "user", - "description": "Provide your API key as the user and leave the password empty." - } + type: 'apiKey', + in: 'user', + description: + 'Provide your API key as the user and leave the password empty.', + }, ], - "tags": [ + tags: [ { - "name": "staging", - "description": "Staging environment" - } + name: 'staging', + description: 'Staging environment', + }, ], - "externalDocs": { - "description": "Additional documentation for the staging server", - "url": "https://example.com/docs/staging" + externalDocs: { + description: 'Additional documentation for the staging server', + url: 'https://example.com/docs/staging', }, - "bindings": { - "amqp": { - "exchange": "my-exchange", - "queue": "my-queue" - } - } - } + bindings: { + amqp: { + exchange: 'my-exchange', + queue: 'my-queue', + }, + }, + }, }, - "channels": { - "user": { - "address": "users.{userId}", - "title": "Users channel", - "description": "This channel is used to exchange messages about user events.", - "messages": { - "userSignedUp": { - "$ref": "#/components/messages/userSignedUp" + channels: { + user: { + address: 'users.{userId}', + title: 'Users channel', + description: + 'This channel is used to exchange messages about user events.', + messages: { + userSignedUp: { + $ref: '#/components/messages/userSignedUp', + }, + userCompletedOrder: { + $ref: '#/components/messages/userCompletedOrder', }, - "userCompletedOrder": { - "$ref": "#/components/messages/userCompletedOrder" - } }, - "parameters": { - "userId": { - "$ref": "#/components/parameters/userId" - } + parameters: { + userId: { + $ref: '#/components/parameters/userId', + }, }, - "servers": [ + servers: [ { - "$ref": "#/servers/production" - } + $ref: '#/servers/production', + }, ], - "bindings": { - "amqp": { - "is": "queue", - "queue": { - "exclusive": true - } - } + bindings: { + amqp: { + is: 'queue', + queue: { + exclusive: true, + }, + }, }, - "tags": [ + tags: [ { - "name": "user", - "description": "User-related messages" - } + name: 'user', + description: 'User-related messages', + }, ], - "externalDocs": { - "description": "Find more info here", - "url": "https://example.com" - } + externalDocs: { + description: 'Find more info here', + url: 'https://example.com', + }, }, - "userSignupReply": { - "address": "users.signup.reply", - "description": "Channel for user signup replies", - "messages": { - "userSignedUpReply": { - "summary": "User signup reply message", - "payload": { - "type": "object", - "properties": { - "status": { - "type": "string", - "description": "Status of the signup process" + userSignupReply: { + address: 'users.signup.reply', + description: 'Channel for user signup replies', + messages: { + userSignedUpReply: { + summary: 'User signup reply message', + payload: { + type: 'object', + properties: { + status: { + type: 'string', + description: 'Status of the signup process', }, - "message": { - "type": "string", - "description": "Additional information" - } - } - } - } - } - } + message: { + type: 'string', + description: 'Additional information', + }, + }, + }, + }, + }, + }, }, - "operations": { - "sendUserSignUp": { - "action": "send", - "title": "User sign up", - "summary": "Action to sign a user up.", - "description": "A longer description", - "channel": { - "$ref": "#/channels/user" + operations: { + sendUserSignUp: { + action: 'send', + title: 'User sign up', + summary: 'Action to sign a user up.', + description: 'A longer description', + channel: { + $ref: '#/channels/user', }, - "security": [ + security: [ { - "type": "oauth2", - "description": "The oauth security descriptions", - "flows": { - "clientCredentials": { - "tokenUrl": "https://example.com/api/oauth/dialog", - "availableScopes": { - "subscribe:auth_revocations": "Scope required for authorization revocation topic" - } - } + type: 'oauth2', + description: 'The oauth security descriptions', + flows: { + clientCredentials: { + tokenUrl: 'https://example.com/api/oauth/dialog', + availableScopes: { + 'subscribe:auth_revocations': + 'Scope required for authorization revocation topic', + }, + }, }, - "scopes": [ - "subscribe:auth_revocations" - ] - } + scopes: ['subscribe:auth_revocations'], + }, ], - "tags": [ + tags: [ { - "name": "user" + name: 'user', }, { - "name": "signup" + name: 'signup', }, { - "name": "register" - } + name: 'register', + }, ], - "bindings": { - "amqp": { - "ack": false - } + bindings: { + amqp: { + ack: false, + }, }, - "messages": [ + messages: [ { - "$ref": "#/channels/user/messages/userSignedUp" - } + $ref: '#/channels/user/messages/userSignedUp', + }, ], - "reply": { - "address": { - "location": "$message.header#/replyTo" + reply: { + address: { + location: '$message.header#/replyTo', }, - "channel": { - "$ref": "#/channels/userSignupReply" + channel: { + $ref: '#/channels/userSignupReply', }, - "messages": [ + messages: [ { - "$ref": "#/channels/userSignupReply/messages/userSignedUpReply" - } - ] - } - } + $ref: '#/channels/userSignupReply/messages/userSignedUpReply', + }, + ], + }, + }, }, - "components": { - "schemas": { - "Category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - } - } + components: { + schemas: { + Category: { + type: 'object', + properties: { + id: { + type: 'integer', + format: 'int64', + }, + }, + }, + AvroExample: { + schemaFormat: 'application/vnd.apache.avro+json;version=1.9.0', + schema: { + $ref: 'path/to/user-create.avsc/#UserCreate', + }, }, - "AvroExample": { - "schemaFormat": "application/vnd.apache.avro+json;version=1.9.0", - "schema": { - "$ref": "path/to/user-create.avsc/#UserCreate" - } - } }, - "servers": { - "development": { - "host": "{stage}.in.mycompany.com", - "protocol": "amqp", - "description": "RabbitMQ broker", - "bindings": { - "$ref": "#/components/serverBindings/devAmqp" + servers: { + development: { + host: '{stage}.in.mycompany.com', + protocol: 'amqp', + description: 'RabbitMQ broker', + bindings: { + $ref: '#/components/serverBindings/devAmqp', }, - "variables": { - "stage": { - "$ref": "#/components/serverVariables/stage" - } + variables: { + stage: { + $ref: '#/components/serverVariables/stage', + }, }, - "security": [ + security: [ { - "$ref": "#/components/securitySchemes/oauth" - } - ] - } + $ref: '#/components/securitySchemes/oauth', + }, + ], + }, }, - "serverVariables": { - "stage": { - "default": "demo", - "description": "This value is assigned by the service provider in this example of `mycompany.com`" - } + serverVariables: { + stage: { + default: 'demo', + description: + 'This value is assigned by the service provider in this example of `mycompany.com`', + }, }, - "channels": { - "user": { - "address": "users.{userId}", - "title": "Users channel", - "description": "This channel is used to exchange messages about user events.", - "messages": { - "userSignedUp": { - "$ref": "#/components/messages/userSignUp" - } + channels: { + user: { + address: 'users.{userId}', + title: 'Users channel', + description: + 'This channel is used to exchange messages about user events.', + messages: { + userSignedUp: { + $ref: '#/components/messages/userSignUp', + }, }, - "parameters": { - "userId": { - "$ref": "#/components/parameters/userId" - } + parameters: { + userId: { + $ref: '#/components/parameters/userId', + }, }, - "servers": [ + servers: [ { - "$ref": "#/components/servers/development" - } + $ref: '#/components/servers/development', + }, ], - "bindings": { - "$ref": "#/components/channelBindings/user" + bindings: { + $ref: '#/components/channelBindings/user', }, - "tags": [ + tags: [ { - "$ref": "#/components/tags/user" - } + $ref: '#/components/tags/user', + }, ], - "externalDocs": { - "$ref": "#/components/externalDocs/infoDocs" - } - } + externalDocs: { + $ref: '#/components/externalDocs/infoDocs', + }, + }, }, - "messages": { - "userSignUp": { - "summary": "Action to sign a user up.", - "traits": [ + messages: { + userSignUp: { + summary: 'Action to sign a user up.', + traits: [ { - "$ref": "#/components/messageTraits/commonHeaders" - } + $ref: '#/components/messageTraits/commonHeaders', + }, ], - "payload": { - "$ref": "#/components/schemas/Category" + payload: { + $ref: '#/components/schemas/Category', }, - "correlationId": { - "$ref": "#/components/correlationIds/default" + correlationId: { + $ref: '#/components/correlationIds/default', + }, + bindings: { + $ref: '#/components/messageBindings/user', }, - "bindings": { - "$ref": "#/components/messageBindings/user" - } }, - "userSignedUp": { - "summary": "User signed up event", - "contentType": "application/json", - "payload": { - "type": "object", - "properties": { - "userId": { - "type": "string", - "description": "The ID of the user" + userSignedUp: { + summary: 'User signed up event', + contentType: 'application/json', + payload: { + type: 'object', + properties: { + userId: { + type: 'string', + description: 'The ID of the user', + }, + email: { + type: 'string', + description: 'The email of the user', }, - "email": { - "type": "string", - "description": "The email of the user" - } - } - } + }, + }, }, - "userCompletedOrder": { - "summary": "User completed order event", - "contentType": "application/json", - "payload": { - "type": "object", - "properties": { - "orderId": { - "type": "string", - "description": "The ID of the order" + userCompletedOrder: { + summary: 'User completed order event', + contentType: 'application/json', + payload: { + type: 'object', + properties: { + orderId: { + type: 'string', + description: 'The ID of the order', }, - "userId": { - "type": "string", - "description": "The ID of the user" + userId: { + type: 'string', + description: 'The ID of the user', }, - "amount": { - "type": "number", - "description": "The total amount of the order" - } - } - } - } + amount: { + type: 'number', + description: 'The total amount of the order', + }, + }, + }, + }, }, - "parameters": { - "userId": { - "description": "Id of the user." - } + parameters: { + userId: { + description: 'Id of the user.', + }, }, - "correlationIds": { - "default": { - "description": "Default Correlation ID", - "location": "$message.header#/correlationId" - } + correlationIds: { + default: { + description: 'Default Correlation ID', + location: '$message.header#/correlationId', + }, }, - "operations": { - "sendUserSignUp": { - "action": "send", - "title": "User sign up", - "channel": { - "$ref": "#/channels/user" + operations: { + sendUserSignUp: { + action: 'send', + title: 'User sign up', + channel: { + $ref: '#/channels/user', }, - "bindings": { - "$ref": "#/components/operationBindings/sendUser" + bindings: { + $ref: '#/components/operationBindings/sendUser', }, - "traits": [ + traits: [ { - "$ref": "#/components/operationTraits/binding" - } + $ref: '#/components/operationTraits/binding', + }, ], - "reply": { - "$ref": "#/components/replies/signupReply" - } - } + reply: { + $ref: '#/components/replies/signupReply', + }, + }, }, - "replies": { - "signupReply": { - "address": { - "$ref": "#/components/replyAddresses/signupReply" + replies: { + signupReply: { + address: { + $ref: '#/components/replyAddresses/signupReply', + }, + channel: { + $ref: '#/channels/userSignupReply', }, - "channel": { - "$ref": "#/channels/userSignupReply" - } - } + }, }, - "replyAddresses": { - "signupReply": { - "location": "$message.header#/replyTo" - } + replyAddresses: { + signupReply: { + location: '$message.header#/replyTo', + }, + }, + securitySchemes: { + oauth: { + type: 'oauth2', + description: 'The oauth security descriptions', + flows: { + clientCredentials: { + tokenUrl: 'https://example.com/api/oauth/dialog', + availableScopes: { + 'subscribe:auth_revocations': + 'Scope required for authorization revocation topic', + }, + }, + }, + scopes: ['subscribe:auth_revocations'], + }, }, - "securitySchemes": { - "oauth": { - "type": "oauth2", - "description": "The oauth security descriptions", - "flows": { - "clientCredentials": { - "tokenUrl": "https://example.com/api/oauth/dialog", - "availableScopes": { - "subscribe:auth_revocations": "Scope required for authorization revocation topic" - } - } + operationTraits: { + binding: { + bindings: { + amqp: { + ack: false, + }, }, - "scopes": [ - "subscribe:auth_revocations" - ] - } + }, }, - "operationTraits": { - "binding": { - "bindings": { - "amqp": { - "ack": false - } - } - } + messageTraits: { + commonHeaders: { + headers: { + type: 'object', + properties: { + 'my-app-header': { + type: 'integer', + minimum: 0, + maximum: 100, + }, + }, + }, + }, }, - "messageTraits": { - "commonHeaders": { - "headers": { - "type": "object", - "properties": { - "my-app-header": { - "type": "integer", - "minimum": 0, - "maximum": 100 - } - } - } - } + tags: { + user: { + name: 'user', + description: 'User-related messages', + }, }, - "tags": { - "user": { - "name": "user", - "description": "User-related messages" - } + externalDocs: { + infoDocs: { + url: 'https://example.com/docs', + description: 'Find more info here', + }, }, - "externalDocs": { - "infoDocs": { - "url": "https://example.com/docs", - "description": "Find more info here" - } + serverBindings: { + devAmqp: { + amqp: { + exchange: 'my-exchange', + queue: 'my-queue', + }, + }, }, - "serverBindings": { - "devAmqp": { - "amqp": { - "exchange": "my-exchange", - "queue": "my-queue" - } - } + channelBindings: { + user: { + amqp: { + is: 'queue', + queue: { + exclusive: true, + }, + }, + }, }, - "channelBindings": { - "user": { - "amqp": { - "is": "queue", - "queue": { - "exclusive": true - } - } - } + operationBindings: { + sendUser: { + amqp: { + ack: false, + }, + }, }, - "operationBindings": { - "sendUser": { - "amqp": { - "ack": false - } - } + messageBindings: { + user: { + amqp: { + contentEncoding: 'gzip', + messageType: 'user.signup', + bindingVersion: '0.3.0', + }, + }, }, - "messageBindings": { - "user": { - "amqp": { - "contentEncoding": "gzip", - "messageType": "user.signup", - "bindingVersion": "0.3.0" - } - } - } - } - } - + }, + }; + const { document, diagnostics } = await parser.parse(documentRaw); expect(document).not.toBeInstanceOf(AsyncAPIDocumentV3);