diff --git a/src/index.ts b/src/index.ts index a3b26f6b..60f67db1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -298,6 +298,11 @@ async function processRecordSchema(avroDefinition: AvroSchema, recordCache: Reco // 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]); + + // check for cached fields that should be marked as required + const cachedProps = propsMap.get(field.name); + const cached = { name: field.name, ...cachedProps }; + requiredAttributesMapping(cached, jsonSchema, cached.default !== undefined); } else { const def = await convertAvroToJsonSchema(field.type, false, recordCache); diff --git a/test/avro-schema-parser.spec.ts b/test/avro-schema-parser.spec.ts index 48b6859d..8751b320 100644 --- a/test/avro-schema-parser.spec.ts +++ b/test/avro-schema-parser.spec.ts @@ -26,10 +26,10 @@ const inputWithInvalidAvro = toParseInput(fs.readFileSync(path.resolve(__dirname const inputWithBrokenAvro = toParseInput(fs.readFileSync(path.resolve(__dirname, './documents/asyncapi-avro-broken.json'), 'utf8')); const inputWithSubAvro190 = toParseInput(fs.readFileSync(path.resolve(__dirname, './documents/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 outputWithSubAvro190 = '{"type":"object","required":["metadata","auth_code","refresh_token","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, './documents/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 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","address"],"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, './documents/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"}'; @@ -37,6 +37,8 @@ const outputWithRecordReferencesAvro190 = '{"type":"object","required":["Record1 const inputWithValidAsyncAPI = fs.readFileSync(path.resolve(__dirname, './documents/valid-asyncapi.yaml'), 'utf8'); const inputWithInvalidAsyncAPI = fs.readFileSync(path.resolve(__dirname, './documents/invalid-asyncapi.yaml'), 'utf8'); +const inputWithReusedEnums = fs.readFileSync(path.resolve(__dirname, './documents/asyncapi-with-reused-enums.yaml'), 'utf8'); + describe('AvroSchemaParser', function () { const parser = AvroSchemaParser(); const coreParser = new Parser(); @@ -109,6 +111,12 @@ describe('AvroSchemaParser', function () { doParseCoreTest((document?.json()?.components?.messages?.testMessage as any)?.payload, outputWithAvro190); }); + it('should include reused types in required properties', async function() { + const { document } = await coreParser.parse(inputWithReusedEnums); + expect(document?.json()?.components?.messages?.example_message?.payload?.schema?.required) + .toEqual(['r1', 'r2', 'r3', 'r5']); + }); + it('should validate valid AsyncAPI', async function() { const diagnostics = await coreParser.validate(inputWithValidAsyncAPI); expect(filterDiagnostics(diagnostics, 'asyncapi2-schemas')).toHaveLength(0); @@ -338,6 +346,9 @@ describe('avroToJsonSchema()', function () { 'x-parser-schema-id': 'recordKey1' } }, + required: [ + 'recordReference' + ], type: 'object' } }, diff --git a/test/documents/asyncapi-with-reused-enums.yaml b/test/documents/asyncapi-with-reused-enums.yaml new file mode 100644 index 00000000..bca4929d --- /dev/null +++ b/test/documents/asyncapi-with-reused-enums.yaml @@ -0,0 +1,46 @@ +asyncapi: 3.0.0 +info: + title: AsyncAPI + version: 1.0.0 + description: AsyncAPI +channels: + example: + address: example + messages: + publish.message: + $ref: '#/components/messages/example_message' +operations: + example.publish: + action: receive + channel: + $ref: '#/channels/example' + messages: + - $ref: '#/channels/example/messages/publish.message' +components: + messages: + example_message: + name: example_event + payload: + schemaFormat: application/vnd.apache.avro;version=1.9.0 + schema: + type: record + name: ParentRecord + fields: + - name: r1 + type: + type: enum + name: MyEnum + symbols: + - A + - B + - C + - name: r2 + type: MyEnum + - name: r3 + type: MyEnum + - name: r4 + type: + - 'null' + - string + - name: r5 + type: string