From 01c1a630b5178004c63ab0b0b3f6805c0695da87 Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Tue, 20 Dec 2022 14:43:52 +0100 Subject: [PATCH 1/3] feat: add `strict` mode In strict mode `moddle` will fail on unknown property access / instantiation. --- lib/moddle.js | 6 ++++- lib/properties.js | 34 ++++++++++++++++++++++-- test/helper.js | 4 +-- test/spec/moddle.js | 65 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 5 deletions(-) diff --git a/lib/moddle.js b/lib/moddle.js index 64bfd47..0b27fa6 100644 --- a/lib/moddle.js +++ b/lib/moddle.js @@ -35,8 +35,10 @@ import { * var moddle = new Moddle([pkg]); * * @param {Array} packages the packages to contain + * + * @param { { strict?: boolean } } [config] moddle configuration */ -export default function Moddle(packages) { +export default function Moddle(packages, config = {}) { this.properties = new Properties(this); @@ -44,6 +46,8 @@ export default function Moddle(packages) { this.registry = new Registry(packages, this.properties); this.typeCache = {}; + + this.config = config; } diff --git a/lib/properties.js b/lib/properties.js index 513a0b1..a42588b 100644 --- a/lib/properties.js +++ b/lib/properties.js @@ -27,7 +27,7 @@ Properties.prototype.set = function(target, name, value) { throw new TypeError('property name must be a non-empty string'); } - var property = this.model.getPropertyDescriptor(target, name); + var property = this.getProperty(target, name); var propertyName = property && property.name; @@ -66,7 +66,7 @@ Properties.prototype.set = function(target, name, value) { */ Properties.prototype.get = function(target, name) { - var property = this.model.getPropertyDescriptor(target, name); + var property = this.getProperty(target, name); if (!property) { return target.$attrs[name]; @@ -123,6 +123,36 @@ Properties.prototype.defineModel = function(target, model) { this.define(target, '$model', { value: model }); }; +/** + * Return property with the given name on the element. + * + * @param {any} target + * @param {string} name + * + * @return {object | null} property + */ +Properties.prototype.getProperty = function(target, name) { + + var model = this.model; + + var property = model.getPropertyDescriptor(target, name); + + if (property) { + return property; + } + + if (name.includes(':')) { + return null; + } + + const strict = model.config.strict === true; + + if (strict) { + throw new TypeError(`unknown property <${ name }> on <${ target.$type }>`); + } + + return null; +}; function isUndefined(val) { return typeof val === 'undefined'; diff --git a/test/helper.js b/test/helper.js index 7056f89..550a6d1 100644 --- a/test/helper.js +++ b/test/helper.js @@ -21,7 +21,7 @@ export function createModelBuilder(base) { throw new Error('[test-util] must specify a base directory'); } - function createModel(packageNames) { + function createModel(packageNames, config = {}) { var packages = map(packageNames, function(f) { var pkg = cache[f]; @@ -38,7 +38,7 @@ export function createModelBuilder(base) { return pkg; }); - return new Moddle(packages); + return new Moddle(packages, config); } return createModel; diff --git a/test/spec/moddle.js b/test/spec/moddle.js index 63e53cf..08e3d53 100644 --- a/test/spec/moddle.js +++ b/test/spec/moddle.js @@ -376,4 +376,69 @@ describe('moddle', function() { }); + + describe('property access (strict)', function() { + + const moddle = createModel([ + 'properties' + ], { + strict: true + }); + + + it('should configure in strict mode', function() { + + // then + expect(moddle.config.strict).to.be.true; + }); + + + describe('typed', function() { + + it('should access basic', function() { + + // when + const element = moddle.create('props:ComplexCount', { + count: 10 + }); + + // then + expect(element.get('count')).to.eql(10); + expect(element.get('props:count')).to.eql(10); + + // available under base name + expect(element.count).to.exist; + }); + + + it('fail accessing unknown property', function() { + + // when + const element = moddle.create('props:ComplexCount'); + + // then + expect(() => { + element.get('foo'); + }).to.throw(/unknown property on /); + + expect(() => { + element.set('foo', 10); + }).to.throw(/unknown property on /); + }); + + + it('fail instantiating with unknown property', function() { + + // then + expect(() => { + moddle.create('props:ComplexCount', { + foo: 10 + }); + }).to.throw(/unknown property on /); + }); + + }); + + }); + }); From d79e0c4388c2767027425b8b8d68d25d442b6b77 Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Sat, 17 Dec 2022 12:25:47 +0100 Subject: [PATCH 2/3] feat: log unknown property access Instead of throwing, log a warning when `strict=false` is configured. This ensures that logging is still opt-in. It won't happen without prior configuration: ```javascript const moddle = new Moddle(packages, { strict: false }); ``` --- lib/properties.js | 14 +++++++++++--- test/spec/moddle.js | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/lib/properties.js b/lib/properties.js index a42588b..bc56234 100644 --- a/lib/properties.js +++ b/lib/properties.js @@ -145,10 +145,18 @@ Properties.prototype.getProperty = function(target, name) { return null; } - const strict = model.config.strict === true; + const strict = model.config.strict; - if (strict) { - throw new TypeError(`unknown property <${ name }> on <${ target.$type }>`); + if (typeof strict !== 'undefined') { + const error = new TypeError(`unknown property <${ name }> on <${ target.$type }>`); + + if (strict) { + throw error; + } else { + + // eslint-disable-next-line no-undef + typeof console !== 'undefined' && console.warn(error); + } } return null; diff --git a/test/spec/moddle.js b/test/spec/moddle.js index 08e3d53..4460ecd 100644 --- a/test/spec/moddle.js +++ b/test/spec/moddle.js @@ -377,6 +377,33 @@ describe('moddle', function() { }); + describe('property access (lax)', function() { + + const moddle = createModel([ + 'properties' + ], { + strict: false + }); + + + describe('typed', function() { + + it('should access unknown attribute', function() { + + // when + const element = moddle.create('props:ComplexCount', { + foo: 'bar' + }); + + // then + expect(element.get('foo')).to.eql('bar'); + }); + + }); + + }); + + describe('property access (strict)', function() { const moddle = createModel([ From d13fbab6aaf7a11576a2921177f5fdf5261053a3 Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Sat, 17 Dec 2022 17:35:27 +0100 Subject: [PATCH 3/3] feat: allow to explicitly declare attributes as global --- lib/properties.js | 10 ++++++--- test/spec/moddle.js | 51 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/lib/properties.js b/lib/properties.js index bc56234..f57e8c4 100644 --- a/lib/properties.js +++ b/lib/properties.js @@ -38,7 +38,7 @@ Properties.prototype.set = function(target, name, value) { if (property) { delete target[propertyName]; } else { - delete target.$attrs[name]; + delete target.$attrs[stripGlobal(name)]; } } else { @@ -51,7 +51,7 @@ Properties.prototype.set = function(target, name, value) { defineProperty(target, property, value); } } else { - target.$attrs[name] = value; + target.$attrs[stripGlobal(name)] = value; } } }; @@ -69,7 +69,7 @@ Properties.prototype.get = function(target, name) { var property = this.getProperty(target, name); if (!property) { - return target.$attrs[name]; + return target.$attrs[stripGlobal(name)]; } var propertyName = property.name; @@ -173,4 +173,8 @@ function defineProperty(target, property, value) { value: value, configurable: true }); +} + +function stripGlobal(name) { + return name.replace(/^:/, ''); } \ No newline at end of file diff --git a/test/spec/moddle.js b/test/spec/moddle.js index 4460ecd..b80f55b 100644 --- a/test/spec/moddle.js +++ b/test/spec/moddle.js @@ -356,6 +356,38 @@ describe('moddle', function() { expect(element.get('props:count')).to.eql(10); }); + + it('should access global name', function() { + + // when + const element = moddle.create('props:ComplexCount', { + ':xmlns': 'http://foo' + }); + + // then + expect(element.get(':xmlns')).to.eql('http://foo'); + expect(element.get('xmlns')).to.eql('http://foo'); + + // available as extension attribute + expect(element.$attrs).to.have.property('xmlns'); + }); + + + it('should access global name (no prefix)', function() { + + // when + const element = moddle.create('props:ComplexCount', { + 'xmlns': 'http://foo' + }); + + // then + expect(element.get(':xmlns')).to.eql('http://foo'); + expect(element.get('xmlns')).to.eql('http://foo'); + + // available as extension attribute + expect(element.$attrs).to.have.property('xmlns'); + }); + }); @@ -438,6 +470,25 @@ describe('moddle', function() { }); + it('should access global name', function() { + + // when + const element = moddle.create('props:ComplexCount', { + ':xmlns': 'http://foo' + }); + + // then + expect(element.get(':xmlns')).to.eql('http://foo'); + + expect(() => { + element.get('xmlns'); + }).to.throw(/unknown property on /); + + // available as extension attribute + expect(element.$attrs).to.have.property('xmlns'); + }); + + it('fail accessing unknown property', function() { // when