Skip to content

Commit eb961c3

Browse files
authored
[FEATURE] Add Configuration Schema (#274)
Adds a JSON Schema to validate a UI5 configuration and report errors. For projects with "specVersion" lower than 2.0 all properties are allowed and no validation is done apart from the specVersion itself.
1 parent 6b717fd commit eb961c3

33 files changed

+6198
-255
lines changed

index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ module.exports = {
1414
Openui5Resolver: require("./lib/ui5Framework/Openui5Resolver"),
1515
Sapui5Resolver: require("./lib/ui5Framework/Sapui5Resolver")
1616
},
17+
/**
18+
* @public
19+
* @see module:@ui5/project.validation
20+
* @namespace
21+
*/
22+
validation: {
23+
validator: require("./lib/validation/validator"),
24+
ValidationError: require("./lib/validation/ValidationError")
25+
},
1726
/**
1827
* @private
1928
* @see module:@ui5/project.translators

lib/projectPreprocessor.js

Lines changed: 110 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ const fs = require("graceful-fs");
33
const path = require("path");
44
const {promisify} = require("util");
55
const readFile = promisify(fs.readFile);
6-
const parseYaml = require("js-yaml").safeLoadAll;
6+
const jsyaml = require("js-yaml");
77
const typeRepository = require("@ui5/builder").types.typeRepository;
8+
const {validate} = require("./validation/validator");
89

910
class ProjectPreprocessor {
1011
constructor({tree}) {
@@ -80,7 +81,7 @@ class ProjectPreprocessor {
8081
return this.applyExtension(extProject);
8182
}));
8283
}
83-
this.applyShims(project);
84+
await this.applyShims(project);
8485
if (this.isConfigValid(project)) {
8586
// Do not apply transparent projects.
8687
// Their only purpose might be to have their dependencies processed
@@ -194,26 +195,19 @@ class ProjectPreprocessor {
194195
async loadProjectConfiguration(project) {
195196
if (project.specVersion) { // Project might already be configured
196197
// Currently, specVersion is the indicator for configured projects
197-
this.normalizeConfig(project);
198-
return {};
199-
}
200198

201-
let configs;
199+
if (project._transparentProject) {
200+
// Assume that project is already processed
201+
return {};
202+
}
202203

203-
// A projects configPath property takes precedence over the default "<projectPath>/ui5.yaml" path
204-
const configPath = project.configPath || path.join(project.path, "/ui5.yaml");
205-
try {
206-
configs = await this.readConfigFile(configPath);
207-
} catch (err) {
208-
const errorText = "Failed to read configuration for project " +
209-
`${project.id} at "${configPath}". Error: ${err.message}`;
204+
await this.validateAndNormalizeExistingProject(project);
210205

211-
if (err.code !== "ENOENT") { // Something else than "File or directory does not exist"
212-
throw new Error(errorText);
213-
}
214-
log.verbose(errorText);
206+
return {};
215207
}
216208

209+
const configs = await this.readConfigFile(project);
210+
217211
if (!configs || !configs.length) {
218212
return {};
219213
}
@@ -384,11 +378,77 @@ class ProjectPreprocessor {
384378
}
385379
}
386380

387-
async readConfigFile(configPath) {
388-
const configFile = await readFile(configPath);
389-
return parseYaml(configFile, {
390-
filename: path
391-
});
381+
async readConfigFile(project) {
382+
// A projects configPath property takes precedence over the default "<projectPath>/ui5.yaml" path
383+
const configPath = project.configPath || path.join(project.path, "ui5.yaml");
384+
let configFile;
385+
try {
386+
configFile = await readFile(configPath, {encoding: "utf8"});
387+
} catch (err) {
388+
const errorText = "Failed to read configuration for project " +
389+
`${project.id} at "${configPath}". Error: ${err.message}`;
390+
391+
// Something else than "File or directory does not exist" or root project
392+
if (err.code !== "ENOENT" || project._level === 0) {
393+
throw new Error(errorText);
394+
} else {
395+
log.verbose(errorText);
396+
return null;
397+
}
398+
}
399+
400+
let configs;
401+
402+
try {
403+
// Using loadAll with DEFAULT_SAFE_SCHEMA instead of safeLoadAll to pass "filename".
404+
// safeLoadAll doesn't handle its parameters properly.
405+
// See https://github.com/nodeca/js-yaml/issues/456 and https://github.com/nodeca/js-yaml/pull/381
406+
configs = jsyaml.loadAll(configFile, undefined, {
407+
filename: configPath,
408+
schema: jsyaml.DEFAULT_SAFE_SCHEMA
409+
});
410+
} catch (err) {
411+
if (err.name === "YAMLException") {
412+
throw new Error("Failed to parse configuration for project " +
413+
`${project.id} at "${configPath}"\nError: ${err.message}`);
414+
} else {
415+
throw err;
416+
}
417+
}
418+
419+
if (!configs || !configs.length) {
420+
return configs;
421+
}
422+
423+
const validationResults = await Promise.all(
424+
configs.map(async (config, documentIndex) => {
425+
// Catch validation errors to ensure proper order of rejections within Promise.all
426+
try {
427+
await validate({
428+
config,
429+
project: {
430+
id: project.id
431+
},
432+
yaml: {
433+
path: configPath,
434+
source: configFile,
435+
documentIndex
436+
}
437+
});
438+
} catch (error) {
439+
return error;
440+
}
441+
})
442+
);
443+
444+
const validationErrors = validationResults.filter(($) => $);
445+
446+
if (validationErrors.length > 0) {
447+
// For now just throw the error of the first invalid document
448+
throw validationErrors[0];
449+
}
450+
451+
return configs;
392452
}
393453

394454
handleShim(extension) {
@@ -451,7 +511,7 @@ class ProjectPreprocessor {
451511
}
452512
}
453513

454-
applyShims(project) {
514+
async applyShims(project) {
455515
const configShim = this.configShims[project.id];
456516
// Apply configuration shims
457517
if (configShim) {
@@ -482,6 +542,8 @@ class ProjectPreprocessor {
482542

483543
Object.assign(project, configShim);
484544
delete project.shimDependenciesResolved; // Remove shim processing metadata from project
545+
546+
await this.validateAndNormalizeExistingProject(project);
485547
}
486548

487549
// Apply collections
@@ -539,6 +601,31 @@ class ProjectPreprocessor {
539601
const middlewarePath = path.join(extension.path, extension.middleware.path);
540602
middlewareRepository.addMiddleware(extension.metadata.name, middlewarePath);
541603
}
604+
605+
async validateAndNormalizeExistingProject(project) {
606+
// Validate project config, but exclude additional properties
607+
const excludedProperties = [
608+
"id",
609+
"version",
610+
"path",
611+
"dependencies",
612+
"_level"
613+
];
614+
const config = {};
615+
for (const key in project) {
616+
if (project.hasOwnProperty(key) && !excludedProperties.includes(key)) {
617+
config[key] = project[key];
618+
}
619+
}
620+
await validate({
621+
config,
622+
project: {
623+
id: project.id
624+
}
625+
});
626+
627+
this.normalizeConfig(project);
628+
}
542629
}
543630

544631
/**

0 commit comments

Comments
 (0)