Skip to content

Commit

Permalink
[FEATURE] Add Configuration Schema (#274)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
matz3 authored Mar 24, 2020
1 parent 6b717fd commit eb961c3
Show file tree
Hide file tree
Showing 33 changed files with 6,198 additions and 255 deletions.
9 changes: 9 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ module.exports = {
Openui5Resolver: require("./lib/ui5Framework/Openui5Resolver"),
Sapui5Resolver: require("./lib/ui5Framework/Sapui5Resolver")
},
/**
* @public
* @see module:@ui5/project.validation
* @namespace
*/
validation: {
validator: require("./lib/validation/validator"),
ValidationError: require("./lib/validation/ValidationError")
},
/**
* @private
* @see module:@ui5/project.translators
Expand Down
133 changes: 110 additions & 23 deletions lib/projectPreprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ const fs = require("graceful-fs");
const path = require("path");
const {promisify} = require("util");
const readFile = promisify(fs.readFile);
const parseYaml = require("js-yaml").safeLoadAll;
const jsyaml = require("js-yaml");
const typeRepository = require("@ui5/builder").types.typeRepository;
const {validate} = require("./validation/validator");

class ProjectPreprocessor {
constructor({tree}) {
Expand Down Expand Up @@ -80,7 +81,7 @@ class ProjectPreprocessor {
return this.applyExtension(extProject);
}));
}
this.applyShims(project);
await this.applyShims(project);
if (this.isConfigValid(project)) {
// Do not apply transparent projects.
// Their only purpose might be to have their dependencies processed
Expand Down Expand Up @@ -194,26 +195,19 @@ class ProjectPreprocessor {
async loadProjectConfiguration(project) {
if (project.specVersion) { // Project might already be configured
// Currently, specVersion is the indicator for configured projects
this.normalizeConfig(project);
return {};
}

let configs;
if (project._transparentProject) {
// Assume that project is already processed
return {};
}

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

if (err.code !== "ENOENT") { // Something else than "File or directory does not exist"
throw new Error(errorText);
}
log.verbose(errorText);
return {};
}

const configs = await this.readConfigFile(project);

if (!configs || !configs.length) {
return {};
}
Expand Down Expand Up @@ -384,11 +378,77 @@ class ProjectPreprocessor {
}
}

async readConfigFile(configPath) {
const configFile = await readFile(configPath);
return parseYaml(configFile, {
filename: path
});
async readConfigFile(project) {
// A projects configPath property takes precedence over the default "<projectPath>/ui5.yaml" path
const configPath = project.configPath || path.join(project.path, "ui5.yaml");
let configFile;
try {
configFile = await readFile(configPath, {encoding: "utf8"});
} catch (err) {
const errorText = "Failed to read configuration for project " +
`${project.id} at "${configPath}". Error: ${err.message}`;

// Something else than "File or directory does not exist" or root project
if (err.code !== "ENOENT" || project._level === 0) {
throw new Error(errorText);
} else {
log.verbose(errorText);
return null;
}
}

let configs;

try {
// Using loadAll with DEFAULT_SAFE_SCHEMA instead of safeLoadAll to pass "filename".
// safeLoadAll doesn't handle its parameters properly.
// See https://github.com/nodeca/js-yaml/issues/456 and https://github.com/nodeca/js-yaml/pull/381
configs = jsyaml.loadAll(configFile, undefined, {
filename: configPath,
schema: jsyaml.DEFAULT_SAFE_SCHEMA
});
} catch (err) {
if (err.name === "YAMLException") {
throw new Error("Failed to parse configuration for project " +
`${project.id} at "${configPath}"\nError: ${err.message}`);
} else {
throw err;
}
}

if (!configs || !configs.length) {
return configs;
}

const validationResults = await Promise.all(
configs.map(async (config, documentIndex) => {
// Catch validation errors to ensure proper order of rejections within Promise.all
try {
await validate({
config,
project: {
id: project.id
},
yaml: {
path: configPath,
source: configFile,
documentIndex
}
});
} catch (error) {
return error;
}
})
);

const validationErrors = validationResults.filter(($) => $);

if (validationErrors.length > 0) {
// For now just throw the error of the first invalid document
throw validationErrors[0];
}

return configs;
}

handleShim(extension) {
Expand Down Expand Up @@ -451,7 +511,7 @@ class ProjectPreprocessor {
}
}

applyShims(project) {
async applyShims(project) {
const configShim = this.configShims[project.id];
// Apply configuration shims
if (configShim) {
Expand Down Expand Up @@ -482,6 +542,8 @@ class ProjectPreprocessor {

Object.assign(project, configShim);
delete project.shimDependenciesResolved; // Remove shim processing metadata from project

await this.validateAndNormalizeExistingProject(project);
}

// Apply collections
Expand Down Expand Up @@ -539,6 +601,31 @@ class ProjectPreprocessor {
const middlewarePath = path.join(extension.path, extension.middleware.path);
middlewareRepository.addMiddleware(extension.metadata.name, middlewarePath);
}

async validateAndNormalizeExistingProject(project) {
// Validate project config, but exclude additional properties
const excludedProperties = [
"id",
"version",
"path",
"dependencies",
"_level"
];
const config = {};
for (const key in project) {
if (project.hasOwnProperty(key) && !excludedProperties.includes(key)) {
config[key] = project[key];
}
}
await validate({
config,
project: {
id: project.id
}
});

this.normalizeConfig(project);
}
}

/**
Expand Down
Loading

0 comments on commit eb961c3

Please sign in to comment.