diff --git a/packages/builder/.babelrc b/packages/builder/.babelrc new file mode 100644 index 0000000000..c52b9d492a --- /dev/null +++ b/packages/builder/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": ["@babel/preset-env"], + "sourceMaps": "inline", + "retainLines": true, + "plugins": [ + ["@babel/plugin-transform-runtime", + { + "regenerator": true + } + ] + ] +} \ No newline at end of file diff --git a/packages/builder/package.json b/packages/builder/package.json index e232dec697..4f7f83fa9e 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -6,6 +6,30 @@ "build": "rollup -c", "start": "rollup -c -w" }, + "jest": { + "globals": { + "GLOBALS": { + "client": "web" + } + }, + "testURL": "http://jest-breaks-if-this-does-not-exist", + "moduleNameMapper": { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/internals/mocks/fileMock.js", + "\\.(css|less|sass|scss)$": "identity-obj-proxy" + }, + "moduleFileExtensions": [ + "js" + ], + "moduleDirectories": [ + "node_modules" + ], + "transform": { + "^.+\\.js$": "babel-jest" + }, + "transformIgnorePatterns": [ + "/node_modules/(?!svelte).+\\.js$" + ] + }, "dependencies": { "@nx-js/compiler-util": "^2.0.0", "budibase-core": "file:../core/dist", @@ -20,8 +44,14 @@ "uikit": "^3.1.5" }, "devDependencies": { + "@babel/core": "^7.5.5", + "@babel/plugin-transform-runtime": "^7.5.5", + "@babel/preset-env": "^7.5.5", + "@babel/runtime": "^7.5.5", + "babel-jest": "^24.8.0", "browser-sync": "^2.26.7", "http-proxy-middleware": "^0.19.1", + "jest": "^24.8.0", "ncp": "^2.0.0", "npm-run-all": "^4.1.5", "rollup": "^1.12.0", diff --git a/packages/builder/src/userInterface/propsDefinitionParsing/createDefaultProps.js b/packages/builder/src/userInterface/propsDefinitionParsing/createDefaultProps.js new file mode 100644 index 0000000000..1db0f33c2a --- /dev/null +++ b/packages/builder/src/userInterface/propsDefinitionParsing/createDefaultProps.js @@ -0,0 +1,68 @@ +import { + isString, + isBoolean, + isNumber, + isArray, + isUndefined +} from "lodash/fp"; + +import { assign } from "lodash"; + +export const createDefaultProps = (propsDefinition, derivedFromProps) => { + + const props = {}; + const errors = []; + + for(let propDef in propsDefinition) { + const parsedPropDef = parsePropDef(propsDefinition[propDef]); + if(parsedPropDef.error) + errors.push({propName:propDef, error:parsedPropDef.error}); + else + props[propDef] = parsedPropDef; + } + + if(derivedFromProps) { + assign(props, ...derivedFromProps); + } + + return ({ + props, errors + }); +} + +const parsePropDef = propDef => { + const error = message => ({error:message}); + + if(isString(propDef)) { + if(!types[propDef]) + return error(`Do not recognise type ${propDef}`); + + return types[propDef].default(); + } + + if(!propDef.type) + return error("Property Definition must declare a type"); + + const type = types[propDef.type]; + if(!type) + return error(`Do not recognise type ${propDef.type}`); + + if(isUndefined(propDef.default)) + return type.default(propDef); + + if(!type.isOfType(propDef.default)) + return error(`${propDef.default} is not of type ${type}`); + + return propDef.default; +} + +const propType = (defaultValue, isOfType) => ({ + isOfType, default:defaultValue +}); + +const types = { + string: propType(() => "", isString), + bool: propType(() => false, isBoolean), + number: propType(() => 0, isNumber), + array: propType(() => [], isArray) +} \ No newline at end of file diff --git a/packages/builder/tests/propsParsing.spec.js b/packages/builder/tests/propsParsing.spec.js new file mode 100644 index 0000000000..13dc77c8a8 --- /dev/null +++ b/packages/builder/tests/propsParsing.spec.js @@ -0,0 +1,162 @@ +import { createDefaultProps } from "../src/userInterface/propsDefinitionParsing/createDefaultProps"; +import { + keys, some +} from "lodash/fp"; + +describe("createDefaultProps", () => { + + it("should create a object with single string value, when default string field set", () => { + const propDef = { + fieldName: {type:"string", default:"something"} + }; + + const { props, errors } = createDefaultProps(propDef); + + expect(errors).toEqual([]); + expect(props.fieldName).toBeDefined(); + expect(props.fieldName).toBe("something"); + expect(keys(props).length).toBe(1); + }); + + it("should create a object with single blank string value, when no default", () => { + const propDef = { + fieldName: {type:"string"} + }; + + const { props, errors } = createDefaultProps(propDef); + + expect(errors).toEqual([]); + expect(props.fieldName).toBeDefined(); + expect(props.fieldName).toBe(""); + }); + + it("should create a object with single blank string value, when prop definition is 'string' ", () => { + const propDef = { + fieldName: "string" + }; + + const { props, errors } = createDefaultProps(propDef); + + expect(errors).toEqual([]); + expect(props.fieldName).toBeDefined(); + expect(props.fieldName).toBe(""); + }); + + it("should create a object with single fals value, when prop definition is 'bool' ", () => { + const propDef = { + fieldName: "bool" + }; + + const { props, errors } = createDefaultProps(propDef); + + expect(errors).toEqual([]); + expect(props.fieldName).toBeDefined(); + expect(props.fieldName).toBe(false); + }); + + it("should create a object with single 0 value, when prop definition is 'number' ", () => { + const propDef = { + fieldName: "number" + }; + + const { props, errors } = createDefaultProps(propDef); + + expect(errors).toEqual([]); + expect(props.fieldName).toBeDefined(); + expect(props.fieldName).toBe(0); + }); + + it("should create a object with single 0 value, when prop definition is 'array' ", () => { + const propDef = { + fieldName: "array" + }; + + const { props, errors } = createDefaultProps(propDef); + + expect(errors).toEqual([]); + expect(props.fieldName).toBeDefined(); + expect(props.fieldName).toEqual([]); + }); + + it("should create an object with multiple prop names", () => { + const propDef = { + fieldName: "string", + fieldLength: { type: "number", default: 500 } + }; + + const { props, errors } = createDefaultProps(propDef); + + expect(errors).toEqual([]); + expect(props.fieldName).toBeDefined(); + expect(props.fieldName).toBe(""); + expect(props.fieldLength).toBeDefined(); + expect(props.fieldLength).toBe(500); + expect(keys(props).length).toBe(2); + }) + + it("should return error when invalid type", () => { + const propDef = { + fieldName: "invalid type name", + fieldLength: { type: "invalid type name "} + }; + + const { errors } = createDefaultProps(propDef); + + expect(errors.length).toBe(2); + expect(some(e => e.propName === "fieldName")(errors)).toBeTruthy(); + expect(some(e => e.propName === "fieldLength")(errors)).toBeTruthy(); + }); + + it("should return error default value is not of declared type", () => { + const propDef = { + fieldName: {type:"string", default: 1} + }; + + const { errors } = createDefaultProps(propDef); + + expect(errors.length).toBe(1); + expect(some(e => e.propName === "fieldName")(errors)).toBeTruthy(); + }); + + it("should merge in derived props", () => { + const propDef = { + fieldName: "string", + fieldLength: { type: "number", default: 500} + }; + + const derivedFrom = { + fieldName: "surname" + }; + + const { props, errors } = createDefaultProps(propDef, [derivedFrom]); + + expect(errors.length).toBe(0); + expect(props.fieldName).toBe("surname"); + expect(props.fieldLength).toBe(500); + + }); + + it("should merge in derived props, last in list taking priority", () => { + const propDef = { + fieldName: "string", + fieldLength: { type: "number", default: 500} + }; + + const derivedFrom1 = { + fieldName: "surname", + fieldLength: 200 + }; + + const derivedFrom2 = { + fieldName: "forename" + }; + + const { props, errors } = createDefaultProps(propDef, [derivedFrom1, derivedFrom2]); + + expect(errors.length).toBe(0); + expect(props.fieldName).toBe("forename"); + expect(props.fieldLength).toBe(200); + + }); + +}) \ No newline at end of file