diff --git a/src/schema-parser/avro-schema-parser.ts b/src/schema-parser/avro-schema-parser.ts deleted file mode 100644 index f81f395ef..000000000 --- a/src/schema-parser/avro-schema-parser.ts +++ /dev/null @@ -1,354 +0,0 @@ -import avsc from 'avsc'; - -import type { JSONSchema7TypeName } from 'json-schema'; -import type { Schema } from 'avsc'; -import type { SchemaParser, ParseSchemaInput, ValidateSchemaInput } from '../schema-parser'; -import type { AsyncAPISchema, SchemaValidateResult } from '../types'; - -import type { v2 } from '../spec-types'; - -type AvroSchema = Schema & { [key: string]: any }; - -export function AvroSchemaParser(): SchemaParser { - return { - validate, - parse, - getMimeTypes, - }; -} - -async function validate(input: ValidateSchemaInput): Promise { - const result: SchemaValidateResult[] = []; - - try { - validateAvroSchema(input.data as AvroSchema); - } catch (error) { - if (error instanceof Error) { - result.push({ - message: error.message, - path: input.path, // avsc doesn't throw errors with meaningful paths - }); - } - } - - return result; -} - -async function parse(input: ParseSchemaInput): Promise { - const asyncAPISchema = await avroToJsonSchema(input.data as AvroSchema); - - // TODO: Should the following modifications to the message object be done in the caller and for all parsers rather than here? - // remove that function when https://github.com/asyncapi/spec/issues/622 will be introduced in AsyncAPI spec - const message = (input.meta as any).message; - const key = message?.bindings?.kafka?.key; - if (key) { - const bindingsTransformed = await avroToJsonSchema(key); - message['x-parser-original-bindings-kafka-key'] = key; - message.bindings.kafka.key = bindingsTransformed; - } - - return asyncAPISchema; -} - -function getMimeTypes() { - return [ - 'application/vnd.apache.avro;version=1.9.0', - 'application/vnd.apache.avro+json;version=1.9.0', - 'application/vnd.apache.avro+yaml;version=1.9.0', - 'application/vnd.apache.avro;version=1.8.2', - 'application/vnd.apache.avro+json;version=1.8.2', - 'application/vnd.apache.avro+yaml;version=1.8.2' - ]; -} - -const BYTES_PATTERN = '^[\u0000-\u00ff]*$'; -const INT_MIN = Math.pow(-2, 31); -const INT_MAX = Math.pow(2, 31) - 1; -const LONG_MIN = Math.pow(-2, 63); -const LONG_MAX = Math.pow(2, 63) - 1; - -const typeMappings: Record = { - null: 'null', - boolean: 'boolean', - int: 'integer', - long: 'integer', - float: 'number', - double: 'number', - bytes: 'string', - string: 'string', - fixed: 'string', - map: 'object', - array: 'array', - enum: 'string', - record: 'object', - uuid: 'string', -}; - -function commonAttributesMapping(avroDefinition: AvroSchema, jsonSchema: v2.AsyncAPISchemaDefinition, recordCache: { [key:string]: AsyncAPISchema }): void { - if (avroDefinition.doc) jsonSchema.description = avroDefinition.doc; - if (avroDefinition.default !== undefined) jsonSchema.default = avroDefinition.default; - - const fullyQualifiedName = getFullyQualifiedName(avroDefinition); - if (fullyQualifiedName !== undefined && recordCache[fullyQualifiedName]) { - jsonSchema['x-parser-schema-id'] = fullyQualifiedName; - } -} - -function getFullyQualifiedName(avroDefinition: AvroSchema) { - let name; - - if (avroDefinition.name) { - if (avroDefinition.namespace) { - name = `${avroDefinition.namespace}.${avroDefinition.name}`; - } else { - name = avroDefinition.name; - } - } - - return name; -} - -/** - * Enrich the parent's required attribute with the required record attributes - * @param fieldDefinition the actual field definition - * @param parentJsonSchema the parent json schema which contains the required property to enrich - * @param haveDefaultValue we assure that a required field does not have a default value - */ -function requiredAttributesMapping(fieldDefinition: any, parentJsonSchema: v2.AsyncAPISchemaDefinition, haveDefaultValue: boolean): void { - const isUnionWithNull = Array.isArray(fieldDefinition.type) && fieldDefinition.type.includes('null'); - - // we assume that a union type without null and a field without default value is required - if (!isUnionWithNull && !haveDefaultValue) { - parentJsonSchema.required = parentJsonSchema.required || []; - parentJsonSchema.required.push(fieldDefinition.name); - } -} - -function extractNonNullableTypeIfNeeded(typeInput: any, jsonSchemaInput: v2.AsyncAPISchemaDefinition): { type: string, jsonSchema: v2.AsyncAPISchemaDefinition } { - let type = typeInput; - let jsonSchema = jsonSchemaInput; - // Map example to first non-null type - if (Array.isArray(typeInput) && typeInput.length > 0) { - const pickSecondType = typeInput.length > 1 && typeInput[0] === 'null'; - type = typeInput[+pickSecondType]; - if (jsonSchema.oneOf !== undefined) { - jsonSchema = jsonSchema.oneOf[0] as v2.AsyncAPISchemaDefinition; - } - } - return { type, jsonSchema }; -} - -function exampleAttributeMapping(type: any, example: any, jsonSchema: v2.AsyncAPISchemaDefinition): void { - if (example === undefined || jsonSchema.examples || Array.isArray(type)) return; - - switch (type) { - case 'boolean': - jsonSchema.examples = [example === 'true']; - break; - case 'int': - jsonSchema.examples = [parseInt(example, 10)]; - break; - default: - jsonSchema.examples = [example]; - } -} - -function additionalAttributesMapping(typeInput: any, avroDefinition: AvroSchema, jsonSchemaInput: v2.AsyncAPISchemaDefinition): void { - const __ret = extractNonNullableTypeIfNeeded(typeInput, jsonSchemaInput); - const type = __ret.type; - const jsonSchema = __ret.jsonSchema; - - exampleAttributeMapping(type, avroDefinition.example, jsonSchema); - - function setAdditionalAttribute(...names: string[]) { - names.forEach(name => { - let isValueCoherent = true; - if (name === 'minLength' || name === 'maxLength') { - isValueCoherent = avroDefinition[name] > -1; - } else if (name === 'multipleOf') { - isValueCoherent = avroDefinition[name] > 0; - } - if (avroDefinition[name] !== undefined && isValueCoherent) jsonSchema[name] = avroDefinition[name]; - }); - } - - switch (type) { - case 'int': // int, long, float, and double must support the attributes bellow - case 'long': - case 'float': - case 'double': - setAdditionalAttribute('minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'); - break; - case 'string': - if (avroDefinition.logicalType) { - jsonSchema.format = avroDefinition.logicalType; - } - setAdditionalAttribute('pattern', 'minLength', 'maxLength'); - break; - case 'array': - setAdditionalAttribute('minItems', 'maxItems', 'uniqueItems'); - break; - default: - break; - } -} - -function validateAvroSchema(avroDefinition: AvroSchema): void | never { - // don't need to use the output from parsing the - // avro definition - we're just using this as a - // validator as this will throw an exception if - // there are any problems with the definition - avsc.Type.forSchema(avroDefinition); -} - -/** - * Cache the passed value under the given key. If the key is undefined the value will not be cached. This function - * uses mutation of the passed cache object rather than a copy on write cache strategy. - * - * @param cache Map the cache to store the JsonSchema - * @param key String | Undefined - the fully qualified name of an avro record - * @param value JsonSchema - The json schema from the avro record - */ -function cacheAvroRecordDef(cache: { [key:string]: AsyncAPISchema }, key: string, value: AsyncAPISchema): void { - if (key !== undefined) { - cache[key] = value; - } -} - -async function convertAvroToJsonSchema(avroDefinition: AvroSchema , isTopLevel: boolean, recordCache: Map | any = {}): Promise { - let jsonSchema: v2.AsyncAPISchemaDefinition = {}; - const isUnion = Array.isArray(avroDefinition); - - if (isUnion) { - return processUnionSchema(jsonSchema, avroDefinition, isTopLevel, recordCache); - } - - // Avro definition can be a string (e.g. "int") - // or an object like { type: "int" } - const type = avroDefinition.type || avroDefinition; - jsonSchema.type = typeMappings[type]; - - switch (type) { - case 'int': { - jsonSchema.minimum = INT_MIN; - jsonSchema.maximum = INT_MAX; - break; - } - case 'long': { - jsonSchema.minimum = LONG_MIN; - jsonSchema.maximum = LONG_MAX; - break; - } - case 'bytes': { - jsonSchema.pattern = BYTES_PATTERN; - break; - } - case 'fixed': { - jsonSchema.pattern = BYTES_PATTERN; - jsonSchema.minLength = avroDefinition.size; - jsonSchema.maxLength = avroDefinition.size; - break; - } - case 'map': { - jsonSchema.additionalProperties = await convertAvroToJsonSchema(avroDefinition.values, false); - break; - } - case 'array': { - jsonSchema.items = await convertAvroToJsonSchema(avroDefinition.items, false); - break; - } - case 'enum': { - jsonSchema.enum = avroDefinition.symbols; - break; - } - case 'float': // float and double must support the format attribute from the avro type - case 'double': { - jsonSchema.format = type; - break; - } - case 'record': { - const propsMap = await processRecordSchema(avroDefinition, recordCache, jsonSchema); - cacheAvroRecordDef(recordCache, getFullyQualifiedName(avroDefinition), propsMap); - jsonSchema.properties = Object.fromEntries(propsMap.entries()); - break; - } - default: { - const cachedRecord = recordCache[getFullyQualifiedName(avroDefinition)]; - if (cachedRecord) { - jsonSchema = cachedRecord; - } - break; - } - } - - commonAttributesMapping(avroDefinition, jsonSchema, recordCache); - additionalAttributesMapping(type, avroDefinition, jsonSchema); - - return jsonSchema; -} - -/** - * When a record type is found in an avro schema this function can be used to process the underlying fields and return - * the map of props contained by the record. The record will also be cached. - * - * @param avroDefinition the avro schema to be processed - * @param recordCache the cache of previously processed avro record types - * @param jsonSchema the schema for the record. - * @returns {Promise>} - */ -async function processRecordSchema(avroDefinition: AvroSchema, recordCache: Record, jsonSchema: v2.AsyncAPISchemaDefinition): Promise> { - const propsMap = new Map(); - for (const field of avroDefinition.fields) { - // If the type is a sub schema it will have been stored in the cache. - if (recordCache[field.type]) { - propsMap.set(field.name, recordCache[field.type]); - } else { - const def = await convertAvroToJsonSchema(field.type, false, recordCache); - - requiredAttributesMapping(field, jsonSchema, field.default !== undefined); - commonAttributesMapping(field, def, recordCache); - additionalAttributesMapping(field.type, field, def); - - propsMap.set(field.name, def); - // If there is a name for the sub record cache it under the name. - const qualifiedFieldName = getFullyQualifiedName(field.type); - cacheAvroRecordDef(recordCache, qualifiedFieldName, def); - } - } - return propsMap; -} - -/** - * Handles processing union avro schema types by creating a oneOf jsonSchema definition. This will mutate the passed - * jsonSchema and recordCache objects. - * - * @param jsonSchema the jsonSchema object that will be mutated. - * @param avroDefinition the avro schema to be processed - * @param isTopLevel is this the top level of the schema or is this a sub schema - * @param recordCache the cache of previously processed record types - * @returns {Promise} the mutated jsonSchema that was provided to the function - */ -async function processUnionSchema(jsonSchema: v2.AsyncAPISchemaDefinition, avroDefinition: AvroSchema, isTopLevel: boolean, recordCache: Record): Promise { - jsonSchema.oneOf = []; - let nullDef = null; - for (const avroDef of avroDefinition as any) { - const def = await convertAvroToJsonSchema(avroDef, isTopLevel, recordCache); - // avroDef can be { type: 'int', default: 1 } and this is why avroDef.type has priority here - const defType = avroDef.type || avroDef; - // To prefer non-null values in the examples skip null definition here and push it as the last element after loop - if (defType === 'null') { - nullDef = def; - } else { - jsonSchema.oneOf.push(def); - const qualifiedName = getFullyQualifiedName(avroDef); - cacheAvroRecordDef(recordCache, qualifiedName, def); - } - } - if (nullDef) jsonSchema.oneOf.push(nullDef); - - return jsonSchema; -} - -export async function avroToJsonSchema(avroDefinition: AvroSchema) { - return convertAvroToJsonSchema(avroDefinition, true); -} diff --git a/src/schema-parser/openapi-schema-parser.ts b/src/schema-parser/openapi-schema-parser.ts deleted file mode 100644 index 42ea8d3d9..000000000 --- a/src/schema-parser/openapi-schema-parser.ts +++ /dev/null @@ -1,106 +0,0 @@ -import Ajv from 'ajv'; -import { schemaV3 } from './openapi/schema_v3'; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const toJsonSchema = require('@openapi-contrib/openapi-schema-to-json-schema'); - -import type { ErrorObject, ValidateFunction } from 'ajv'; -import type { SchemaParser, ParseSchemaInput, ValidateSchemaInput } from '../schema-parser'; -import type { AsyncAPISchema, SchemaValidateResult } from '../types'; - -const ajv = new Ajv({ - allErrors: true, - strict: false, - logger: false, -}); -ajv.addSchema(schemaV3, 'openapi'); - -export function OpenAPISchemaParser(): SchemaParser { - return { - validate, - parse, - getMimeTypes, - }; -} - -async function validate(input: ValidateSchemaInput): Promise { - const validator = ajv.getSchema('openapi') as ValidateFunction; - - let result: SchemaValidateResult[] = []; - const valid = validator(input.data); - if (!valid && validator.errors) { - result = ajvToSpectralResult(input.path, [...validator.errors]); - } - - return result; -} - -async function parse(input: ParseSchemaInput): Promise { - const transformed = toJsonSchema(input.data, { - cloneSchema: true, - keepNotSupported: [ - 'discriminator', - 'readOnly', - 'writeOnly', - 'deprecated', - 'xml', - 'example', - ], - }); - - iterateSchema(transformed); - return transformed; -} - -function getMimeTypes() { - return [ - 'application/vnd.oai.openapi;version=3.0.0', - 'application/vnd.oai.openapi+json;version=3.0.0', - 'application/vnd.oai.openapi+yaml;version=3.0.0', - ]; -} - -function ajvToSpectralResult(path: Array, errors: ErrorObject[]): SchemaValidateResult[] { - return errors.map(error => { - return { - message: error.message, - path: [...path, ...error.instancePath.replace(/^\//, '').split('/')], - }; - }) as SchemaValidateResult[]; -} - -function iterateSchema(schema: any) { - if (schema.example !== undefined) { - const examples = schema.examples || []; - examples.push(schema.example); - schema.examples = examples; - delete schema.example; - } - - if (schema.$schema !== undefined) { - delete schema.$schema; - } - - aliasProps(schema.properties); - aliasProps(schema.patternProperties); - aliasProps(schema.additionalProperties); - aliasProps(schema.items); - aliasProps(schema.additionalItems); - aliasProps(schema.oneOf); - aliasProps(schema.anyOf); - aliasProps(schema.allOf); - aliasProps(schema.not); -} - -function aliasProps(obj: any) { - for (const key in obj) { - const prop = obj[key]; - - if (prop.xml !== undefined) { - prop['x-xml'] = prop.xml; - delete prop.xml; - } - - iterateSchema(obj[key]); - } -} diff --git a/src/schema-parser/openapi/schema_v3.ts b/src/schema-parser/openapi/schema_v3.ts deleted file mode 100644 index 3689984c5..000000000 --- a/src/schema-parser/openapi/schema_v3.ts +++ /dev/null @@ -1,275 +0,0 @@ -/* eslint-disable */ -// From https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/schemas/v3.0/schema.json -export const schemaV3 ={ - type: 'object', - definitions: { - Reference: { - type: 'object', - required: ['$ref'], - patternProperties: { - '^\\$ref$': { - type: 'string', - format: 'uri-reference' - } - } - }, - Discriminator: { - type: 'object', - required: ['propertyName'], - properties: { - propertyName: { - type: 'string' - }, - mapping: { - type: 'object', - additionalProperties: { - type: 'string' - } - } - } - }, - ExternalDocumentation: { - type: 'object', - required: ['url'], - properties: { - description: { - type: 'string' - }, - url: { - type: 'string', - format: 'uri-reference' - } - }, - patternProperties: { - '^x-': {} - }, - additionalProperties: false - }, - XML: { - type: 'object', - properties: { - name: { - type: 'string' - }, - namespace: { - type: 'string', - format: 'uri' - }, - prefix: { - type: 'string' - }, - attribute: { - type: 'boolean', - default: false - }, - wrapped: { - type: 'boolean', - default: false - } - }, - patternProperties: { - '^x-': {} - }, - additionalProperties: false - } - }, - properties: { - title: { - type: 'string' - }, - multipleOf: { - type: 'number', - exclusiveMinimum: 0 - }, - maximum: { - type: 'number' - }, - exclusiveMaximum: { - type: 'boolean', - default: false - }, - minimum: { - type: 'number' - }, - exclusiveMinimum: { - type: 'boolean', - default: false - }, - maxLength: { - type: 'integer', - minimum: 0 - }, - minLength: { - type: 'integer', - minimum: 0, - default: 0 - }, - pattern: { - type: 'string', - format: 'regex' - }, - maxItems: { - type: 'integer', - minimum: 0 - }, - minItems: { - type: 'integer', - minimum: 0, - default: 0 - }, - uniqueItems: { - type: 'boolean', - default: false - }, - maxProperties: { - type: 'integer', - minimum: 0 - }, - minProperties: { - type: 'integer', - minimum: 0, - default: 0 - }, - required: { - type: 'array', - items: { - type: 'string' - }, - minItems: 1, - uniqueItems: true - }, - enum: { - type: 'array', - items: {}, - minItems: 1, - uniqueItems: false - }, - type: { - type: 'string', - enum: ['array', 'boolean', 'integer', 'number', 'object', 'string'] - }, - not: { - oneOf: [ - { - $ref: '#' - }, - { - $ref: '#/definitions/Reference' - } - ] - }, - allOf: { - type: 'array', - items: { - oneOf: [ - { - $ref: '#' - }, - { - $ref: '#/definitions/Reference' - } - ] - } - }, - oneOf: { - type: 'array', - items: { - oneOf: [ - { - $ref: '#' - }, - { - $ref: '#/definitions/Reference' - } - ] - } - }, - anyOf: { - type: 'array', - items: { - oneOf: [ - { - $ref: '#' - }, - { - $ref: '#/definitions/Reference' - } - ] - } - }, - items: { - oneOf: [ - { - $ref: '#' - }, - { - $ref: '#/definitions/Reference' - } - ] - }, - properties: { - type: 'object', - additionalProperties: { - oneOf: [ - { - $ref: '#' - }, - { - $ref: '#/definitions/Reference' - } - ] - } - }, - additionalProperties: { - oneOf: [ - { - $ref: '#' - }, - { - $ref: '#/definitions/Reference' - }, - { - type: 'boolean' - } - ], - default: true - }, - description: { - type: 'string' - }, - format: { - type: 'string' - }, - default: {}, - nullable: { - type: 'boolean', - default: false - }, - discriminator: { - $ref: '#/definitions/Discriminator' - }, - readOnly: { - type: 'boolean', - default: false - }, - writeOnly: { - type: 'boolean', - default: false - }, - example: {}, - externalDocs: { - $ref: '#/definitions/ExternalDocumentation' - }, - deprecated: { - type: 'boolean', - default: false - }, - xml: { - $ref: '#/definitions/XML' - } - }, - patternProperties: { - '^x-': {} - }, - additionalProperties: false -}; \ No newline at end of file diff --git a/src/schema-parser/raml-schema-parser.ts b/src/schema-parser/raml-schema-parser.ts deleted file mode 100644 index 55127021b..000000000 --- a/src/schema-parser/raml-schema-parser.ts +++ /dev/null @@ -1,59 +0,0 @@ -import yaml from 'js-yaml'; -import * as lib from 'webapi-parser'; - -/* eslint-disable */ -const wap = lib.WebApiParser; -const r2j = require('ramldt2jsonschema'); -/* eslint-enable */ - -import type { SchemaParser, ParseSchemaInput, ValidateSchemaInput } from '../schema-parser'; -import type { AsyncAPISchema, SchemaValidateResult } from '../types'; - -export function RamlSchemaParser(): SchemaParser { - return { - validate, - parse, - getMimeTypes, - }; -} - -async function parse(input: ParseSchemaInput): Promise { - const payload = formatPayload(input.data); - - // Draft 6 is compatible with 7. - const jsonModel = await r2j.dt2js(payload, 'tmpType', { draft: '06' }); - return jsonModel.definitions.tmpType; -} - -function getMimeTypes() { - return [ - 'application/raml+yaml;version=1.0', - ]; -} - -async function validate(input: ValidateSchemaInput): Promise { - const payload = formatPayload(input.data); - const parsed = await wap.raml10.parse(payload); - const report = await wap.raml10.validate(parsed); - if (report.conforms) { - // No errors found. - return []; - } - - const validateResult: SchemaValidateResult[] = []; - report.results.forEach(result => { - validateResult.push({ - message: result.message, - path: input.path, // RAML parser doesn't provide a path to the error. - }); - }); - - return validateResult; -} - -function formatPayload(payload: unknown): string { - if (typeof payload === 'object') { - return `#%RAML 1.0 Library\n${yaml.dump({ types: { tmpType: payload } })}`; - } - return payload as string; -} \ No newline at end of file diff --git a/test/schema-parser/avro/asyncapi-avro-1.8.2.json b/test/schema-parser/avro/asyncapi-avro-1.8.2.json deleted file mode 100644 index beca0196d..000000000 --- a/test/schema-parser/avro/asyncapi-avro-1.8.2.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "schemaFormat": "application/vnd.apache.avro;version=1.8.2", - "payload": { - "type": "record", - "fields": [ - {"name": "name", "type": "string", "example": "Donkey"}, - {"name": "age", "type": ["null", "int"], "default": null}, - { - "name": "favoriteProgrammingLanguage", - "type": {"name": "ProgrammingLanguage", "type": "enum", "symbols": ["JS", "Java", "Go", "Rust", "C"]} - }, - { - "name": "address", - "type": { - "name": "Address", - "type": "record", - "fields": [{"name": "zipcode", "type": "int", "example": 53003}] - } - } - ] - } -} \ No newline at end of file diff --git a/test/schema-parser/avro/asyncapi-avro-1.9.0-additional-attributes.json b/test/schema-parser/avro/asyncapi-avro-1.9.0-additional-attributes.json deleted file mode 100644 index 56c767390..000000000 --- a/test/schema-parser/avro/asyncapi-avro-1.9.0-additional-attributes.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "schemaFormat": "application/vnd.apache.avro;version=1.9.0", - "payload": { - "name": "Person", - "namespace": "com.company", - "type": "record", - "fields": [ - {"name": "name", "type": "string", "example": "Donkey", "minLength": 0}, - {"name": "serialNo", "type": "string", "minLength": 0, "maxLength": 50}, - {"name": "email", "type": ["null","string"], "example": "donkey@asyncapi.com", "pattern": "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"}, - {"name": "age", "type": ["null", "int"], "default": null, "example": 123, "exclusiveMinimum": 0, "exclusiveMaximum": 200}, - { - "name": "favoriteProgrammingLanguage", - "type": {"name": "ProgrammingLanguage", "type": "enum", "symbols": ["JS", "Java", "Go", "Rust", "C"], "default": "JS"} - }, - {"name": "certifications", "type": {"type": "array", "items": "string", "minItems": 1, "maxItems": 500, "uniqueItems": true}}, - { - "name": "address", - "type": { - "name": "Address", - "type": "record", - "fields": [ - {"name": "zipcode", "type": "int", "example": 53003}, - {"name": "country", "type": ["null", "string"]} - ] - } - }, - {"name": "weight", "type": "float", "example": 65.1, "minimum": 0, "maximum": 500}, - {"name": "height", "type": "double", "example": 1.85, "minimum": 0, "maximum": 3.0}, - {"name": "someid", "type": "string", "logicalType": "uuid"} - ] - } -} \ No newline at end of file diff --git a/test/schema-parser/avro/asyncapi-avro-1.9.0-bindings.json b/test/schema-parser/avro/asyncapi-avro-1.9.0-bindings.json deleted file mode 100644 index c86d03670..000000000 --- a/test/schema-parser/avro/asyncapi-avro-1.9.0-bindings.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "schemaFormat": "application/vnd.apache.avro;version=1.9.0", - "payload": { - "name": "Person", - "namespace": "com.company", - "type": "record", - "fields": [ - {"name": "name", "type": "string", "example": "Donkey"}, - {"name": "age", "type": ["null", "int"], "default": null, "example": 123}, - { - "name": "favoriteProgrammingLanguage", - "type": {"name": "ProgrammingLanguage", "type": "enum", "symbols": ["JS", "Java", "Go", "Rust", "C"], "default": "JS"} - }, - { - "name": "address", - "type": { - "name": "Address", - "type": "record", - "fields": [{"name": "zipcode", "type": "int", "example": 53003}] - } - }, - {"name": "someid", "type": "string", "logicalType": "uuid"} - ] - }, - "bindings": { - "kafka": { - "key": { - "name": "Person", - "namespace": "com.company", - "type": "record", - "fields": [ - {"name": "name", "type": "string", "example": "Donkey"}, - {"name": "age", "type": ["null", "int"], "default": null, "example": 123}, - { - "name": "favoriteProgrammingLanguage", - "type": {"name": "ProgrammingLanguage", "type": "enum", "symbols": ["JS", "Java", "Go", "Rust", "C"], "default": "JS"} - }, - { - "name": "address", - "type": { - "name": "Address", - "type": "record", - "fields": [{"name": "zipcode", "type": "int", "example": 53003}] - } - }, - {"name": "someid", "type": "string", "logicalType": "uuid"} - ] - } - }, - "mqtt": { - "x-test": { - "type": "string" - } - } - } -} \ No newline at end of file diff --git a/test/schema-parser/avro/asyncapi-avro-1.9.0-namespace.json b/test/schema-parser/avro/asyncapi-avro-1.9.0-namespace.json deleted file mode 100644 index bea78bf34..000000000 --- a/test/schema-parser/avro/asyncapi-avro-1.9.0-namespace.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "schemaFormat": "application/vnd.apache.avro;version=1.9.0", - "payload": { - "name": "Person", - "namespace": "com.company", - "type": "record", - "fields": [ - {"name": "name", "type": "string", "example": "Donkey"}, - {"name": "age", "type": ["null", "int"], "default": null, "example": 123}, - { - "name": "favoriteProgrammingLanguage", - "type": {"name": "ProgrammingLanguage", "type": "enum", "symbols": ["JS", "Java", "Go", "Rust", "C"], "default": "JS"} - }, - { - "name": "address", - "type": { - "name": "Address", - "type": "record", - "fields": [{"name": "zipcode", "type": "int", "example": 53003}] - } - }, - {"name": "someid", "type": "string", "logicalType": "uuid"} - ] - } -} \ No newline at end of file diff --git a/test/schema-parser/avro/asyncapi-avro-1.9.0.json b/test/schema-parser/avro/asyncapi-avro-1.9.0.json deleted file mode 100644 index 5667d0510..000000000 --- a/test/schema-parser/avro/asyncapi-avro-1.9.0.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "schemaFormat": "application/vnd.apache.avro;version=1.9.0", - "payload": { - "name": "Person", - "type": "record", - "fields": [ - {"name": "name", "type": "string", "example": "Donkey"}, - {"name": "age", "type": ["null", "int"], "default": null, "example": 123}, - { - "name": "favoriteProgrammingLanguage", - "type": {"name": "ProgrammingLanguage", "type": "enum", "symbols": ["JS", "Java", "Go", "Rust", "C"], "default": "JS"} - }, - { - "name": "address", - "type": { - "name": "Address", - "type": "record", - "fields": [{"name": "zipcode", "type": "int", "example": 53003}] - } - }, - {"name": "someid", "type": "string", "logicalType": "uuid"} - ] - } -} \ No newline at end of file diff --git a/test/schema-parser/avro/asyncapi-avro-111-1.9.0.json b/test/schema-parser/avro/asyncapi-avro-111-1.9.0.json deleted file mode 100644 index d0eff6813..000000000 --- a/test/schema-parser/avro/asyncapi-avro-111-1.9.0.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "schemaFormat": "application/vnd.apache.avro;version=1.9.0", - "payload": { - "type": "record", - "name": "ConnectionRequested", - "namespace": "com.foo.connections", - "doc": "An example schema to illustrate the issue", - "fields": [ - { - "name": "metadata", - "type": { - "type": "record", - "name": "EventMetadata", - "namespace": "com.foo", - "doc": "Metadata to be associated with every published event", - "fields": [ - { - "name": "id", - "type": { - "type": "string", - "logicalType": "uuid" - }, - "doc": "Unique identifier for this specific event" - }, - { - "name": "timestamp", - "type": { - "type": "long", - "logicalType": "timestamp-millis" - }, - "doc": "Instant the event took place (not necessary when it was published)" - }, - { - "name": "correlation_id", - "type": [ - "null", - { - "type": "string", - "logicalType": "uuid" - } - ], - "doc": "id of the event that resulted in this\nevent being published (optional)", - "default": null - }, - { - "name": "publisher_context", - "type": [ - "null", - { - "type": "map", - "values": { - "type": "string", - "avro.java.string": "String" - }, - "avro.java.string": "String" - } - ], - "doc": "optional set of key-value pairs of context to be echoed back\nin any resulting message (like a richer\ncorrelationId.\n\nThese values are likely only meaningful to the publisher\nof the correlated event", - "default": null - } - ] - } - }, - { - "name": "auth_code", - "type": { - "type": "record", - "name": "EncryptedString", - "namespace": "com.foo", - "doc": "A string that was encrypted with AES (using CTR mode), its key encrypted with RSA, and the nonce used for the encryption.", - "fields": [ - { - "name": "value", - "type": "string", - "doc": "A sequence of bytes that has been AES encrypted in CTR mode." - }, - { - "name": "nonce", - "type": "string", - "doc": "A nonce, used by the CTR encryption mode for our encrypted value. Not encrypted, not a secret." - }, - { - "name": "key", - "type": "string", - "doc": "An AES key, used to encrypt the value field, that has itself been encrypted using RSA." - } - ] - }, - "doc": "Encrypted auth_code received when user authorizes the app." - }, - { - "name": "refresh_token", - "type": "com.foo.EncryptedString", - "doc": "Encrypted refresh_token generated by using clientId and clientSecret." - }, - { - "name": "triggered_by", - "type": { - "type": "string", - "logicalType": "uuid" - }, - "doc": "ID of the user who triggered this event." - } - ] - } -} \ No newline at end of file diff --git a/test/schema-parser/avro/asyncapi-avro-113-1.9.0.json b/test/schema-parser/avro/asyncapi-avro-113-1.9.0.json deleted file mode 100644 index 8f139ade8..000000000 --- a/test/schema-parser/avro/asyncapi-avro-113-1.9.0.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "schemaFormat": "application/vnd.apache.avro;version=1.9.0", - "payload": [ - { - "type": "record", - "name": "Address", - "namespace": "com.example", - "fields": [ - { - "name": "streetaddress", - "type": "string" - }, - { - "name": "city", - "type": "string" - } - ] - }, - { - "type": "record", - "name": "Person", - "namespace": "com.example", - "fields": [ - { - "name": "firstname", - "type": "string" - }, - { - "name": "lastname", - "type": "string" - }, - { - "name": "address", - "type": "com.example.Address" - } - ] - } - ] -} \ No newline at end of file diff --git a/test/schema-parser/avro/asyncapi-avro-148-1.9.0.json b/test/schema-parser/avro/asyncapi-avro-148-1.9.0.json deleted file mode 100644 index 7bf12c833..000000000 --- a/test/schema-parser/avro/asyncapi-avro-148-1.9.0.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "schemaFormat": "application/vnd.apache.avro;version=1.9.0", - "payload": { - "type":"record", - "name":"RecordWithReferences", - "namespace":"com.example", - "fields":[ - { - "name":"Record1", - "type":{ - "type":"record", - "name":"Record1", - "doc":"Reused in other fields", - "fields":[ - { - "name":"string", - "type":"string", - "doc":"field in Record1" - } - ] - } - }, - { - "name":"FieldThatDefineRecordInUnion", - "type":[ - "null", - { - "type":"record", - "name":"RecordDefinedInUnion", - "namespace":"com.example.model", - "doc":"", - "fields":[ - { - "name":"number", - "type":"long", - "doc":"field in RecordDefinedInUnion", - "minimum": 0, - "maximum": 2 - } - ] - } - ], - "default":null - }, - { - "name":"FieldThatReuseRecordDefinedInUnion", - "type":[ - "null", - "com.example.model.RecordDefinedInUnion" - ], - "default":null - }, - { - "name":"FieldThatReuseRecord1", - "type":[ - "null", - "Record1" - ], - "default":null - }, - { - "name": "simpleField", - "type": "string" - } - ] - } -} \ No newline at end of file diff --git a/test/schema-parser/avro/asyncapi-avro-broken.json b/test/schema-parser/avro/asyncapi-avro-broken.json deleted file mode 100644 index 7b51ce3ec..000000000 --- a/test/schema-parser/avro/asyncapi-avro-broken.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "schemaFormat": "application/vnd.apache.avro;version=1.9.0", - "payload": { - "name": "Person", - "type": "record", - "fields": [ - {"name": "name", "type": "string", "example": "Donkey"}, - {"name": "age", "type": ["null", "int"], "default": null, "example": "123"}, - { - "name": "favoriteProgrammingLanguage", - "type": {"name": "ProgrammingLanguage", "type": "enum", "symbols": ["JS", "Java", "Go", "Rust", "C"], "default": "JS"} - }, - { - "name": "address", - "type": { - "name": "Address", - "type": "record", - "fields": [{"name": "zipcode", "type": "notAValidAvroType", "example": "53003"}] - } - }, - {"name": "someid", "type": "string", "logicalType": "uuid"} - ] - } -} \ No newline at end of file diff --git a/test/schema-parser/avro/asyncapi-avro-invalid.json b/test/schema-parser/avro/asyncapi-avro-invalid.json deleted file mode 100644 index ab504a1c1..000000000 --- a/test/schema-parser/avro/asyncapi-avro-invalid.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "schemaFormat": "application/vnd.apache.avro;version=1.9.0", - "payload": { - "isThisAnAvroSchema": "no" - } -} \ No newline at end of file diff --git a/test/schema-parser/avro/avro-schema-parser.spec.ts b/test/schema-parser/avro/avro-schema-parser.spec.ts deleted file mode 100644 index 41ff8118c..000000000 --- a/test/schema-parser/avro/avro-schema-parser.spec.ts +++ /dev/null @@ -1,441 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { AvroSchemaParser, avroToJsonSchema } from '../../../src/schema-parser/avro-schema-parser'; - -import type { ParseSchemaInput } from '../../../src/schema-parser'; - -const inputWithAvro182 = toParseInput(fs.readFileSync(path.resolve(__dirname, './asyncapi-avro-1.8.2.json'), 'utf8')); -const outputWithAvro182 = '{"type":"object","required":["name","favoriteProgrammingLanguage","address"],"properties":{"name":{"type":"string","examples":["Donkey"]},"age":{"oneOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}],"default":null},"favoriteProgrammingLanguage":{"type":"string","enum":["JS","Java","Go","Rust","C"]},"address":{"type":"object","x-parser-schema-id":"Address","required":["zipcode"],"properties":{"zipcode":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[53003]}}}}}'; - -const inputWithAvro190 = toParseInput(fs.readFileSync(path.resolve(__dirname, './asyncapi-avro-1.9.0.json'), 'utf8')); -const outputWithAvro190 = '{"type":"object","required":["name","favoriteProgrammingLanguage","address","someid"],"properties":{"name":{"type":"string","examples":["Donkey"]},"age":{"oneOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[123]},{"type":"null"}],"default":null},"favoriteProgrammingLanguage":{"type":"string","enum":["JS","Java","Go","Rust","C"],"default":"JS"},"address":{"type":"object","x-parser-schema-id":"Address","required":["zipcode"],"properties":{"zipcode":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[53003]}}},"someid":{"type":"string","format":"uuid"}},"x-parser-schema-id":"Person"}'; - -const inputWithAvro190WithNamespace = toParseInput(fs.readFileSync(path.resolve(__dirname, './asyncapi-avro-1.9.0-namespace.json'), 'utf8')); -const outputWithAvro190WithNamespace = '{"type":"object","required":["name","favoriteProgrammingLanguage","address","someid"],"properties":{"name":{"type":"string","examples":["Donkey"]},"age":{"oneOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[123]},{"type":"null"}],"default":null},"favoriteProgrammingLanguage":{"type":"string","enum":["JS","Java","Go","Rust","C"],"default":"JS"},"address":{"type":"object","x-parser-schema-id":"Address","required":["zipcode"],"properties":{"zipcode":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[53003]}}},"someid":{"type":"string","format":"uuid"}},"x-parser-schema-id":"com.company.Person"}'; - -const inputWithAvro190WithBindings = toParseInput(fs.readFileSync(path.resolve(__dirname, './asyncapi-avro-1.9.0-bindings.json'), 'utf8')); -const outputWithAvro190WithBindings = '{"type":"object","required":["name","favoriteProgrammingLanguage","address","someid"],"properties":{"name":{"type":"string","examples":["Donkey"]},"age":{"oneOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[123]},{"type":"null"}],"default":null},"favoriteProgrammingLanguage":{"type":"string","enum":["JS","Java","Go","Rust","C"],"default":"JS"},"address":{"type":"object","x-parser-schema-id":"Address","required":["zipcode"],"properties":{"zipcode":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[53003]}}},"someid":{"type":"string","format":"uuid"}},"x-parser-schema-id":"com.company.Person"}'; -const outputWithAvro190WithBindingsKafkaKeyTransformed = '{"type":"object","required":["name","favoriteProgrammingLanguage","address","someid"],"properties":{"name":{"type":"string","examples":["Donkey"]},"age":{"oneOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[123]},{"type":"null"}],"default":null},"favoriteProgrammingLanguage":{"type":"string","enum":["JS","Java","Go","Rust","C"],"default":"JS"},"address":{"type":"object","required":["zipcode"],"properties":{"zipcode":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[53003]}},"x-parser-schema-id":"Address"},"someid":{"type":"string","format":"uuid"}},"x-parser-schema-id":"com.company.Person"}'; -const outputWithAvro190WithBindingsKafkaKeyOriginal = '{"name":"Person","namespace":"com.company","type":"record","fields":[{"name":"name","type":"string","example":"Donkey"},{"name":"age","type":["null","int"],"default":null,"example":123},{"name":"favoriteProgrammingLanguage","type":{"name":"ProgrammingLanguage","type":"enum","symbols":["JS","Java","Go","Rust","C"],"default":"JS"}},{"name":"address","type":{"name":"Address","type":"record","fields":[{"name":"zipcode","type":"int","example":53003}]}},{"name":"someid","type":"string","logicalType":"uuid"}]}'; - -const inputWithAvroAdditionalAttributes = toParseInput(fs.readFileSync(path.resolve(__dirname, './asyncapi-avro-1.9.0-additional-attributes.json'), 'utf8')); -const outputWithAvroAdditionalAttributes = '{"type":"object","required":["name","serialNo","favoriteProgrammingLanguage","certifications","address","weight","height","someid"],"properties":{"name":{"type":"string","examples":["Donkey"],"minLength":0},"serialNo":{"type":"string","minLength":0,"maxLength":50},"email":{"oneOf":[{"type":"string","examples":["donkey@asyncapi.com"],"pattern":"^[\\\\w-\\\\.]+@([\\\\w-]+\\\\.)+[\\\\w-]{2,4}$"},{"type":"null"}]},"age":{"oneOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[123],"exclusiveMinimum":0,"exclusiveMaximum":200},{"type":"null"}],"default":null},"favoriteProgrammingLanguage":{"type":"string","enum":["JS","Java","Go","Rust","C"],"default":"JS"},"certifications":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":500,"uniqueItems":true},"address":{"type":"object","x-parser-schema-id":"Address","required":["zipcode"],"properties":{"zipcode":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[53003]},"country":{"oneOf":[{"type":"string"},{"type":"null"}]}}},"weight":{"type":"number","format":"float","examples":[65.1],"minimum":0,"maximum":500},"height":{"type":"number","format":"double","examples":[1.85],"minimum":0,"maximum":3},"someid":{"type":"string","format":"uuid"}},"x-parser-schema-id":"com.company.Person"}'; - -const inputWithInvalidAvro = toParseInput(fs.readFileSync(path.resolve(__dirname, './asyncapi-avro-invalid.json'), 'utf8')); -const inputWithBrokenAvro = toParseInput(fs.readFileSync(path.resolve(__dirname, './asyncapi-avro-broken.json'), 'utf8')); - -const inputWithSubAvro190 = toParseInput(fs.readFileSync(path.resolve(__dirname, './asyncapi-avro-111-1.9.0.json'), 'utf8')); -const outputWithSubAvro190 = '{"type":"object","required":["metadata","auth_code","triggered_by"],"properties":{"metadata":{"type":"object","x-parser-schema-id":"com.foo.EventMetadata","required":["id","timestamp"],"properties":{"id":{"type":"string","format":"uuid","description":"Unique identifier for this specific event"},"timestamp":{"type":"integer","minimum":-9223372036854776000,"maximum":9223372036854776000,"description":"Instant the event took place (not necessary when it was published)"},"correlation_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"id of the event that resulted in this\\nevent being published (optional)","default":null},"publisher_context":{"oneOf":[{"type":"object","additionalProperties":{"type":"string"}},{"type":"null"}],"description":"optional set of key-value pairs of context to be echoed back\\nin any resulting message (like a richer\\ncorrelationId.\\n\\nThese values are likely only meaningful to the publisher\\nof the correlated event","default":null}},"description":"Metadata to be associated with every published event"},"auth_code":{"type":"object","x-parser-schema-id":"com.foo.EncryptedString","required":["value","nonce","key"],"properties":{"value":{"type":"string","description":"A sequence of bytes that has been AES encrypted in CTR mode."},"nonce":{"type":"string","description":"A nonce, used by the CTR encryption mode for our encrypted value. Not encrypted, not a secret."},"key":{"type":"string","description":"An AES key, used to encrypt the value field, that has itself been encrypted using RSA."}},"description":"Encrypted auth_code received when user authorizes the app."},"refresh_token":{"type":"object","required":["value","nonce","key"],"properties":{"value":{"type":"string","description":"A sequence of bytes that has been AES encrypted in CTR mode."},"nonce":{"type":"string","description":"A nonce, used by the CTR encryption mode for our encrypted value. Not encrypted, not a secret."},"key":{"type":"string","description":"An AES key, used to encrypt the value field, that has itself been encrypted using RSA."}},"description":"Encrypted auth_code received when user authorizes the app.","x-parser-schema-id":"com.foo.EncryptedString"},"triggered_by":{"type":"string","format":"uuid","description":"ID of the user who triggered this event."}},"description":"An example schema to illustrate the issue","x-parser-schema-id":"com.foo.connections.ConnectionRequested"}'; - -const inputWithOneOfReferenceAvro190 = toParseInput(fs.readFileSync(path.resolve(__dirname, './asyncapi-avro-113-1.9.0.json'), 'utf8')); -const outputWithOneOfReferenceAvro190 = '{"oneOf":[{"type":"object","required":["streetaddress","city"],"properties":{"streetaddress":{"type":"string"},"city":{"type":"string"}},"x-parser-schema-id":"com.example.Address"},{"type":"object","required":["firstname","lastname"],"properties":{"firstname":{"type":"string"},"lastname":{"type":"string"},"address":{"type":"object","required":["streetaddress","city"],"properties":{"streetaddress":{"type":"string"},"city":{"type":"string"}},"x-parser-schema-id":"com.example.Address"}},"x-parser-schema-id":"com.example.Person"}]}'; - -const inputWithRecordReferencesAvro190 = toParseInput(fs.readFileSync(path.resolve(__dirname, './asyncapi-avro-148-1.9.0.json'), 'utf8')); -const outputWithRecordReferencesAvro190 = '{"type":"object","required":["Record1","simpleField"],"properties":{"Record1":{"type":"object","required":["string"],"properties":{"string":{"type":"string","description":"field in Record1"}},"description":"Reused in other fields","x-parser-schema-id":"Record1"},"FieldThatDefineRecordInUnion":{"oneOf":[{"type":"object","required":["number"],"properties":{"number":{"type":"integer","minimum":0,"maximum":2,"description":"field in RecordDefinedInUnion"}},"x-parser-schema-id":"com.example.model.RecordDefinedInUnion"},{"type":"null"}],"default":null},"FieldThatReuseRecordDefinedInUnion":{"oneOf":[{},{"type":"null"}],"default":null},"FieldThatReuseRecord1":{"oneOf":[{},{"type":"null"}],"default":null},"simpleField":{"type":"string"}},"x-parser-schema-id":"com.example.RecordWithReferences"}'; - -describe('AvroSchemaParser', function () { - const parser = AvroSchemaParser(); - - it('should return Mime Types', async function () { - expect(parser.getMimeTypes()).not.toEqual([]); - }); - - it('should parse Avro schema 1.8.2', async function() { - await doTest(inputWithAvro182, outputWithAvro182); - }); - - it('should parse Avro schema 1.9.0', async function() { - await doTest(inputWithAvro190, outputWithAvro190); - }); - - it('should parse Avro schema 1.9.0 with a namespace', async function() { - await doTest(inputWithAvro190WithNamespace, outputWithAvro190WithNamespace); - }); - - it('should parse Avro schema in kafka bindings', async function() { - const input = {...inputWithAvro190WithBindings}; - const result = await parser.parse(input); - - // Check that the return value of parse() is the expected JSON Schema. - expect(result).toEqual(JSON.parse(outputWithAvro190WithBindings)); - - // Check that the message got modified, i.e. adding extensions, setting the payload, etc. - const message = (input.meta as any).message; - expect(JSON.stringify(message.bindings.kafka.key)).toEqual(outputWithAvro190WithBindingsKafkaKeyTransformed); - expect(JSON.stringify(message['x-parser-original-bindings-kafka-key'])).toEqual(outputWithAvro190WithBindingsKafkaKeyOriginal); - }); - - it('should handle additional Avro attributes like', async function() { - await doTest(inputWithAvroAdditionalAttributes, outputWithAvroAdditionalAttributes); - }); - - it('should validate invalid Avro schemas as invalid', async function() { - const result = await parser.validate(inputWithInvalidAvro); - expect(result).toEqual([{message: 'unknown type: undefined', path: []}]); - }); - - it('should validate valid Avro schemas', async function() { - const result = await parser.validate(inputWithAvro182); - expect(result).toHaveLength(0); - }); - - it('should identify bugs in Avro schemas', async function() { - const result = await parser.validate(inputWithBrokenAvro); - expect(result).toEqual([{message: 'undefined type name: notAValidAvroType', path: []}]); - }); - - it('Issue #111 should handle pre defined records in schemas', async function() { - await doTest(inputWithSubAvro190, outputWithSubAvro190); - }); - - it('Issue #113 should handle pre defined records in top level oneOf schema', async function() { - await doTest(inputWithOneOfReferenceAvro190, outputWithOneOfReferenceAvro190); - }); - - it('Issue #148 should handle records references in a top level record', async function() { - await doTest(inputWithRecordReferencesAvro190, outputWithRecordReferencesAvro190); - }); - - async function doTest(originalInput: ParseSchemaInput, expectedOutput: any) { - 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)); - } -}); - -const BYTES_PATTERN = '^[\u0000-\u00ff]*$'; -const INT_MIN = Math.pow(-2, 31); -const INT_MAX = Math.pow(2, 31) - 1; -const LONG_MIN = Math.pow(-2, 63); -const LONG_MAX = Math.pow(2, 63) - 1; - -describe('avroToJsonSchema()', function () { - it('transforms null values', async function () { - const result = await avroToJsonSchema({type: 'null'}); - expect(result).toEqual({type: 'null'}); - }); - - it('transforms boolean values', async function () { - const result = await avroToJsonSchema({type: 'boolean'}); - expect(result).toEqual({type: 'boolean'}); - }); - - it('transforms int values', async function () { - const result = await avroToJsonSchema({type: 'int'}); - expect(result).toEqual({type: 'integer', minimum: INT_MIN, maximum: INT_MAX}); - }); - - it('transforms long values', async function () { - const result = await avroToJsonSchema({type: 'long'}); - expect(result).toEqual({type: 'integer', minimum: LONG_MIN, maximum: LONG_MAX}); - }); - - it('transforms float values', async function () { - const result = await avroToJsonSchema({type: 'float'}); - expect(result).toEqual({type: 'number', format: 'float'}); - }); - - it('transforms double values', async function () { - const result = await avroToJsonSchema({type: 'double'}); - expect(result).toEqual({type: 'number', format: 'double'}); - }); - - it('transforms bytes values', async function () { - const result = await avroToJsonSchema({type: 'bytes'}); - expect(result).toEqual({type: 'string', pattern: BYTES_PATTERN}); - }); - - it('transforms string values', async function () { - const result = await avroToJsonSchema({type: 'string'}); - expect(result).toEqual({type: 'string'}); - }); - - it('transforms fixed values', async function () { - const schema = {type: 'fixed', size: 5}; - const result = await avroToJsonSchema(schema as any); - expect(result).toEqual({type: 'string', pattern: BYTES_PATTERN, minLength: 5, maxLength: 5}); - }); - - it('transforms union values', async function () { - const result = await avroToJsonSchema(['null', 'int']); - expect(result).toEqual({oneOf: [{type: 'integer', minimum: INT_MIN, maximum: INT_MAX}, {type: 'null'}]}); - }); - - it('transforms map values', async function () { - const result = await avroToJsonSchema({type: 'map', values: 'long'}); - expect(result).toEqual({ - type: 'object', - additionalProperties: {type: 'integer', minimum: LONG_MIN, maximum: LONG_MAX} - }); - }); - - it('transforms array values', async function () { - const result = await avroToJsonSchema({type: 'array', items: 'long'}); - expect(result).toEqual({type: 'array', items: {type: 'integer', minimum: LONG_MIN, maximum: LONG_MAX}}); - }); - - it('transforms enum values', async function () { - const schema = { - type: 'enum', - doc: 'My test enum', - symbols: ['one', 'two', 'three'], - default: 'one' - }; - - const result = await avroToJsonSchema(schema as any); - expect(result).toEqual({ - type: 'string', - enum: ['one', 'two', 'three'], - default: 'one', - description: 'My test enum' - }); - }); - - it('transforms record values', async function () { - const result = await avroToJsonSchema({ - type: 'record', - doc: 'My test record', - name: 'MyName', - fields: [ - {name: 'key1', type: 'long', doc: 'Key1 docs'}, - {name: 'key2', type: 'string', default: 'value2', doc: 'Key2 docs'}, - ] - }); - expect(result).toEqual({ - type: 'object', - 'x-parser-schema-id': 'MyName', - description: 'My test record', - required: ['key1'], - properties: { - key1: { - type: 'integer', - minimum: LONG_MIN, - maximum: LONG_MAX, - description: 'Key1 docs', - }, - key2: { - type: 'string', - default: 'value2', - description: 'Key2 docs', - }, - } - }); - }); - - it('assigns default values correctly in types and fields', async function () { - expect( - await avroToJsonSchema({type: 'int', default: 1}) - ).toEqual({type: 'integer', minimum: INT_MIN, maximum: INT_MAX, default: 1}); - - const schema = {type: 'record', fields: [{name: 'field1', type: 'string', default: 'AsyncAPI rocks!'}]}; - expect( - await avroToJsonSchema(schema as any) - ).toEqual({ - type: 'object', - properties: {field1: {type: 'string', default: 'AsyncAPI rocks!'}} - }); - }); - - it('treats array Avro documents as unions', async function () { - expect( - await avroToJsonSchema([{type: 'int', default: 1}, 'string']) - ).toEqual({ - oneOf: [ - {type: 'integer', minimum: INT_MIN, maximum: INT_MAX, default: 1}, - {type: 'string'}, - ] - }); - }); - - it('support record references', async function () { - const result = await avroToJsonSchema({ - type: 'record', - doc: 'My test record', - name: 'MyName', - fields: [ - { - name: 'key1', - type: {type: 'record', name: 'recordKey1', doc: 'Key1 docs', fields: [{type: 'string', name: 'test'}]} - }, - {name: 'key2', type: {type: 'record', fields: [{name: 'recordReference', type: 'recordKey1'}]}}, - ] - } as any); - expect(result).toEqual({ - description: 'My test record', - properties: { - key1: { - description: 'Key1 docs', - properties: { - test: { - type: 'string' - } - }, - required: [ - 'test' - ], - type: 'object', - 'x-parser-schema-id': 'recordKey1' - }, - key2: { - properties: { - recordReference: { - description: 'Key1 docs', - properties: { - test: { - type: 'string' - } - }, - required: [ - 'test' - ], - type: 'object', - 'x-parser-schema-id': 'recordKey1' - } - }, - type: 'object' - } - }, - required: [ - 'key1', - 'key2' - ], - type: 'object', - 'x-parser-schema-id': 'MyName' - }); - }); -}); - -function toParseInput(raw: string): ParseSchemaInput { - const message = JSON.parse(raw); - return { - asyncapi: { - semver: { - version: '2.4.0', - major: 2, - minor: 4, - patch: 0 - }, - source: '', - parsed: {} as any, - }, - data: message.payload, - meta: { - message, - }, - path: [], - schemaFormat: message.schemaFormat, - defaultSchemaFormat: 'application/vnd.aai.asyncapi;version=2.4.0', - }; -} - -describe('supportExampleAttribute', function () { - it('transforms example on union values', async function () { - const schema = { - type: 'record', - name: 'MyName', - fields: [ - {name: 'example', type: ['null', 'int'], example: 3} - ] - }; - const result = await avroToJsonSchema(schema as any); - expect(result).toEqual({ - type: 'object', - 'x-parser-schema-id': 'MyName', - properties: { - example: { - oneOf: [{ - type: 'integer', - minimum: INT_MIN, - maximum: INT_MAX, - examples: [3] - }, { - type: 'null' - } - ] - } - } - }); - }); -}); - -describe('requiredAttributesMapping()', function () { - it('support required fields', async function () { - const result = await avroToJsonSchema({ - type: 'record', - doc: 'My test record', - name: 'MyName', - fields: [ - {name: 'required1', type: ['int', 'long']}, - {name: 'required2', type: ['long']}, - {name: 'notRequiredBecauseDefault', type: 'string', default: 'value2'}, - {name: 'notRequiredBecauseUnionWithNull', type: ['null', 'string']}, - ] - }); - expect(result).toMatchObject({ - type: 'object', - 'x-parser-schema-id': 'MyName', - description: 'My test record', - required: ['required1', 'required2'] - }); - }); -}); - -describe('additionalAttributesMapping()', function () { - it('support minimum and maximum for float', async function () { - const result = await avroToJsonSchema({type: 'float', minimum: 0, maximum: 10}); - expect(result).toEqual({type: 'number', format: 'float', minimum: 0, maximum: 10}); - }); - - it('support exclusiveMinimum and exclusiveMaximum for float', async function () { - const result = await avroToJsonSchema({type: 'float', exclusiveMinimum: 0, exclusiveMaximum: 10}); - expect(result).toEqual({type: 'number', format: 'float', exclusiveMinimum: 0, exclusiveMaximum: 10}); - }); - - it('support minimum and maximum for double', async function () { - const result = await avroToJsonSchema({type: 'double', minimum: 0, maximum: 10}); - expect(result).toEqual({type: 'number', format: 'double', minimum: 0, maximum: 10}); - }); - - it('support exclusiveMinimum and exclusiveMaximum for double', async function () { - const result = await avroToJsonSchema({type: 'double', exclusiveMinimum: 0, exclusiveMaximum: 10}); - expect(result).toMatchObject({type: 'number', format: 'double', exclusiveMinimum: 0, exclusiveMaximum: 10}); - }); - - it('support minimum and maximum for long and int', async function () { - let result = await avroToJsonSchema({type: 'long', minimum: 0, maximum: 10}); - expect(result).toEqual({type: 'integer', minimum: 0, maximum: 10}); - - result = await avroToJsonSchema({type: 'int', minimum: 0, maximum: 10}); - expect(result).toEqual({type: 'integer', minimum: 0, maximum: 10}); - }); - - it('long and int type support exclusiveMinimum and exclusiveMaximum', async function () { - let result = await avroToJsonSchema({type: 'long', exclusiveMinimum: 0, exclusiveMaximum: 10}); - expect(result).toMatchObject({type: 'integer', exclusiveMinimum: 0, exclusiveMaximum: 10}); - - result = await avroToJsonSchema({type: 'int', exclusiveMinimum: 0, exclusiveMaximum: 10}); - expect(result).toMatchObject({type: 'integer', exclusiveMinimum: 0, exclusiveMaximum: 10}); - }); - - it('support pattern, minLength and maxLength for string', async function () { - const result = await avroToJsonSchema({type: 'string', pattern: '$pattern^', minLength: 1, maxLength: 10}); - expect(result).toEqual({type: 'string', pattern: '$pattern^', minLength: 1, maxLength: 10}); - }); - - it('handle non-negative integer value for minLength and maxLength', async function () { - const result = await avroToJsonSchema({type: 'string', minLength: -1, maxLength: -110}); - expect(result).toEqual({type: 'string'}); - }); - - it('support minItems and maxItems for array', async function () { - const result = await avroToJsonSchema({type: 'array', items: 'long', minItems: 0, maxItems: 10}); - expect(result).toMatchObject({type: 'array', items: {type: 'integer'}, minItems: 0, maxItems: 10}); - }); - - it('support uniqueItems for array', async function () { - const result = await avroToJsonSchema({type: 'array', items: 'long', uniqueItems: true}); - expect(result).toMatchObject({type: 'array', items: {type: 'integer'}, uniqueItems: true}); - }); -}); \ No newline at end of file diff --git a/test/schema-parser/openapi/invalid.json b/test/schema-parser/openapi/invalid.json deleted file mode 100644 index 838c5a61d..000000000 --- a/test/schema-parser/openapi/invalid.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "schemaFormat":"application/vnd.oai.openapi;version=3.0.0", - "payload": { - "properties": { - "name": { - "type": "nonexistent" - }, - "surname": { - "format": {} - } - } - } -} \ No newline at end of file diff --git a/test/schema-parser/openapi/openapi-schema-parser.spec.ts b/test/schema-parser/openapi/openapi-schema-parser.spec.ts deleted file mode 100644 index 7165ab916..000000000 --- a/test/schema-parser/openapi/openapi-schema-parser.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { OpenAPISchemaParser } from '../../../src/schema-parser/openapi-schema-parser'; -import * as fs from 'fs'; -import * as path from 'path'; - -import type { ParseSchemaInput, ValidateSchemaInput } from '../../../src/schema-parser'; - -const inputWithValidOpenApi3 = toParseInput(fs.readFileSync(path.resolve(__dirname, './valid.json'), 'utf8')); -const outputWithValidOpenApi3 = '{"type":["object","null"],"properties":{"name":{"type":"string"},"discriminatorTest":{"discriminator":{"propertyName":"objectType"},"oneOf":[{"type":"object","properties":{"objectType":{"type":"string"},"prop1":{"type":"string"}}},{"type":"object","properties":{"objectType":{"type":"string"},"prop2":{"type":"string"}}}]},"test":{"type":"object","properties":{"testing":{"type":"string"}}}},"examples":[{"name":"Fran"}]}'; - -const inputWithInvalidOpenApi3 = toParseInput(fs.readFileSync(path.resolve(__dirname, './invalid.json'), 'utf8')); - -describe('OpenAPISchemaParser', function () { - const parser = OpenAPISchemaParser(); - - it('should return Mime Types', async function () { - expect(parser.getMimeTypes()).not.toEqual([]); - }); - - it('should parse OpenAPI 3', async function() { - await doParseTest(inputWithValidOpenApi3, outputWithValidOpenApi3); - }); - - it('should validate valid OpenAPI 3', async function() { - const result = await parser.validate(inputWithValidOpenApi3); - expect(result).toHaveLength(0); - }); - - it('should validate invalid OpenAPI 3', async function() { - const result = await parser.validate(inputWithInvalidOpenApi3); - expect(result).toHaveLength(6); - expect(result).toEqual([ - { - message: 'must be equal to one of the allowed values', - path: ['channels', 'myChannel', 'publish', 'message', 'payload', 'properties', 'name', 'type'] - }, - { - message: 'must have required property \'$ref\'', - path: ['channels', 'myChannel', 'publish', 'message', 'payload', 'properties', 'name'] - }, - { - message: 'must match exactly one schema in oneOf', - path: ['channels', 'myChannel', 'publish', 'message', 'payload', 'properties','name'] - }, - { - message: 'must be string', - path: ['channels', 'myChannel', 'publish', 'message', 'payload', 'properties','surname','format'] - }, - { - message: 'must have required property \'$ref\'', - path: ['channels', 'myChannel', 'publish', 'message', 'payload', 'properties','surname'] - }, - { - message: 'must match exactly one schema in oneOf', - path: ['channels', 'myChannel', 'publish', 'message', 'payload', 'properties','surname'] - } - ]); - }); - - async function doParseTest(originalInput: ParseSchemaInput, expectedOutput: any) { - 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)); - } -}); - -function toParseInput(raw: string): ParseSchemaInput | ValidateSchemaInput { - const message = JSON.parse(raw); - return { - asyncapi: { - semver: { - version: '2.4.0', - major: 2, - minor: 4, - patch: 0 - }, - source: '', - parsed: {} as any, - }, - data: message.payload, - meta: { - message, - }, - path: ['channels', 'myChannel', 'publish', 'message', 'payload'], - schemaFormat: message.schemaFormat, - defaultSchemaFormat: 'application/vnd.aai.asyncapi;version=2.4.0', - }; -} diff --git a/test/schema-parser/openapi/valid.json b/test/schema-parser/openapi/valid.json deleted file mode 100644 index b1f8ecad4..000000000 --- a/test/schema-parser/openapi/valid.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "schemaFormat":"application/vnd.oai.openapi;version=3.0.0", - "payload":{ - "type":"object", - "nullable":true, - "example":{ - "name":"Fran" - }, - "properties":{ - "name":{ - "type":"string" - }, - "discriminatorTest":{ - "discriminator":{"propertyName": "objectType"}, - "oneOf":[ - { - "type":"object", - "properties":{ - "objectType":{ - "type":"string" - }, - "prop1":{ - "type":"string" - } - } - }, - { - "type":"object", - "properties":{ - "objectType":{ - "type":"string" - }, - "prop2":{ - "type":"string" - } - } - } - ] - }, - "test":{ - "type":"object", - "properties":{ - "testing":{ - "type":"string" - } - } - } - } - } - } \ No newline at end of file diff --git a/test/schema-parser/raml/complex.json b/test/schema-parser/raml/complex.json deleted file mode 100644 index f7cac2385..000000000 --- a/test/schema-parser/raml/complex.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "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/schema-parser/raml/invalid.json b/test/schema-parser/raml/invalid.json deleted file mode 100644 index 1ed399fb7..000000000 --- a/test/schema-parser/raml/invalid.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "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/schema-parser/raml/raml-schema-parser.spec.ts b/test/schema-parser/raml/raml-schema-parser.spec.ts deleted file mode 100644 index e3cbe66e0..000000000 --- a/test/schema-parser/raml/raml-schema-parser.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { RamlSchemaParser } from '../../../src/schema-parser/raml-schema-parser'; -import * as fs from 'fs'; -import * as path from 'path'; - -import type { ParseSchemaInput, ValidateSchemaInput } from '../../../src/schema-parser'; -import type { SchemaValidateResult } from '../../../src/types'; - -const inputWithSimpleRAML = toInput(fs.readFileSync(path.resolve(__dirname, './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, './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, './invalid.json'), 'utf8')); - -describe('parse()', function() { - const parser = RamlSchemaParser(); - - it('should parse simple RAML data types', async function() { - await doTest(inputWithSimpleRAML, outputWithSimpleRAML); - }); - - it('should parse complex RAML data types', async function() { - await doTest(inputWithComplexRAML, outputWithComplexRAML); - }); - - async function doTest(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)); - } -}); - -describe('validate()', function() { - const parser = RamlSchemaParser(); - - 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 - }); -}); - -function toInput(raw: string): ParseSchemaInput | ValidateSchemaInput { - const message = JSON.parse(raw); - return { - asyncapi: { - semver: { - version: '2.4.0', - major: 2, - minor: 4, - 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.4.0', - }; -} \ No newline at end of file diff --git a/test/schema-parser/raml/simple.json b/test/schema-parser/raml/simple.json deleted file mode 100644 index 4ba42fe61..000000000 --- a/test/schema-parser/raml/simple.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "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