Skip to content

Commit cafcc85

Browse files
committed
feat: added avro validation
1 parent 0647187 commit cafcc85

File tree

4 files changed

+104
-3
lines changed

4 files changed

+104
-3
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
"uuid": "^11.1.0",
5151
"winston": "^3.17.0",
5252
"ws": "^8.2.3",
53-
"yaml": "^2.6.1"
53+
"yaml": "^2.6.1",
54+
"avsc": "^5.7.7"
5455
},
5556
"devDependencies": {
5657
"@asyncapi/minimaltemplate": "./test/fixtures/minimaltemplate",

src/domains/services/validation.service.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { Specification } from '@models/SpecificationFile';
3131
import { ParseOptions } from '@asyncapi/parser';
3232
import { ParserOptions } from '@asyncapi/parser/cjs/parser';
3333
import { calculateScore } from '@/utils/scoreCalculator';
34+
import { Type as AvroType } from 'avsc';
3435

3536
import { ConfigService } from './config.service';
3637

@@ -282,11 +283,66 @@ export class ValidationService extends BaseService {
282283
},
283284
);
284285

285-
const status = this.determineDiagnosticsStatus(diagnostics, options);
286+
// Additional diagnostics for Avro payload schemas using avsc
287+
const extraDiagnostics: Diagnostic[] = [];
288+
try {
289+
const docJson: any = document?.json ? document.json() : undefined;
290+
if (docJson && docJson.components && docJson.components.messages) {
291+
const messages = docJson.components.messages as Record<string, any>;
292+
for (const [msgKey, msgVal] of Object.entries(messages)) {
293+
const payload = (msgVal as any)?.payload;
294+
if (!payload || typeof payload !== 'object') {
295+
continue;
296+
}
297+
const schemaFormat: string | undefined = payload.schemaFormat || payload.schemaformat;
298+
if (!schemaFormat || typeof schemaFormat !== 'string') {
299+
continue;
300+
}
301+
const isAvro = (/application\/vnd\.apache\.avro\+(json|yaml)/i).test(schemaFormat);
302+
if (!isAvro) {
303+
continue;
304+
}
305+
306+
const avroSchema = (payload as any).schema;
307+
if (!avroSchema || typeof avroSchema !== 'object') {
308+
extraDiagnostics.push({
309+
code: 'avro-schema-missing',
310+
message: 'Avro payload has schemaFormat set to Avro but no schema provided.',
311+
path: ['components', 'messages', msgKey, 'payload', 'schema'],
312+
severity: DiagnosticSeverity.Error,
313+
range: {
314+
start: { line: 0, character: 0 },
315+
end: { line: 0, character: 0 },
316+
},
317+
} as unknown as Diagnostic);
318+
continue;
319+
}
320+
try {
321+
AvroType.forSchema(avroSchema as any);
322+
} catch (e: any) {
323+
extraDiagnostics.push({
324+
code: 'avro-schema-invalid',
325+
message: e?.message || 'Invalid Avro schema.',
326+
path: ['components', 'messages', msgKey, 'payload', 'schema'],
327+
severity: DiagnosticSeverity.Error,
328+
range: {
329+
start: { line: 0, character: 0 },
330+
end: { line: 0, character: 0 },
331+
},
332+
} as unknown as Diagnostic);
333+
}
334+
}
335+
}
336+
} catch {
337+
//Outer catch block will handle the error
338+
}
339+
340+
const allDiagnostics = [...diagnostics, ...extraDiagnostics];
341+
const status = this.determineDiagnosticsStatus(allDiagnostics, options);
286342

287343
const result: ValidationResult = {
288344
status: status as 'valid' | 'invalid',
289-
diagnostics,
345+
diagnostics: allDiagnostics,
290346
score: await calculateScore(document),
291347
document: document?.json ? document.json() : undefined,
292348
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# yaml-language-server: $schema=https://asyncapi.com/schema-store/3.0.0-without-$id.json
2+
asyncapi: 3.0.0
3+
info:
4+
title: Coffee Bar Checkout Events
5+
version: 1.0.0
6+
description: AsyncAPI definition for order execution events in a coffee bar microservice.
7+
8+
channels:
9+
orderExecuted:
10+
address: coffee-bar.checkout.order-executed
11+
messages:
12+
orderExecuted:
13+
$ref: '#/components/messages/orderExecuted'
14+
15+
operations:
16+
sendAdCreated:
17+
action: receive
18+
channel:
19+
$ref: '#/channels/orderExecuted'
20+
21+
components:
22+
messages:
23+
orderExecuted:
24+
payload:
25+
schemaFormat: 'application/vnd.apache.avro+yaml;version=1.9.0'
26+
schema:
27+
type: record
28+
namespace: io.examples.checkout
29+
name: OrderExecuted
30+
fields:
31+
- name: orderId
32+
type: notexisting
33+
34+

test/integration/validate.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ describe('validate', () => {
8484
expect(ctx.stderr).to.equal('');
8585
done();
8686
});
87+
88+
test
89+
.stderr()
90+
.stdout()
91+
.command(['validate', './test/fixtures/asyncapi_avro_invalid.yml', '--log-diagnostics'])
92+
.it('fails and reports diagnostics when Avro schema is invalid', (ctx, done) => {
93+
expect(process.exitCode).to.equal(1);
94+
expect(ctx.stdout).to.contain('Errors');
95+
done();
96+
});
8797
});
8898

8999
describe('with context names', () => {

0 commit comments

Comments
 (0)