Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: include reused types in required properties #246

Merged
merged 2 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading