From 9649dc82e9a73dc492a75f56854093a5ec007514 Mon Sep 17 00:00:00 2001 From: Will Barnett Date: Fri, 3 Nov 2023 09:35:05 +0000 Subject: [PATCH] Support for activity file validation and display of multiple error messages. --- platform/src/ActivityManager.js | 94 ++++++++++++++++++++------ platform/src/EducationPlatformError.js | 13 ++++ platform/src/Playground.js | 16 +++-- platform/src/Utility.js | 30 ++++---- platform/test/spec/testUtilitySpec.js | 17 +++++ 5 files changed, 133 insertions(+), 37 deletions(-) create mode 100644 platform/src/EducationPlatformError.js diff --git a/platform/src/ActivityManager.js b/platform/src/ActivityManager.js index 1101cfa..d6c8d88 100644 --- a/platform/src/ActivityManager.js +++ b/platform/src/ActivityManager.js @@ -11,6 +11,7 @@ class ActivityManager { customActivitiesUrl = false; toolsUrl; customToolsUrl= false; + configErrors = []; activities = {}; activeSubMenu; @@ -42,7 +43,7 @@ class ActivityManager { } } - this.fetchActivities(); + this.configErrors = this.configErrors.concat(this.fetchActivities()); for(var activityKey of Object.keys(this.activities)) { this.resolveActionReferences( this.activities[activityKey].id ); @@ -90,42 +91,88 @@ class ActivityManager { /** * Fetches all the activities from activitiesUrl * and populates the activities array + * @returns errors from parsing and validation */ fetchActivities() { let file = this.fileHandler.fetchFile( this.activitiesUrl , urlParamPrivateRepo() ) let fileContent = file.content; + let errors = []; + if (fileContent != null){ - let config = parseConfigFile(fileContent); + let validatedConfig = this.parseAndValidateActivityConfig(fileContent); + + if ( validatedConfig.errors.length == 0 ){ + + this.createActivitiesMenu(validatedConfig.config); + + } else { + // Error config file parsing error + errors = errors.concat(validatedConfig.errors); + } + } + + return errors; + } + + /** + * Parses and validates an activity file + * @param {*} activityFile + * @returns object containing the validated config file object and an array of errors + */ + parseAndValidateActivityConfig(activityFile){ + let validationResult = {}; + + validationResult.errors = []; + + let config = parseConfigFile(activityFile); + + if ( !(config instanceof Error) ){ - for (const activity of config.activities) { + validationResult.config = config; + // TODO - validate activity configuration - if (activity.id) { - this.storeActivity(activity); - this.createActivityMenuEntry(null, activity); - } - else { - var active = false; - for (const nestedActivity of activity.activities) { - this.storeActivity(nestedActivity); - if (nestedActivity.id == this.activityId) { - active = true; - } - } + } else { + validationResult.errors.push(config); + validationResult.config = null; + } + + return validationResult; + } - var subMenu = this.createActivitiesSubMenu(activity.title, active); + /** + * Create the activities menu + * @param {*} config valid activities configuration object + */ + createActivitiesMenu(config){ - for (const nestedActivity of activity.activities) { - this.createActivityMenuEntry(subMenu, nestedActivity); + for (const activity of config.activities) { + + if (activity.id) { + this.storeActivity(activity); + this.createActivityMenuEntry(null, activity); + } + else { + var active = false; + for (const nestedActivity of activity.activities) { + this.storeActivity(nestedActivity); + if (nestedActivity.id == this.activityId) { + active = true; } } - } - } + var subMenu = this.createActivitiesSubMenu(activity.title, active); + + for (const nestedActivity of activity.activities) { + this.createActivityMenuEntry(subMenu, nestedActivity); + } + } + } } + subMenuNumber = 0; createActivitiesSubMenu(title, active = false) { @@ -406,6 +453,13 @@ class ActivityManager { } } + /** + * Returns the errors found parsing and validating the activty configuration file + * @returns array of errors + */ + getConfigErrors(){ + return this.configErrors; + } } diff --git a/platform/src/EducationPlatformError.js b/platform/src/EducationPlatformError.js new file mode 100644 index 0000000..4125209 --- /dev/null +++ b/platform/src/EducationPlatformError.js @@ -0,0 +1,13 @@ + +class EducationPlatformError extends Error { + + constructor(message){ + + super(message); + + this.name= "EducationPlatformError"; + } + +} + +export {EducationPlatformError} \ No newline at end of file diff --git a/platform/src/Playground.js b/platform/src/Playground.js index 2df59d2..7806e70 100644 --- a/platform/src/Playground.js +++ b/platform/src/Playground.js @@ -12,6 +12,7 @@ import 'metro4'; import { FileHandler } from './FileHandler.js'; import { ActivityManager } from './ActivityManager.js'; import { ToolManager as ToolsManager } from './ToolsManager.js'; +import { EducationPlatformError } from './EducationPlatformError.js' import { ConsolePanel } from "./ConsolePanel.js"; import { ProgramPanel } from "./ProgramPanel.js"; @@ -140,8 +141,16 @@ function initialiseActivity(){ initialisePanels(); } else { - // No activity configuration has been given + displayErrors([new EducationPlatformError("No activity configuration has been specified.")]); + } +} + +function displayErrors(errors){ + + let errorText = ""; + errors.forEach((err) => errorText += err.message + " \n\n"); + const contentPanelName = "content-panel"; panels.push(new BlankPanel(contentPanelName)); @@ -155,9 +164,8 @@ function initialiseActivity(){ fit(); var contentPanelDiv = document.getElementById(contentPanelName); - var content = document.createTextNode("No activity configuration has been specified."); + var content = document.createTextNode(errorText); contentPanelDiv.append(content); - } } function initialisePanels() { @@ -200,8 +208,6 @@ function initialisePanels() { } - - /** * Create a panel for a given panel config entry * diff --git a/platform/src/Utility.js b/platform/src/Utility.js index 561e859..af4f3bf 100644 --- a/platform/src/Utility.js +++ b/platform/src/Utility.js @@ -1,5 +1,5 @@ -import { parse as yamlParse} from 'yaml' +import { parse as yamlParse, YAMLParseError} from 'yaml' export const ARRAY_ANY_ELEMENT = '*'; @@ -179,25 +179,31 @@ export function isAuthenticated(){ * Parses the platform configuration files, YAML and JSON types are supported. * @param {String} contents the configuration file contents * @param {String} extension the configuration file extenstion - * @returns the parsed configuration object + * @returns the parsed configuration object or an error */ export function parseConfigFile(contents, extension="yml"){ let configObject; - switch(extension){ - case "json": - configObject= JSON.parse(contents); - break; + try { + switch(extension){ + case "json": + configObject= JSON.parse(contents); + break; - case "yml": - configObject= yamlParse(contents); - break; + case "yml": + configObject= yamlParse(contents); + break; - default: - console.log("Cannont parse unsupported configuration file type '" + extension + "'."); - configObject = null; + default: + console.log("Cannont parse unsupported configuration file type '" + extension + "'."); + configObject = null; + } + } catch(e){ + if (e instanceof YAMLParseError || e instanceof SyntaxError){ + configObject = e; + } } return configObject; diff --git a/platform/test/spec/testUtilitySpec.js b/platform/test/spec/testUtilitySpec.js index e6a4cb2..3f19e50 100644 --- a/platform/test/spec/testUtilitySpec.js +++ b/platform/test/spec/testUtilitySpec.js @@ -102,6 +102,19 @@ describe("Utility", () => { expect(result).toEqual(expectedObject); }) + it("parseConfigFile - an invald yaml file returns an error", ()=> { + + let result = parseConfigFile(INVALID_FILE, "yml"); + + expect(result).toBeInstanceOf(Error); + }) + + it("parseConfigFile - an invald json file returns an error", ()=> { + + let result = parseConfigFile(INVALID_FILE, "json"); + + expect(result).toBeInstanceOf(Error); + }) }) const JSON_ACTIVITY_CONFIG= "{\n" @@ -542,3 +555,7 @@ const YML_ACTIVITY_CONFIG= "activities:\n" + " tools:\n" + " - http://127.0.0.1:8070/epsilon_tool.json\n" + ""; + +const INVALID_FILE = "{ \n" ++ "----------------- \n" ++ "]";