From bcc76c4f87a50efbde5bbcde572ae84765b5b2a9 Mon Sep 17 00:00:00 2001 From: Radim Loskot Date: Fri, 7 May 2021 22:01:39 +0200 Subject: [PATCH] feat: add support to display examples --- tests/parse.test.js | 4 +-- tests/schemas/Person-1.8.2.avsc | 4 +-- tests/schemas/Person-1.9.0.avsc | 6 ++--- tests/to-json-schema.test.js | 2 +- to-json-schema.js | 47 +++++++++++++++++++++++++++++---- 5 files changed, 50 insertions(+), 13 deletions(-) diff --git a/tests/parse.test.js b/tests/parse.test.js index 427b35f8..44fd746f 100644 --- a/tests/parse.test.js +++ b/tests/parse.test.js @@ -4,10 +4,10 @@ const avroSchemaParser = require('..'); const parser = require('@asyncapi/parser'); const inputWithAvro190 = fs.readFileSync(path.resolve(__dirname, './asyncapi-avro-1.9.0.yaml'), 'utf8'); -const outputWithAvro190 = '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{"mychannel":{"publish":{"message":{"payload":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":""},"age":{"oneOf":[{"type":"null","x-parser-schema-id":""},{"type":"integer","minimum":-2147483648,"maximum":2147483647,"x-parser-schema-id":""}],"x-parser-schema-id":""},"favoriteProgrammingLanguage":{"type":"string","enum":["JS","Java","Go","Rust","C"],"default":"JS","x-parser-schema-id":""},"address":{"type":"object","properties":{"zipcode":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"x-parser-schema-id":""}},"x-parser-schema-id":""},"someid":{"type":"string","x-parser-schema-id":""}},"x-parser-schema-id":""},"x-parser-original-schema-format":"application/vnd.apache.avro;version=1.9.0","x-parser-original-payload":{"name":"Person","type":"record","fields":[{"name":"name","type":"string"},{"name":"age","type":["null","int"],"default":null},{"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"}]}},{"name":"someid","type":"uuid"}]},"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-parsed":true,"x-parser-message-name":""}}}},"x-parser-spec-parsed":true}'; +const outputWithAvro190 = '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{"mychannel":{"publish":{"message":{"payload":{"type":"object","properties":{"name":{"type":"string","examples":["Donkey"],"x-parser-schema-id":""},"age":{"oneOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[123],"x-parser-schema-id":""},{"type":"null","x-parser-schema-id":""}],"default":null,"x-parser-schema-id":""},"favoriteProgrammingLanguage":{"type":"string","enum":["JS","Java","Go","Rust","C"],"default":"JS","x-parser-schema-id":""},"address":{"type":"object","properties":{"zipcode":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[53003],"x-parser-schema-id":""}},"x-parser-schema-id":""},"someid":{"type":"string","x-parser-schema-id":""}},"x-parser-schema-id":""},"x-parser-original-schema-format":"application/vnd.apache.avro;version=1.9.0","x-parser-original-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":"uuid"}]},"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-parsed":true,"x-parser-message-name":""}}}},"x-parser-spec-parsed":true}'; const inputWithAvro182 = fs.readFileSync(path.resolve(__dirname, './asyncapi-avro-1.8.2.yaml'), 'utf8'); -const outputWithAvro182 = '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{"mychannel":{"publish":{"message":{"payload":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":""},"age":{"oneOf":[{"type":"null","x-parser-schema-id":""},{"type":"integer","minimum":-2147483648,"maximum":2147483647,"x-parser-schema-id":""}],"x-parser-schema-id":""},"favoriteProgrammingLanguage":{"type":"string","enum":["JS","Java","Go","Rust","C"],"x-parser-schema-id":""},"address":{"type":"object","properties":{"zipcode":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"x-parser-schema-id":""}},"x-parser-schema-id":""}},"x-parser-schema-id":""},"x-parser-original-schema-format":"application/vnd.apache.avro;version=1.8.2","x-parser-original-payload":{"name":"Person","type":"record","fields":[{"name":"name","type":"string"},{"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"}]}}]},"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-parsed":true,"x-parser-message-name":""}}}},"x-parser-spec-parsed":true}'; +const outputWithAvro182 = '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{"mychannel":{"publish":{"message":{"payload":{"type":"object","properties":{"name":{"type":"string","examples":["Donkey"],"x-parser-schema-id":""},"age":{"oneOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"x-parser-schema-id":""},{"type":"null","x-parser-schema-id":""}],"default":null,"x-parser-schema-id":""},"favoriteProgrammingLanguage":{"type":"string","enum":["JS","Java","Go","Rust","C"],"x-parser-schema-id":""},"address":{"type":"object","properties":{"zipcode":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"examples":[53003],"x-parser-schema-id":""}},"x-parser-schema-id":""}},"x-parser-schema-id":""},"x-parser-original-schema-format":"application/vnd.apache.avro;version=1.8.2","x-parser-original-payload":{"name":"Person","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"}]}}]},"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-parsed":true,"x-parser-message-name":""}}}},"x-parser-spec-parsed":true}'; parser.registerSchemaParser(avroSchemaParser); diff --git a/tests/schemas/Person-1.8.2.avsc b/tests/schemas/Person-1.8.2.avsc index 7c99f907..8d9dcbd5 100644 --- a/tests/schemas/Person-1.8.2.avsc +++ b/tests/schemas/Person-1.8.2.avsc @@ -2,7 +2,7 @@ "name": "Person", "type": "record", "fields": [ - {"name": "name", "type": "string"}, + {"name": "name", "type": "string", example: "Donkey"}, {"name": "age", "type": ["null", "int"], "default": null}, { "name": "favoriteProgrammingLanguage", @@ -13,7 +13,7 @@ "type": { "name": "Address", "type": "record", - "fields": [{"name": "zipcode", "type": "int"}] + "fields": [{"name": "zipcode", "type": "int", example: "53003"}] } } ] diff --git a/tests/schemas/Person-1.9.0.avsc b/tests/schemas/Person-1.9.0.avsc index 01b41e29..ab4660f5 100644 --- a/tests/schemas/Person-1.9.0.avsc +++ b/tests/schemas/Person-1.9.0.avsc @@ -2,8 +2,8 @@ "name": "Person", "type": "record", "fields": [ - {"name": "name", "type": "string"}, - {"name": "age", "type": ["null", "int"], "default": null}, + {"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"} @@ -13,7 +13,7 @@ "type": { "name": "Address", "type": "record", - "fields": [{"name": "zipcode", "type": "int"}] + "fields": [{"name": "zipcode", "type": "int", example: "53003"}] } }, {"name": "someid", "type": "uuid"} diff --git a/tests/to-json-schema.test.js b/tests/to-json-schema.test.js index 54ed71c5..2802275f 100644 --- a/tests/to-json-schema.test.js +++ b/tests/to-json-schema.test.js @@ -59,7 +59,7 @@ describe('avroToJsonSchema()', function () { it('transforms union values', async function () { const result = await avroToJsonSchema(['null', 'int']); - expect(result).toEqual({ oneOf: [{ type: 'null' }, { type: 'integer', minimum: INT_MIN, maximum: INT_MAX }] }); + expect(result).toEqual({ oneOf: [{ type: 'integer', minimum: INT_MIN, maximum: INT_MAX }, { type: 'null' }] }); }); it('transforms map values', async function () { diff --git a/to-json-schema.js b/to-json-schema.js index f69180b8..29ab260d 100644 --- a/to-json-schema.js +++ b/to-json-schema.js @@ -21,17 +21,52 @@ const typeMappings = { uuid: 'string', }; +const commonAttributesMapping = (avroDefinition, jsonSchema) => { + if (avroDefinition.doc) jsonSchema.description = avroDefinition.doc; + if (avroDefinition.default !== undefined) jsonSchema.default = avroDefinition.default; +}; + +const exampleAttributeMapping = (typeInput, example, jsonSchemaInput) => { + 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]; + jsonSchema = jsonSchema.oneOf[0]; + } + + 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]; + } +}; + module.exports.avroToJsonSchema = async function avroToJsonSchema(avroDefinition) { const jsonSchema = {}; const isUnion = Array.isArray(avroDefinition); if (isUnion) { jsonSchema.oneOf = []; + let nullDef = null; for (const avroDef of avroDefinition) { const def = await avroToJsonSchema(avroDef); - jsonSchema.oneOf.push(def); + const defType = avroDef.type || avroDef; + // To prefer non-null values in the examples put null as the last element + if (defType === 'null') nullDef = def; else jsonSchema.oneOf.push(def); } + if (nullDef) jsonSchema.oneOf.push(nullDef); + return jsonSchema; } @@ -70,16 +105,18 @@ module.exports.avroToJsonSchema = async function avroToJsonSchema(avroDefinition const propsMap = new Map(); for (const field of avroDefinition.fields) { const def = await avroToJsonSchema(field.type); - if (field.doc) def.description = field.doc; - if (field.default) def.default = field.default; + + commonAttributesMapping(field, def); + exampleAttributeMapping(field.type, field.example, def); + propsMap.set(field.name, def); } jsonSchema.properties = Object.fromEntries(propsMap.entries()); break; } - if (avroDefinition.doc) jsonSchema.description = avroDefinition.doc; - if (avroDefinition.default !== undefined) jsonSchema.default = avroDefinition.default; + commonAttributesMapping(avroDefinition, jsonSchema); + exampleAttributeMapping(type, avroDefinition.example, jsonSchema); return jsonSchema; };