diff --git a/CODEOWNERS b/CODEOWNERS index 398b2ebe..ca915f75 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -6,4 +6,4 @@ # The default owners are automatically added as reviewers when you open a pull request unless different owners are specified in the file. -* @fmvilas @jstoiko @asyncapi-bot-eve +* @fmvilas @jstoiko @magicmatatjahu @asyncapi-bot-eve diff --git a/index.js b/index.js deleted file mode 100644 index 77646739..00000000 --- a/index.js +++ /dev/null @@ -1,35 +0,0 @@ -const yaml = require('js-yaml'); -const r2j = require('ramldt2jsonschema'); - -module.exports = { - parse, - getMimeTypes -}; - -async function parse({ message, defaultSchemaFormat }) { - try { - let payload = message.payload; - if (typeof payload === 'object') { - payload = `#%RAML 1.0 Library\n${ - yaml.safeDump({ types: { tmpType: payload } })}`; - } - - // Draft 6 is compatible with 7. - const jsonModel = await r2j.dt2js(payload, 'tmpType', { draft: '06' }); - const convertedType = jsonModel.definitions.tmpType; - - message['x-parser-original-schema-format'] = message.schemaFormat || defaultSchemaFormat; - message['x-parser-original-payload'] = payload; - message.payload = convertedType; - delete message.schemaFormat; - } catch (e) { - console.error(e); - } -} - -function getMimeTypes() { - return [ - 'application/raml+yaml;version=1.0', - ]; -} - diff --git a/src/index.ts b/src/index.ts index 53fb4ed2..cbeeb15a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,13 +9,14 @@ const r2j = require('ramldt2jsonschema'); import type { SchemaParser, ParseSchemaInput, ValidateSchemaInput, SchemaValidateResult } from '@asyncapi/parser'; import type { v2 } from '@asyncapi/parser/esm/spec-types'; -export function RamlSchemaParser(): SchemaParser { +export function RamlDTSchemaParser(): SchemaParser { return { validate, parse, getMimeTypes, }; } +export default RamlDTSchemaParser; async function validate(input: ValidateSchemaInput): Promise { const payload = formatPayload(input.data); diff --git a/test/complex_cat.raml b/test/complex_cat.raml deleted file mode 100644 index b0c18da3..00000000 --- a/test/complex_cat.raml +++ /dev/null @@ -1,111 +0,0 @@ -#%RAML 1.0 Library - -types: - NameXML: - displayName: Name XML - type: string - xml: - attribute: false - wrapped: true - name: cat - prefix: animal - - CatName: - type: NameXML - default: "regular cat" - example: fluffy - displayName: Cat - description: "Cat name" - facets: - amazing: boolean - - CatBreed: - type: string - pattern: "\\w{3,}" - examples: - dogone: "CatOne" - dogtwo: "CatTwo" - enum: ["CatOne", "CatTwo", "CatThree"] - - CatPros: - type: array - uniqueItems: false - items: string - minItems: 1 - maxItems: 100 - - CatCons: - type: array - uniqueItems: true - items: - type: string - minLength: 1 - maxLength: 100 - - CatAge: - type: number - minimum: 1 - maximum: 50 - format: int32 - - CatWithCity: - additionalProperties: false - properties: - city: string - - CatWithAddress: - additionalProperties: false - properties: - address: string - - Cat: - type: [CatWithAddress, CatWithCity] - minProperties: 1 - maxProperties: 50 - additionalProperties: false - discriminator: breed - discriminatorValue: CatOne - properties: - proscons: - type: CatPros | CatCons - required: true - name: - type: CatName - amazing: true - breed: - type: CatBreed - age: CatAge - rating: - type: integer - multipleOf: 5 - example: - displayName: Cat's rating - description: Rating of cat's awesomeness - strict: false - value: 50 - year_of_birth: date-only - time_of_birth: time-only - dt_of_birth: - type: datetime-only - required: false - addition_date: - type: datetime - format: rfc2616 - removal_date: - type: datetime - photo: - type: file - fileTypes: ['image/jpeg', 'image/png'] - minLength: 1 - maxLength: 307200 - description: nil - habits?: string - character: nil | string - siblings: string[] - parents: CatName[] - ratingHistory: (integer | number)[] - additionalData: - type: - type: object - properties: - weight: number diff --git a/test/documents/complex.json b/test/documents/complex.json new file mode 100644 index 00000000..18b6f904 --- /dev/null +++ b/test/documents/complex.json @@ -0,0 +1,90 @@ +{ + "schemaFormat": "application/raml+yaml;version=1.0", + "payload": + { + "type": + [ + "CatWithAddress", + "CatWithCity" + ], + "minProperties": 1, + "maxProperties": 50, + "additionalProperties": false, + "discriminator": "breed", + "discriminatorValue": "CatOne", + "properties": + { + "proscons": + { + "type": "CatPros | CatCons", + "required": true + }, + "name": + { + "type": "CatName", + "amazing": true + }, + "breed": + { + "type": "CatBreed" + }, + "age": "CatAge", + "rating": + { + "type": "integer", + "multipleOf": 5, + "example": + { + "displayName": "Cat's rating", + "description": "Rating of cat's awesomeness", + "strict": false, + "value": 50 + } + }, + "year_of_birth": "date-only", + "time_of_birth": "time-only", + "dt_of_birth": + { + "type": "datetime-only", + "required": false + }, + "addition_date": + { + "type": "datetime", + "format": "rfc2616" + }, + "removal_date": + { + "type": "datetime" + }, + "photo": + { + "type": "file", + "fileTypes": + [ + "image/jpeg", + "image/png" + ], + "minLength": 1, + "maxLength": 307200 + }, + "description": "nil", + "habits?": "string", + "character": "nil | string", + "siblings": "string[]", + "parents": "CatName[]", + "ratingHistory": "(integer | number)[]", + "additionalData": + { + "type": + { + "type": "object", + "properties": + { + "weight": "number" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/documents/invalid-asyncapi.yaml b/test/documents/invalid-asyncapi.yaml new file mode 100644 index 00000000..1a2f51d4 --- /dev/null +++ b/test/documents/invalid-asyncapi.yaml @@ -0,0 +1,22 @@ +asyncapi: 2.4.0 +info: + title: My API + version: '1.0.0' + +channels: + myChannel: + publish: + message: + $ref: '#/components/messages/testMessage' + +components: + messages: + testMessage: + schemaFormat: application/raml+yaml;version=1.0 + payload: + type: object + properties: + title: string + author: + type: string + examples: test diff --git a/test/documents/invalid.json b/test/documents/invalid.json new file mode 100644 index 00000000..1ed399fb --- /dev/null +++ b/test/documents/invalid.json @@ -0,0 +1,13 @@ +{ + "schemaFormat": "application/raml+yaml;version=1.0", + "payload": { + "type": "object", + "properties": { + "title": "string", + "author": { + "type": "string", + "examples": "test" + } + } + } +} \ No newline at end of file diff --git a/test/documents/simple.json b/test/documents/simple.json new file mode 100644 index 00000000..b67ea10a --- /dev/null +++ b/test/documents/simple.json @@ -0,0 +1,21 @@ +{ + "schemaFormat": "application/raml+yaml;version=1.0", + "payload": { + "type": "object", + "properties": { + "title": "string", + "author": { + "type": "string", + "examples": { + "anExample": "Eva" + } + } + }, + "examples": { + "exampleOne": { + "title": "A book", + "author": "An author" + } + } + } +} \ No newline at end of file diff --git a/test/asyncapi-raml.yaml b/test/documents/valid-asyncapi.yaml similarity index 55% rename from test/asyncapi-raml.yaml rename to test/documents/valid-asyncapi.yaml index 8b91c8ec..7b0073a2 100644 --- a/test/asyncapi-raml.yaml +++ b/test/documents/valid-asyncapi.yaml @@ -1,23 +1,18 @@ -asyncapi: 2.0.0 +asyncapi: 2.4.0 info: - title: Example using RAML data types + title: My API version: '1.0.0' + channels: - mychannel: + myChannel: publish: message: $ref: '#/components/messages/testMessage' - otherchannel: - subscribe: - message: - schemaFormat: 'application/raml+yaml;version=1.0' - payload: - $ref: 'complex_cat.raml#/types/Cat' components: messages: testMessage: - schemaFormat: 'application/raml+yaml;version=1.0' + schemaFormat: application/raml+yaml;version=1.0 payload: type: object properties: @@ -29,4 +24,4 @@ components: examples: exampleOne: title: A book - author: An author + author: An author \ No newline at end of file diff --git a/test/parse_test.js b/test/parse_test.js deleted file mode 100644 index e5932494..00000000 --- a/test/parse_test.js +++ /dev/null @@ -1,21 +0,0 @@ -const chai = require('chai'); -const chaiAsPromised = require('chai-as-promised'); -const fs = require('fs'); -const path = require('path'); -const ramlDtParser = require('..'); -const parser = require('@asyncapi/parser'); - -chai.use(chaiAsPromised); -const expect = chai.expect; - -const inputWithRAML = fs.readFileSync(path.resolve(__dirname, './asyncapi-raml.yaml'), 'utf8'); -const outputWithRAML = JSON.stringify({asyncapi: '2.0.0',info: {title: 'Example using RAML data types',version: '1.0.0'},channels: {mychannel: {publish: {message: {payload: {type: 'object',examples: [{title: 'A book',author: 'An author'}],additionalProperties: true,required: ['title','author'],properties: {title: {type: 'string','x-parser-schema-id': ''},author: {type: 'string',examples: ['Eva'],'x-parser-schema-id': ''}},'x-parser-schema-id': ''},'x-parser-original-schema-format': 'application/raml+yaml;version=1.0','x-parser-original-payload': '#%RAML 1.0 Library\ntypes:\n tmpType:\n type: object\n properties:\n title: string\n author:\n type: string\n examples:\n anExample: Eva\n examples:\n exampleOne:\n title: A book\n author: An author\n',schemaFormat: 'application/vnd.aai.asyncapi;version=2.0.0','x-parser-message-parsed': true,'x-parser-message-name': 'testMessage'}}},otherchannel: {subscribe: {message: {payload: {minProperties: 1,maxProperties: 50,additionalProperties: false,discriminator: 'breed',discriminatorValue: 'CatOne',type: 'object',required: ['proscons','name','breed','age','rating','year_of_birth','time_of_birth','addition_date','removal_date','photo','description','character','siblings','parents','ratingHistory','additionalData'],properties: {proscons: {anyOf: [true,true],'x-parser-schema-id': ''},name: true,breed: true,age: true,rating: {type: 'integer',multipleOf: 5,example: {displayName: 'Cat\'s rating',description: 'Rating of cat\'s awesomeness',strict: false,value: 50},'x-parser-schema-id': ''},year_of_birth: {type: 'string',format: 'date','x-parser-schema-id': ''},time_of_birth: {type: 'string',format: 'time','x-parser-schema-id': ''},dt_of_birth: {type: 'string',format: 'date-time-only','x-parser-schema-id': ''},addition_date: {type: 'string',format: 'rfc2616','x-parser-schema-id': ''},removal_date: {type: 'string',format: 'date-time','x-parser-schema-id': ''},photo: {type: 'string',minLength: 1,maxLength: 307200,'x-parser-schema-id': ''},description: {type: 'null','x-parser-schema-id': ''},habits: {type: 'string','x-parser-schema-id': ''},character: {anyOf: [{type: 'null','x-parser-schema-id': ''},{type: 'string','x-parser-schema-id': ''}],'x-parser-schema-id': ''},siblings: {type: 'array',items: {type: 'string','x-parser-schema-id': ''},'x-parser-schema-id': ''},parents: {type: 'array',items: true,'x-parser-schema-id': ''},ratingHistory: {type: 'array',items: {anyOf: [{type: 'integer','x-parser-schema-id': ''},{type: 'number','x-parser-schema-id': ''}],'x-parser-schema-id': ''},'x-parser-schema-id': ''},additionalData: {type: 'object',additionalProperties: true,required: ['weight'],properties: {weight: {type: 'number','x-parser-schema-id': ''}},'x-parser-schema-id': ''}},'x-parser-schema-id': ''},'x-parser-original-schema-format': 'application/raml+yaml;version=1.0','x-parser-original-payload': '#%RAML 1.0 Library\ntypes:\n tmpType:\n type:\n - CatWithAddress\n - CatWithCity\n minProperties: 1\n maxProperties: 50\n additionalProperties: false\n discriminator: breed\n discriminatorValue: CatOne\n properties:\n proscons:\n type: CatPros | CatCons\n required: true\n name:\n type: CatName\n amazing: true\n breed:\n type: CatBreed\n age: CatAge\n rating:\n type: integer\n multipleOf: 5\n example:\n displayName: Cat\'s rating\n description: Rating of cat\'s awesomeness\n strict: false\n value: 50\n year_of_birth: date-only\n time_of_birth: time-only\n dt_of_birth:\n type: datetime-only\n required: false\n addition_date:\n type: datetime\n format: rfc2616\n removal_date:\n type: datetime\n photo:\n type: file\n fileTypes:\n - image/jpeg\n - image/png\n minLength: 1\n maxLength: 307200\n description: nil\n habits?: string\n character: nil | string\n siblings: \'string[]\'\n parents: \'CatName[]\'\n ratingHistory: \'(integer | number)[]\'\n additionalData:\n type:\n type: object\n properties:\n weight: number\n',schemaFormat: 'application/vnd.aai.asyncapi;version=2.0.0','x-parser-message-parsed': true,'x-parser-message-name': ''}}}},components: {messages: {testMessage: {payload: {type: 'object',examples: [{title: 'A book',author: 'An author'}],additionalProperties: true,required: ['title','author'],properties: {title: {type: 'string','x-parser-schema-id': ''},author: {type: 'string',examples: ['Eva'],'x-parser-schema-id': ''}},'x-parser-schema-id': ''},'x-parser-original-schema-format': 'application/raml+yaml;version=1.0','x-parser-original-payload': '#%RAML 1.0 Library\ntypes:\n tmpType:\n type: object\n properties:\n title: string\n author:\n type: string\n examples:\n anExample: Eva\n examples:\n exampleOne:\n title: A book\n author: An author\n',schemaFormat: 'application/vnd.aai.asyncapi;version=2.0.0','x-parser-message-parsed': true,'x-parser-message-name': 'testMessage'}}}, 'x-parser-spec-parsed': true}); - -parser.registerSchemaParser(ramlDtParser); - -describe('parse()', function() { - it('should parse RAML data types', async function() { - const result = await parser.parse(inputWithRAML, { path: __filename }); - await expect(JSON.stringify(result.json())).to.equal(outputWithRAML); - }); -}); diff --git a/test/parser.spec.ts b/test/parser.spec.ts index e69de29b..09423835 100644 --- a/test/parser.spec.ts +++ b/test/parser.spec.ts @@ -0,0 +1,131 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { Parser } from '@asyncapi/parser'; +import { RamlDTSchemaParser } from '../src'; + +import type { ParseSchemaInput, ValidateSchemaInput, SchemaValidateResult, Diagnostic } from '@asyncapi/parser'; + +const inputWithSimpleRAML = toInput(fs.readFileSync(path.resolve(__dirname, './documents/simple.json'), 'utf8')); +const outputWithSimpleRAML = '{"type":"object","examples":[{"title":"A book","author":"An author"}],"additionalProperties":true,"required":["title","author"],"properties":{"title":{"type":"string"},"author":{"type":"string","examples":["Eva"]}}}'; + +const inputWithComplexRAML = toInput(fs.readFileSync(path.resolve(__dirname, './documents/complex.json'), 'utf8')); +const outputWithComplexRAML = '{"minProperties":1,"maxProperties":50,"additionalProperties":false,"discriminator":"breed","discriminatorValue":"CatOne","type":"object","required":["proscons","name","breed","age","rating","year_of_birth","time_of_birth","addition_date","removal_date","photo","description","character","siblings","parents","ratingHistory","additionalData"],"properties":{"proscons":{"anyOf":[true,true]},"name":true,"breed":true,"age":true,"rating":{"type":"integer","multipleOf":5,"example":{"displayName":"Cat\'s rating","description":"Rating of cat\'s awesomeness","strict":false,"value":50}},"year_of_birth":{"type":"string","format":"date"},"time_of_birth":{"type":"string","format":"time"},"dt_of_birth":{"type":"string","format":"date-time-only"},"addition_date":{"type":"string","format":"rfc2616"},"removal_date":{"type":"string","format":"date-time"},"photo":{"type":"string","minLength":1,"maxLength":307200},"description":{"type":"null"},"habits":{"type":"string"},"character":{"anyOf":[{"type":"null"},{"type":"string"}]},"siblings":{"type":"array","items":{"type":"string"}},"parents":{"type":"array","items":true},"ratingHistory":{"type":"array","items":{"anyOf":[{"type":"integer"},{"type":"number"}]}},"additionalData":{"type":"object","additionalProperties":true,"required":["weight"],"properties":{"weight":{"type":"number"}}}}}'; + +const inputWithInvalidRAML = toInput(fs.readFileSync(path.resolve(__dirname, './documents/invalid.json'), 'utf8')); + +const inputWithValidAsyncAPI = fs.readFileSync(path.resolve(__dirname, './documents/valid-asyncapi.yaml'), 'utf8'); + +const inputWithInvalidAsyncAPI = fs.readFileSync(path.resolve(__dirname, './documents/invalid-asyncapi.yaml'), 'utf8'); + +describe('parse()', function() { + const parser = RamlDTSchemaParser(); + const coreParser = new Parser(); + coreParser.registerSchemaParser(parser); + + it('should parse simple RAML data types', async function() { + await doParseTest(inputWithSimpleRAML, outputWithSimpleRAML); + }); + + it('should parse complex RAML data types', async function() { + await doParseTest(inputWithComplexRAML, outputWithComplexRAML); + }); + + it('should parse valid AsyncAPI', async function() { + const { document, diagnostics } = await coreParser.parse(inputWithValidAsyncAPI); + expect(filterDiagnostics(diagnostics, 'asyncapi-schemas-v2')).toHaveLength(0); + doParseCoreTest((document?.json()?.channels?.myChannel?.publish?.message as any)?.payload, outputWithSimpleRAML); + doParseCoreTest((document?.json()?.components?.messages?.testMessage as any)?.payload, outputWithSimpleRAML); + }); + + async function doParseTest(originalInput: ParseSchemaInput, expectedOutput: string) { + const input = {...originalInput}; + const result = await parser.parse(input); + + // Check that the return value of parse() is the expected JSON Schema. + expect(result).toEqual(JSON.parse(expectedOutput)); + } + + async function doParseCoreTest(parsedSchema: any, expectedOutput: string) { + const result = JSON.parse(JSON.stringify(parsedSchema, (field: string, value: unknown) => { + if (field === 'x-parser-schema-id') return; + return value; + })); + + // Check that the return value of parse() is the expected JSON Schema. + expect(result).toEqual(JSON.parse(expectedOutput)); + } +}); + +describe('validate()', function() { + const parser = RamlDTSchemaParser(); + const coreParser = new Parser(); + coreParser.registerSchemaParser(parser); + + it('should validate valid RAML', async function() { + const result = await parser.validate(inputWithSimpleRAML); + expect(result).toHaveLength(0); + }); + + it('should validate invalid RAML', async function() { + const results = await parser.validate(inputWithInvalidRAML); + expect(results).toHaveLength(1); + + const result = (results as SchemaValidateResult[])[0]; + expect(result.message).toEqual('Property \'examples\' should be a map'); + expect(result.path).toEqual(['otherchannel', 'subscribe', 'message', 'payload']); // Validator doesn't provide info about the error path + }); + + it('should validate valid AsyncAPI', async function() { + const diagnostics = await coreParser.validate(inputWithValidAsyncAPI); + expect(filterDiagnostics(diagnostics, 'asyncapi-schemas-v2')).toHaveLength(0); + }); + + it('should validate invalid AsyncAPI', async function() { + const diagnostics = await coreParser.validate(inputWithInvalidAsyncAPI); + expect(filterDiagnostics(diagnostics, 'asyncapi-schemas-v2')).toHaveLength(2); + expectDiagnostics(diagnostics, 'asyncapi-schemas-v2', [ + // in channels + { + message: 'Property \'examples\' should be a map', + path: ['channels', 'myChannel', 'publish', 'message', 'payload'] + }, + + // in components.messages + { + message: 'Property \'examples\' should be a map', + path: ['components', 'messages', 'testMessage', 'payload'] + }, + ]); + }); +}); + +function toInput(raw: string): ParseSchemaInput | ValidateSchemaInput { + const message = JSON.parse(raw); + return { + asyncapi: { + semver: { + version: '2.5.0', + major: 2, + minor: 5, + patch: 0 + }, + source: '', + parsed: {} as any, + }, + path: ['otherchannel', 'subscribe', 'message', 'payload'], + data: message.payload, + meta: { + message, + }, + schemaFormat: message.schemaFormat, + defaultSchemaFormat: 'application/vnd.aai.asyncapi;version=2.5.0', + }; +} + +function filterDiagnostics(diagnostics: Diagnostic[], code: string) { + return diagnostics.filter(d => d.code === code); +} + +function expectDiagnostics(diagnostics: Diagnostic[], code: string, results: SchemaValidateResult[]) { + expect(filterDiagnostics(diagnostics, code)).toEqual(results.map(e => expect.objectContaining(e))); +}