project/lib/validation/validator.js

import {fileURLToPath} from "node:url";
import {readFile} from "node:fs/promises";

/**
 * @module @ui5/project/validation/validator
 * @description A collection of validation related APIs
 * @public
 */

/**
 * @enum {string}
 * @private
 * @readonly
 */
export const SCHEMA_VARIANTS = {
	"ui5": "ui5.json",
	"ui5-workspace": "ui5-workspace.json"
};

class Validator {
	constructor({Ajv, ajvErrors, schemaName}) {
		if (!schemaName || !SCHEMA_VARIANTS[schemaName]) {
			throw new Error(
				`"schemaName" is missing or incorrect. The available schemaName variants are ${Object.keys(
					SCHEMA_VARIANTS
				).join(", ")}`
			);
		}

		this._schemaName = SCHEMA_VARIANTS[schemaName];

		this.ajv = new Ajv({
			allErrors: true,
			jsonPointers: true,
			loadSchema: Validator.loadSchema
		});
		ajvErrors(this.ajv);
	}

	_compileSchema() {
		const schemaName = this._schemaName;

		if (!this._compiling) {
			this._compiling = Promise.resolve().then(async () => {
				const schema = await Validator.loadSchema(schemaName);
				const validate = await this.ajv.compileAsync(schema);
				return validate;
			});
		}
		return this._compiling;
	}

	async validate({config, project, yaml}) {
		const fnValidate = await this._compileSchema();
		const valid = fnValidate(config);
		if (!valid) {
			// Read errors/schema from fnValidate before lazy loading ValidationError module.
			// Otherwise they might be cleared already.
			const {errors, schema} = fnValidate;
			const {default: ValidationError} = await import("./ValidationError.js");
			throw new ValidationError({
				errors,
				schema,
				project,
				yaml
			});
		}
	}

	static async loadSchema(schemaPath) {
		const filePath = schemaPath.replace("http://ui5.sap/schema/", "");
		const schemaFile = await readFile(
			fileURLToPath(new URL(`./schema/${filePath}`, import.meta.url)), {encoding: "utf8"}
		);
		return JSON.parse(schemaFile);
	}
}

const validator = Object.create(null);

async function _validate(schemaName, options) {
	if (!validator[schemaName]) {
		validator[schemaName] = (async () => {
			const {default: Ajv} = await import("ajv");
			const {default: ajvErrors} = await import("ajv-errors");
			return new Validator({Ajv, ajvErrors, schemaName});
		})();
	}

	const schemaValidator = await validator[schemaName];
	await schemaValidator.validate(options);
}

/**
 * Validates the given ui5 configuration.
 *
 * @public
 * @function
 * @static
 * @param {object} options
 * @param {object} options.config UI5 Configuration to validate
 * @param {object} options.project Project information
 * @param {string} options.project.id ID of the project
 * @param {object} [options.yaml] YAML information
 * @param {string} options.yaml.path Path of the YAML file
 * @param {string} options.yaml.source Content of the YAML file
 * @param {number} [options.yaml.documentIndex=0] Document index in case the YAML file contains multiple documents
 * @throws {@ui5/project/validation/ValidationError}
 *   Rejects with a {@link @ui5/project/validation/ValidationError ValidationError}
 *   when the validation fails.
 * @returns {Promise<undefined>} Returns a Promise that resolves when the validation succeeds
 */
export async function validate(options) {
	await _validate("ui5", options);
}

/**
 * Validates the given ui5-workspace configuration.
 *
 * @public
 * @function
 * @static
 * @param {object} options
 * @param {object} options.config ui5-workspace Configuration to validate
 * @param {object} [options.yaml] YAML information
 * @param {string} options.yaml.path Path of the YAML file
 * @param {string} options.yaml.source Content of the YAML file
 * @param {number} [options.yaml.documentIndex=0] Document index in case the YAML file contains multiple documents
 * @throws {@ui5/project/validation/ValidationError}
 *   Rejects with a {@link @ui5/project/validation/ValidationError ValidationError}
 *   when the validation fails.
 * @returns {Promise<undefined>} Returns a Promise that resolves when the validation succeeds
 */
export async function validateWorkspace(options) {
	await _validate("ui5-workspace", options);
}

export {
	/**
	 * For testing only!
	 *
	 * @private
	 */
	Validator as _Validator
};