Skip to content

Commit

Permalink
fix: include reused types in required properties (#246)
Browse files Browse the repository at this point in the history
  • Loading branch information
dalelane committed Feb 27, 2024
1 parent 5b6c01d commit 05fa317
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 2 deletions.
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
15 changes: 13 additions & 2 deletions test/avro-schema-parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,19 @@ 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"}';

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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -338,6 +346,9 @@ describe('avroToJsonSchema()', function () {
'x-parser-schema-id': 'recordKey1'
}
},
required: [
'recordReference'
],
type: 'object'
}
},
Expand Down
46 changes: 46 additions & 0 deletions test/documents/asyncapi-with-reused-enums.yaml
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 05fa317

Please sign in to comment.