Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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
12 changes: 7 additions & 5 deletions apps/generator/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const path = require('path');
const baseConfig = require('../../jest.config.base');

module.exports = {
...baseConfig(__dirname, {
moduleNameMapper: {
'^@asyncapi/nunjucks-filters$': path.resolve(__dirname, '../nunjucks-filters'),
},
}),
clearMocks: true,
moduleNameMapper: {
'^nimma/legacy$': '<rootDir>../../node_modules/nimma/dist/legacy/cjs/index.js',
'^nimma/(.*)': '<rootDir>../../node_modules/nimma/dist/cjs/$1',
'^@asyncapi/nunjucks-filters$': path.resolve(__dirname, '../nunjucks-filters'),
},
};
14 changes: 14 additions & 0 deletions apps/keeper/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const baseConfig = require('../../jest.config.base');

module.exports = {
...baseConfig(__dirname),
moduleFileExtensions: [
'js',
'json',
'jsx'
],
transform: {
'^.+\\.jsx?$': 'babel-jest'
},
};

5 changes: 0 additions & 5 deletions apps/keeper/jest.setup.js

This file was deleted.

28 changes: 3 additions & 25 deletions apps/keeper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,19 @@
"license": "Apache-2.0",
"dependencies": {
"@asyncapi/parser": "^3.4.0",
"@hyperjump/json-schema": "^1.16.2"
"ajv": "^8.17.1"
},
"devDependencies": {
"@babel/cli": "^7.25.9",
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@ungap/structured-clone": "^1.3.0",
"babel-jest": "^27.3.1",
"jest": "^27.3.1",
"jest-esm-transformer": "^1.0.0"
},
"jest": {
"setupFiles": [
"<rootDir>/jest.setup.js"
],
"transformIgnorePatterns": [
"node_modules/(?!@hyperjump)"
],
"moduleFileExtensions": [
"js",
"json",
"jsx"
],
"transform": {
"^.+\\.jsx?$": "jest-esm-transformer"
},
"moduleNameMapper": {
"^nimma/legacy$": "<rootDir>/../../node_modules/nimma/dist/legacy/cjs/index.js",
"^nimma/(.*)": "<rootDir>/../../node_modules/nimma/dist/cjs/$1",
"^@hyperjump/browser/jref$": "<rootDir>/../../node_modules/@hyperjump/browser/lib/jref/index.js",
"^@hyperjump/json-schema/experimental$": "<rootDir>/node_modules/@hyperjump/json-schema/lib/experimental.js"
}
},
"babel": {
"presets": [
"@babel/preset-env"
]
}
}
}
119 changes: 50 additions & 69 deletions apps/keeper/src/index.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,31 @@
import {
getAllRegisteredSchemaUris,
registerSchema,
unregisterSchema,
validate,
setMetaSchemaOutputFormat
} from '@hyperjump/json-schema/draft-07';
import { generateSchemaURI, parseAsyncAPIDocumentFromFile } from './utils';
import { DETAILED } from '@hyperjump/json-schema/experimental';
import { parseAsyncAPIDocumentFromFile } from './utils.js';
import Ajv from 'ajv';

setMetaSchemaOutputFormat(DETAILED);
const ajv = new Ajv({ strict: false, allErrors: true });

/**
* Compiles a single JSON Schema for validation.
* @param {object} schema - The JSON Schema object to compile.
* @returns {function} A compiled schema validator function.
* @throws {Error} Throws error if AJV cannot compile the schema.
*/
export function compileSchema(schema) {
return ajv.compile(schema);
}

/**
* Compiles multiple JSON Schemas for validation.
* @async
* @param {Array<object>} schemas - Array of JSON Schema objects to compile
* @returns {Promise<Array<function>>} Array of compiled schema validator functions
* @throws {Error} When schemas parameter is not an array
* @param {Array<object>} schemas - Array of JSON Schema objects to compile.
* @returns {Array<function>} Array of compiled schema validator functions.
* @throws {Error} Throws error if AJV cannot compile a schema.
*/
export async function compileSchemas(schemas) {
if (!schemas || schemas.length === 0) {
console.log('Skipping compilation: no schemas provided.');
return [];
}
export function compileSchemas(schemas) {
if (!Array.isArray(schemas)) {
throw new Error(`Invalid "schemas" parameter: expected an array, received ${typeof schemas}`);
throw new Error('Invalid "schemas" parameter: must be an array.');

Check warning on line 24 in apps/keeper/src/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`new Error()` is too unspecific for a type check. Use `new TypeError()` instead.

See more on https://sonarcloud.io/project/issues?id=asyncapi_generator&issues=AZpDbO7pf-7x-vKk_SZK&open=AZpDbO7pf-7x-vKk_SZK&pullRequest=1747
}
const dialectId = 'http://json-schema.org/draft-07/schema#';
const schemaUris = [];

// Register all schemas first
for (const schema of schemas) {
const schemaURI = generateSchemaURI();
registerSchema(schema, schemaURI, dialectId);
schemaUris.push(schemaURI);
}

const compiledSchemas = [];
for (const schemaUri of schemaUris) {
const compileSchema = await validate(schemaUri);
compiledSchemas.push(compileSchema);
for (const schema of schemas) {
compiledSchemas.push(compileSchema(schema));
}
return compiledSchemas;
}
Expand All @@ -48,57 +35,51 @@
* @async
* @param {string} asyncapiFilepath - Path to the AsyncAPI document file
* @param {string} operationId - ID of the operation to extract message schemas from
* @returns {Promise<Array<function>>} Array of compiled schema validator functions
* @throws {Error} When operationId is invalid or when the operation/messages can't be found
* @returns {Promise<Array<function>>} A Promise that resolves to an array of compiled schema validator functions.
* @throws {Error} When operationId is invalid or when the operation/messages can't be found.
*/
export async function compileSchemasByOperationId(asyncapiFilepath, operationId) {
if (typeof operationId !== 'string' || !operationId.trim()) {
throw new Error(`Invalid "operationId" parameter: must be a non-empty string, received ${operationId}`);
throw new Error(`Invalid "operationId" parameter: must be a non-empty string, received ${operationId}`);
}
const asyncapi = await parseAsyncAPIDocumentFromFile(asyncapiFilepath);
const operation = asyncapi.operations().get(operationId);
if (!operation) {
throw new Error(`Operation with ID "${operationId}" not found in the AsyncAPI document.`);
}
const messages = operation.messages().all();
if (!messages || messages.length === 0) {
console.warn(`No messages found for operation ID "${operationId}".`);
return [];
}
const schemas = messages
.filter(message => message.hasPayload())
.map(message => message.payload().json());
.map(message => message.payload().json());
return compileSchemas(schemas);
}

/**
* Validates a message payload against an array of operation message schemas.
* Uses the Hyperjump JSON Schema validator (Draft-07) to check if the message
* conforms to at least one of the provided schemas.
* @param {Array<function>} compiledSchemas - Array of compiled schema validator functions
* @param {object} message - The message payload to validate
* @returns {{ isValid: boolean, validationErrors?: Array<object> }} Object containing validation result and errors if invalid
* @throws {Error} When message parameter is null or undefined
* Validates a message payload against a compiled message schema.
*
* @param {function} compiledSchema - Compiled schema validator function.
* @param {object} message - The message payload to validate.
* @returns {{ isValid: boolean, validationErrors?: Array<object> }} Object containing validation result.
* If invalid, `validationErrors` will contain the validation errors.
* @throws {Error} When message parameter is null/undefined or compiledSchema is not a function.
*/
export function validateMessage(compiledSchemas, message) {
if (!compiledSchemas || compiledSchemas.length === 0) {
console.log('Skipping validation: no schemas provided for message validation.');
return { isValid: true };
}
if (message === null || message === undefined) {
throw new Error(`Invalid "message" parameter: expected a non-null object to validate, but received ${message}`);
export function validateMessage(compiledSchema, message) {
if (message === undefined) {
throw new Error(`Invalid "message" parameter: expected a value to validate, but received ${message}`);
}
const validationErrors = [];
for (const compiledSchema of compiledSchemas) {
const result = compiledSchema(message, DETAILED);
if (result.valid) {
return { isValid: true };
}
validationErrors.push(...result.errors);
if (typeof compiledSchema !== 'function') {
throw new Error(`Invalid "compiledSchema" parameter: expected a validator function, received ${typeof compiledSchema}`);

Check warning on line 75 in apps/keeper/src/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`new Error()` is too unspecific for a type check. Use `new TypeError()` instead.

See more on https://sonarcloud.io/project/issues?id=asyncapi_generator&issues=AZpDbO7pf-7x-vKk_SZL&open=AZpDbO7pf-7x-vKk_SZL&pullRequest=1747
}
return { isValid: false, validationErrors };
}

/**
* Unregisters all currently registered schemas from the validator.
* @returns {void}
*/
export function removeCompiledSchemas() {
const schemaUris = getAllRegisteredSchemaUris();
for (const schemaUri of schemaUris) {
unregisterSchema(schemaUri);
const validate = compiledSchema(message);
if (validate) {
return { isValid: true };
}
return {
isValid: false,
validationErrors: compiledSchema.errors || []
};
}
10 changes: 0 additions & 10 deletions apps/keeper/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,4 @@ export const parseAsyncAPIDocumentFromFile = async (asyncapiFilepath) => {
throw new Error(`Failed to parse AsyncAPI document: ${error.message}`);
}
return asyncapi;
};

/**
* Generates a unique schema URI using the current timestamp.
*
* @returns {string} A unique schema URI.
*/
export const generateSchemaURI = () => {
const timestamp = Date.now().toString();
return `http://asyncapi/keeper/schema/${timestamp}`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ components:
type: string
description: The content to be echoed back
example: "test message"
timestamp:
type: string
format: date-time
description: When the message was sent
messageId:
type: number
description: Unique identifier for the message
required:
- content
Loading