diff --git a/.releases/4.47.0.md b/.releases/4.47.0.md new file mode 100644 index 0000000..429bbf5 --- /dev/null +++ b/.releases/4.47.0.md @@ -0,0 +1,3 @@ +**New Features** + +* Added an optional numeric `mapping` value to the `Enum` class (used when external systems identify enumeration items with integer values). This change is backwards compatible. \ No newline at end of file diff --git a/lang/Enum.js b/lang/Enum.js index 85f446a..292e5b7 100644 --- a/lang/Enum.js +++ b/lang/Enum.js @@ -1,4 +1,5 @@ -const assert = require('./assert'); +const assert = require('./assert'), + is = require('./is'); module.exports = (() => { 'use strict'; @@ -14,24 +15,32 @@ module.exports = (() => { * @interface * @param {String} code - The unique code of the enumeration item. * @param {String} description - A description of the enumeration item. + * @param {Number=} mapping - An alternate key value (used when external systems identify enumeration items using integer values). */ class Enum { - constructor(code, description) { + constructor(code, description, mapping) { assert.argumentIsRequired(code, 'code', String); assert.argumentIsRequired(description, 'description', String); + assert.argumentIsOptional(mapping, 'mapping', Number); + + if (is.number(mapping)) { + assert.argumentIsValid(mapping, 'mapping', is.integer, 'must be an integer'); + } this._code = code; this._description = description; + this._mapping = mapping || null; + const c = this.constructor; if (!types.has(c)) { types.set(c, [ ]); } - const existing = Enum.fromCode(c, code); + const valid = Enum.fromCode(c, this._code) === null && (this._mapping === null || Enum.fromMapping(c, this._mapping) === null); - if (existing === null) { + if (valid) { types.get(c).push(this); } } @@ -56,6 +65,17 @@ module.exports = (() => { return this._description; } + /** + * An alternate key value (used when external systems identify enumeration items + * using numeric values). This value will not be present for all enumerations. + * + * @public + * @returns {Number|null} + */ + get mapping() { + return this._mapping; + } + /** * Returns true if the provided {@link Enum} argument is equal * to the instance. @@ -86,14 +106,32 @@ module.exports = (() => { * @static * @param {Function} type - The enumeration type. * @param {String} code - The enumeration item's code. - * @returns {*|null} + * @returns {Enum|null} */ static fromCode(type, code) { return Enum.getItems(type).find(x => x.code === code) || null; } /** - * Returns all of the enumeration's items (given an enumeration type). + * Looks up a enumeration item; given the enumeration type and the enumeration + * item's value. If no matching item can be found, a null value is returned. + * + * @public + * @static + * @param {Function} type - The enumeration type. + * @param {String} mapping - The enumeration item's mapping value. + * @returns {Enum|null} + */ + static fromMapping(type, mapping) { + if (mapping === null) { + return null; + } + + return Enum.getItems(type).find(x => x.mapping === mapping) || null; + } + + /** + * Returns the enumeration's items (given an enumeration type). * * @public * @static diff --git a/test/specs/lang/EnumSpec.js b/test/specs/lang/EnumSpec.js index 9665262..f1cdfba 100644 --- a/test/specs/lang/EnumSpec.js +++ b/test/specs/lang/EnumSpec.js @@ -37,14 +37,75 @@ describe('When Enum is extended (as types EnumA and EnumB) and type items are ad }); describe('and a duplicate item (A-x) is added', () => { - let axx = new EnumA('x', 'A-XX'); + let invalid = new EnumA('x', 'A-XX'); - it('should be still find the original instance in EnumA for X', () => { + it('should still only have two items', () => { + expect(Enum.getItems(EnumA).length).toEqual(2); + }); + + it('should still able to find the original instance in EnumA for X', () => { expect(Enum.fromCode(EnumA, 'x')).toBe(ax); }); + it('should not be able to find the mapping for the duplicated item', () => { + expect(Enum.getItems(EnumA).some(x => x === invalid)).toEqual(false); + }); + it('should should equal the original instance (for X)', () => { - expect(Enum.fromCode(EnumA, 'x').equals(axx)).toBe(true); + expect(Enum.fromCode(EnumA, 'x').equals(ax)).toBe(true); + }); + }); +}); + +describe('When Enum is extended (as types EnumA and EnumB) and type items are added to each (X and Y) which include mapping values', () => { + 'use strict'; + + class EnumA extends Enum { + constructor(code, description, mapping) { + super(code, description, mapping); + } + } + + class EnumB extends Enum { + constructor(code, description, mapping) { + super(code, description, mapping); + } + } + + let ax = new EnumA('x', 'A-X', 1); + let ay = new EnumA('y', 'A-Y', 2); + let bx = new EnumB('x', 'B-X', 1); + let by = new EnumB('y', 'B-Y', 2); + + it('should be able to find X in EnumA using the mapping value', () => { + expect(Enum.fromMapping(EnumA, 1)).toBe(ax); + }); + + it('should be able to find Y in EnumA using the mapping value', () => { + expect(Enum.fromMapping(EnumA, 2)).toBe(ay); + }); + + it('should be able to find X in EnumB using the mapping value', () => { + expect(Enum.fromMapping(EnumB, 1)).toBe(bx); + }); + + it('should be able to find Y in EnumB using the mapping value', () => { + expect(Enum.fromMapping(EnumB, 2)).toBe(by); + }); + + describe('and a duplicate mapping value is added', () => { + let invalid = new EnumA('z', 'A-Z', 2); + + it('should still only have two items', () => { + expect(Enum.getItems(EnumA).length).toEqual(2); + }); + + it('should still able to find the original instance in EnumA for Y', () => { + expect(Enum.fromMapping(EnumA, 2)).toBe(ay); + }); + + it('should not be able to find the mapping for the duplicated item', () => { + expect(Enum.getItems(EnumA).some(x => x === invalid)).toEqual(false); }); }); }); \ No newline at end of file