From e699002c967157f7be1b4fb926c804c21bfdc5a9 Mon Sep 17 00:00:00 2001 From: Klemek Date: Mon, 15 Jul 2019 10:55:16 +0200 Subject: [PATCH] type enforcement --- README.md | 1 + src/placing.js | 25 +++++++++++++++++++++++ src/rendering.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++ src/utils.js | 34 +++++++++++++++++++++++++++++++ test/utils.test.js | 48 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+) diff --git a/README.md b/README.md index 21175a3..f71bc03 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,7 @@ You can define a relative icon with the following: | --- | --- | --- | --- | | **`icon`** | string | **yes** | name of the Font-Awesome icon of the sub-element (see [Icon names](#icon-names)) | | `color` | string | no | redefined the color | +| `scale` | number | no | redefine this icon scale | ## More info *[back to top](#top)* diff --git a/src/placing.js b/src/placing.js index cad2350..64b7bcc 100644 --- a/src/placing.js +++ b/src/placing.js @@ -15,6 +15,18 @@ const utils = require('./utils'); * @property {string|undefined} direction */ +const NODE_DEF = { + 'name': '!string', + 'x': 'number', + 'y': 'number' +}; + +const LINK_DEF = { + 'from': '!string', + 'to': '!string', + 'direction': 'string' +}; + const DEFAULT_OPTIONS = { 'max-link-length': 3, 'diagonals': true, @@ -307,6 +319,19 @@ module.exports = (options) => { * @returns {Object|null} */ compute: (nodes, links) => { + + Object.keys(nodes).forEach(key => { + const res = utils.isValid(nodes[key], NODE_DEF); + if (res) + throw `node '${key}' is invalid at key ${res}`; + }); + + links.forEach((link, i) => { + const res = utils.isValid(link, LINK_DEF); + if (res) + throw `link ${i} (${link.from}->${link.to}) is invalid at key ${res}`; + }); + Object.values(nodes).forEach(node => { node.const = { beforeX: [], diff --git a/src/rendering.js b/src/rendering.js index 005420c..4af4fb8 100644 --- a/src/rendering.js +++ b/src/rendering.js @@ -23,6 +23,40 @@ try { * @property {string|undefined} type */ +const SUB_DEF = { + 'text': 'string', + 'icon': 'string', + 'color': 'string', + 'font': 'string', + 'font-size': 'number', + 'font-style': 'string', + 'scale': 'number', +}; + +const NODE_DEF = { + 'name': '!string', + 'icon': '!string', + 'x': '!number', + 'y': '!number', + 'color': 'string', + 'scale': 'number', + 'top': SUB_DEF, + 'bottom': SUB_DEF, + 'left': SUB_DEF, + 'right': SUB_DEF +}; + +const LINK_DEF = { + 'from': '!string', + 'to': '!string', + 'type': 'string', + 'color': 'string', + 'scale': 'number', + 'size': 'number', + 'top': SUB_DEF, + 'bottom': SUB_DEF, +}; + const DEFAULT_OPTIONS = { 'beautify': false, 'scale': 128, @@ -230,8 +264,24 @@ module.exports = (options) => { }); }, + /** + * @param {Object} nodes + * @param {Link2[]} links + */ compute: (nodes, links) => { + Object.keys(nodes).forEach(key => { + const res = utils.isValid(nodes[key], NODE_DEF); + if (res) + throw `node '${key}' is invalid at key ${res}`; + }); + + links.forEach((link, i) => { + const res = utils.isValid(link, LINK_DEF); + if (res) + throw `link ${i} (${link.from}->${link.to}) is invalid at key ${res}`; + }); + const bounds = self.getBounds(nodes); const data = {'g': []}; diff --git a/src/utils.js b/src/utils.js index 57558f1..b2a2fd8 100644 --- a/src/utils.js +++ b/src/utils.js @@ -21,6 +21,40 @@ const self = { } }, + /** + * Verify if an object respect it's definition + * @param obj + * @param def + * @returns {null|string} + */ + isValid: (obj, def) => { + const keys = Object.keys(def); + let key; + let type; + for (let i = 0; i < keys.length; i++) { + key = keys[i]; + type = (typeof obj !== 'object' || obj[key] === undefined || obj[key] === null) ? null : typeof obj[key]; + if (type === 'object' && obj[key].length > 0) + type = 'array'; + if (typeof def[key] === 'object') { + if (type && type !== 'object') + return key; + const res = self.isValid(type ? obj[key] : undefined, def[key]); + if (res) + return key + '.' + res; + } else { + if (def[key][0] === '!') { + def[key] = def[key].substr(1); + if (!type) + return key; + } + if (type && type !== def[key]) + return key; + } + } + return null; + }, + /** * Clone any JS variable or object * @param {*} arg diff --git a/test/utils.test.js b/test/utils.test.js index cd85da4..f2b115c 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -36,6 +36,54 @@ describe('merge', () => { }); }); +describe('isValid', () => { + test('valid number', () => { + expect(utils.isValid({a: 0}, {a: 'number'})).toBe(null); + }); + test('invalid number', () => { + expect(utils.isValid({b: 'number'}, {b: 'number'})).toBe('b'); + }); + test('valid string', () => { + expect(utils.isValid({b: ''}, {b: 'string'})).toBe(null); + }); + test('invalid string', () => { + expect(utils.isValid({b: 0}, {b: 'string'})).toBe('b'); + }); + test('valid array', () => { + expect(utils.isValid({c: [1, 2, 3]}, {c: 'array'})).toBe(null); + }); + test('invalid array', () => { + expect(utils.isValid({c: {d: 5}}, {c: 'array'})).toBe('c'); + }); + test('undefined optional key', () => { + expect(utils.isValid({}, {a: 'number'})).toBe(null); + }); + test('undefined required key', () => { + expect(utils.isValid({}, {a: '!number'})).toBe('a'); + }); + test('defined required key', () => { + expect(utils.isValid({a: 5}, {a: '!number'})).toBe(null); + }); + test('invalid sub-object', () => { + expect(utils.isValid({a: 5}, {a: {b: 'number'}})).toBe('a'); + }); + test('undefined not required sub-object', () => { + expect(utils.isValid({}, {a: {b: 'number'}})).toBe(null); + }); + test('undefined required sub-object', () => { + expect(utils.isValid({}, {a: {b: '!number'}})).toBe('a.b'); + }); + test('invalid sub-object', () => { + expect(utils.isValid({a: {b: 'hello'}}, {a: {b: 'number'}})).toBe('a.b'); + }); + test('defined required sub-object', () => { + expect(utils.isValid({a: {b: 5}}, {a: {b: '!number'}})).toBe(null); + }); + test('ignored extra key', () => { + expect(utils.isValid({b: 5}, {a: 'number'})).toBe(null); + }); +}); + test('ezClone', () => { const a = { 'a': 5,