UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

206 lines (188 loc) 7.65 kB
'use strict'; const { forEach } = require('../utils/objectUtils'); /* eslint-disable arrow-body-style */ const booleanValidator = { validate: val => val === true || val === false, expected: () => 'type boolean', found: val => `type ${ typeof val }`, }; /** * Validation function. Returns false if invalid. * * @typedef {(input: any) => boolean} ValidateFunction */ /** * @typedef {object} Validator * @property {ValidateFunction} validate Run the validation check * @property {Function} expected Returns the expected type/value as a string. * @property {Function} found Returns the actually found type/value as a string. */ /** * Generate a Validator that validates that the * input is a string and one of the available options. * The validation of the option values is case-insensitive. * * @param {any} availableValues Available values * @returns {Validator} Return a validator for a string in an expected range */ function generateStringValidator( availableValues ) { return { validate: val => typeof val === 'string' && availableValues.some( av => av.toLowerCase() === val.toLowerCase() ), expected: (val) => { return typeof val !== 'string' ? 'type string' : availableValues.join(', '); }, found: (val) => { return typeof val !== 'string' ? `type ${ typeof val }` : `value ${ val }`; }, }; } const validators = { beta: { validate: val => val !== null && typeof val === 'object' && !Array.isArray(val), expected: () => 'type object', found: (val) => { return val === null ? val : `type ${ typeof val }`; }, }, deprecated: { validate: val => val !== null && typeof val === 'object' && !Array.isArray(val), expected: () => 'type object', found: (val) => { return val === null ? val : `type ${ typeof val }`; }, }, severities: { validate: (val) => { if (val !== null && typeof val === 'object' && !Array.isArray(val)) return true; return false; }, expected: () => 'type object', found: (val) => { return val === null ? val : `type ${ typeof val }`; }, }, // TODO: Maybe do a deep validation of the whole object with leafs? variableReplacements: { validate: val => val !== null && typeof val === 'object' && !Array.isArray(val), expected: () => 'type object', found: (val) => { return val === null ? val : `type ${ typeof val }`; }, }, messages: { validate: val => Array.isArray(val), expected: () => 'type array', found: val => `type ${ typeof val }`, }, sqlDialect: generateStringValidator([ 'sqlite', 'hana', 'plain', 'postgres', 'h2' ]), sqlMapping: generateStringValidator([ 'plain', 'quoted', 'hdbcds' ]), odataVersion: generateStringValidator([ 'v2', 'v4' ]), odataFormat: generateStringValidator([ 'flat', 'structured' ]), odataVocabularies: { validate: val => (typeof val === 'object' && !Array.isArray(val)), expected: () => 'type JSON object', found: val => `type ${ Array.isArray(val) ? 'JSON array' : typeof val }`, }, service: { validate: val => typeof val === 'string', expected: () => 'type string', found: val => `type ${ typeof val }`, }, serviceNames: { validate: val => Array.isArray(val) && !val.some(y => (typeof y !== 'string')), expected: () => 'type array of string', found: val => `type ${ typeof val }`, }, effectiveServiceName: { validate: val => typeof val === 'string', expected: () => 'type string', found: val => `type ${ typeof val }`, }, defaultBinaryLength: { validate: val => !Number.isNaN(Number(val)) && Number.isInteger(Number.parseFloat(val)), expected: () => 'Integer literal', found: val => `${ (!Number.isNaN(Number(val)) ? val : 'Not a Number') }`, }, defaultStringLength: { validate: val => !Number.isNaN(Number(val)) && Number.isInteger(Number.parseFloat(val)), expected: () => 'Integer literal', found: val => `${ (!Number.isNaN(Number(val)) ? val : 'Not a Number') }`, }, csnFlavor: { validate: val => typeof val === 'string', expected: () => 'type string', found: val => `type ${ typeof val }`, }, testMode: { validate: val => typeof val === 'boolean' || typeof val === 'number' || val === '$noAssertConsistency', expected: () => 'type boolean|number', found: val => `type ${ typeof val }`, }, withLocations: { validate: val => typeof val === 'boolean' || val === 'withEndPosition', expected: () => 'type boolean|"withEndPosition"', found: val => `type ${ typeof val }`, }, dictionaryPrototype: { validate: () => true, }, assertIntegrity: { validate: val => typeof val === 'string' && val === 'individual' || typeof val === 'boolean', expected: () => 'a boolean or a string with value \'individual\'', found: val => (typeof val === 'string' ? val : `type ${ typeof val }`), }, assertIntegrityType: generateStringValidator([ 'DB', 'RT' ]), tenantDiscriminator: { validate: () => true }, // do it ourselves }; // Note: if `validate()` returns true, it means the option is _invalid_! const allCombinationValidators = { 'valid-structured': (options, message) => { if (options.odataVersion === 'v2' && options.odataFormat === 'structured') message.error('api-invalid-combination', null, { '#': 'valid-structured' }); }, 'sql-dialect-and-naming': (options, message) => { if (options.sqlDialect && options.sqlMapping && options.sqlDialect !== 'hana' && [ 'quoted', 'hdbcds' ].includes(options.sqlMapping)) message.error('api-invalid-combination', null, { '#': 'sql-dialect-and-naming', name: options.sqlDialect, prop: options.sqlMapping }); }, 'beta-no-test': (options, message) => { if (options.beta && !options.testMode) message.warning('api-unexpected-combination', null, { '#': 'beta-no-test', option: 'beta' }); }, 'effectiveServiceName-and-type-resolution': (options, message) => { if (options.effectiveServiceName && !options.resolveSimpleTypes) message.error('api-invalid-combination', null, { '#': 'effectiveServiceName-and-type-resolution', name: 'effectiveServiceName', prop: 'resolveSimpleTypes' }); }, }; const alwaysRunValidators = [ 'beta-no-test' ]; /** * Run the validations for each option. * Use a custom validator or "default" custom validator, fallback to Boolean validator. * * @param {object} options Flat options object to validate * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @param {object} [customValidators] Map of custom validators to use * @param {string[]} [combinationValidators] Validate option combinations * @returns {void} * @throws {CompilationError} Throws in case of invalid option usage */ function validate( options, messageFunctions, customValidators = {}, combinationValidators = [] ) { const { error, throwWithError } = messageFunctions; forEach(options, (optionName, optionValue) => { const validator = customValidators[optionName] || validators[optionName] || booleanValidator; if (!validator.validate(optionValue)) { error('api-invalid-option', null, { '#': 'value', prop: optionName, value: validator.expected(optionValue), othervalue: validator.found(optionValue), }); } }); throwWithError(); for (const combinationValidatorName of combinationValidators.concat(alwaysRunValidators)) allCombinationValidators[combinationValidatorName](options, messageFunctions); throwWithError(); } module.exports = { validate, generateStringValidator }; /* eslint-enable arrow-body-style */