Skip to content

Commit

Permalink
Make defaultKey/defaultValue immutable, broaden constructor input ins…
Browse files Browse the repository at this point in the history
…tead. Also support enum object comparison and serialization.
  • Loading branch information
Evan King committed Mar 7, 2016
1 parent 851e9e2 commit f8ab8b3
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 99 deletions.
116 changes: 86 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
[![deps status][daviddm-img]][daviddm-url]
[![mit license][license-img]][license-url]

Primitive-enum is a a lightweight enum generator that aims to optimize convenience
and utility without resorting to enumerated keys, values, or pairs wrapped in objects.
This enum facility may be the best fit if you need full control over how values are
generated for enumerated properties, want efficient lookup for both properties and
values, want to maximize interoperability by keeping values primitive (string, int,
float), or just want to reference enums via terse expressions.
Primitive-enum is a a lightweight generator of immutable enums that aims to optimize
convenience and utility without resorting to enumerated keys, values, or pairs wrapped
in objects. This enum facility may be the best fit if you:

- want convenient lookup for both properties and values
- need full control over how values are generated for enumerated properties
- want to maximize interoperability by keeping values primitive (string, int, float)
- want the reliability of immutable enums
- or just want to keep enum usage terse

## Basic Usage

Expand All @@ -23,17 +26,33 @@ const Enum = require('primitive-enum');

const myEnum = Enum({a: 'x', c: 'z', b: 'y'});

// myEnum is identifiable
myEnum instanceof Enum; // => true
myEnum.name; // => 'PrimitiveEnum'

// myEnum provides immutable metadata
myEnum.keys; // => ['a', 'c', 'b'];
myEnum.values; // => ['x', 'z', 'y'];
myEnum.a; // => 'x'
myEnum.x; // => 'a'
myEnum['a']; // => 'x'
myEnum.map; // => {a: 'x', c: 'z', b: 'y'}
myEnum.reverseMap; // => {x: 'a', z: 'c', y: 'b'}
myEnum.defaultKey; // => 'a'
myEnum.defaultValue; // => 'x'
myEnum.count; // => 3

// myEnum supports multiple forms of expression and lookup
myEnum.a; // => 'x'
myEnum['a']; // => 'x'
myEnum('a'); // => 'x'
myEnum.value('a') // => 'x'
myEnum.value('x') // => undefined

myEnum.x; // => 'a'
myEnum['x']; // => 'a'
myEnum('x'); // => 'a'
myEnum.key('x') // => 'a'
myEnum.key('a') // => undefined

// myEnum is iterable
var keys = [];
for(var key in myEnum) {
keys.push(key);
Expand All @@ -46,11 +65,13 @@ keys; // => ['a', 'c', 'b']
### Construction

```javascript
const myEnum = Enum(mapping, transform);
const myEnum = Enum(mapping, options);
```

* `mapping` is either an object defining key => value enum pairs, or an array listing only keys.
* `transform` is a function of the form `fn(value, key|idx) => enum-value` used to transform or generate the enum values to pair with keys. Several built-in transform options are available, along with configurable default behavior. See [Value Transforms](#value-transforms).
- `mapping` is either an object defining key => value enum pairs, or an array listing only keys.
- `options` is either a configuration object or one of the properties accepted in one:
- `options.defaultKey` is a string identifying the default key (and by extension default value). If unspecified, it will be the first key defined.
- `options.transform` is a function of the form `fn(value, key|idx) => enum-value` used to transform or generate the enum values to pair with keys. Several built-in transform options are available, along with configurable default behavior. See [Value Transforms](#value-transforms).

### Retrieval

Expand Down Expand Up @@ -134,24 +155,48 @@ myEnum.value('a'); // => 'x'
myEnum.value('x'); // => undefined
```

### Extras

PrimitiveEnums also specify a default key/value, which is initially the first pair defined.

###### Enum.defaultKey

```javascript
myEnum.defaultKey; // => 'a'
myEnum.defaultKey = 'b'; // => 'b'
myEnum.defaultValue; // => 'y'
myEnum.defaultKey = 'd'; // throws Error
myEnum.defaultKey = 'b'; // throws Error
```

###### Enum.defaultValue

```javascript
myEnum.defaultValue; // => 'y'
myEnum.defaultValue = 'x'; // => 'x'
myEnum.defaultKey; // => 'a'
myEnum.defaultValue = 'w'; // throws Error
myEnum.defaultValue; // => 'x'
myEnum.defaultValue = 'y'; // throws Error
```

PrimitiveEnum instances are string-comparable, so long as their enumerated values are
comparable primitives, and serializable. The constructed results are preserved, but
details of construction are discarded.

###### Enum.toString()

```javascript
const
enum1 = Enum(['a', 'b', 'c'], Enum.bitwise),
enum2 = Enum({a: 1, b: 2, c: 4});

enum1.toString(); // => '[Function: PrimitiveEnum] a,b,c|1,2,3|0
''+enum1 == enum2; // => true
```

###### Enum.fromJSON(str|obj)

```javascript
const jsonStr = JSON.stringify(myEnum);
const copiedEnum = Enum.fromJSON(jsonStr);
''+copiedEnum == myEnum; // => true
```


### Value Transforms

When constructing an enum from an array, the array values are treated as enum keys.
Expand All @@ -161,9 +206,9 @@ function to determine the paired enum values. Several standard options are made
through the enum constructor, and as well as configuration properties to alter the
default choices.

* `Enum.defaultArrayTransform` Default transform to use on arrays. Initially [Enum.sequence](#enumsequence)
* `Enum.defaultObjectTransform` Default transform to use on objects. Initially unset (`Enum.defaultTransform` is used instead).
* `Enum.defaultTransform` Default transform to use for arrays or objects if no input-type-specific transform is provided. Initially [Enum.identity](#enumidentity). Changing is supported but not advised.
- `Enum.defaultArrayTransform` Default transform to use on arrays. Initially [Enum.sequence](#enumsequence)
- `Enum.defaultObjectTransform` Default transform to use on objects. Initially unset (`Enum.defaultTransform` is used instead).
- `Enum.defaultTransform` Default transform to use for arrays or objects if no input-type-specific transform is provided. Initially [Enum.identity](#enumidentity). Changing is supported but not advised.

###### Enum.identity

Expand Down Expand Up @@ -228,18 +273,29 @@ const evenEnum = Enum(['a', 'b'], even);
evenEnum.map; // => {a: 2, b: 4}
```

### Limitations
## Limitations

By design, primitive-enum does not allow the same value to be used as two different keys
nor as two different values. Future support for explicit enum property aliases is plausible
but not planned. Additionally, no same value may be used as both enum key and enum value,
except in the case of matching key-value pairs where key == value. This is partly to enforce
good enum-defining conventions, and partly to minimize drawbacks of using the most convenient
means of performing enum lookups - which works with keys and values interchangeably.
nor as two different values. Additionally, no same value may be used as both enum key
and enum value, except in the case of matching key-value pairs where key == value. This
is partly to enforce good enum-defining conventions, and partly to minimize limitations
of using the most convenient means of performing enum lookups - which works with keys and
values interchangeably.

Lastly, all enum keys and values are expected to be simple primitives, castable to strings.
Instead supplying custom objects which cast to (unique) strings should also work, but is not
explicitly supported at this time.
Instead supplying custom objects which cast to (unique) strings may also work, but is not
explicitly supported.

## Roadmap

The following features are planned for the 1.0 release:

- Package for client-side use (with browser testing and elimination of es6 dependencies).
- Add support for aliases (`options.aliases` probably as map of key => [alternate keys]).

The following feature is being (weakly) considered:

- Throw errors where possible when referencing an invalid enum key or value.

[version-url]: https://github.com/evan-king/node-primitive-enum/releases
[version-img]: https://img.shields.io/github/release/evan-king/node-primitive-enum.svg?style=flat
Expand Down
64 changes: 46 additions & 18 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,31 @@
* myEnum.count: // 2
*
* @param mixed inputMap Array of keys or Object mapping keys to values
* @param mixed transform Function describing how to generate enum values from inputMap
* @param mixed options Configuration object supporting these options:
* - transform: Function describing how to generate enum values from inputMap
* - defaultKey: String specifying which key/value are considered the default option
* Alternatively, options can be either one of those directly.
*/
const Enum = function PrimitiveEnumBuilder(inputMap, transform) {
const Enum = function PrimitiveEnumBuilder(inputMap, options) {

const
opts = buildOptions(options),
keys = [],
values = [],
map = {},
reverseMap = {},
API = function PrimitiveEnum(lookup) { return API[''+lookup]; } ;

function buildOptions(options) {
switch(typeof options) {
case 'string': return {defaultKey: options};
case 'function': return {transform: options};
case 'object': return options;
case 'undefined': return {};
default: throw new TypeError('Invalid argument:' + typeof options);
}
}

// Note: We don't want a value appearing twice on one side or
// appearing on both sides unless as part of the same mapping.
// We could handle them safely with slightly reduced functionality,
Expand All @@ -55,12 +69,6 @@ const Enum = function PrimitiveEnumBuilder(inputMap, transform) {
});
}

function index(val) {
if(val === undefined) return index.def || 0;
if(keys[val] === undefined) throw new Error('Default out of bounds');
return index.def = val;
}

function addMapping(key, val) {
key.__proto__ = API;
val.__proto__ = API;
Expand All @@ -73,6 +81,7 @@ const Enum = function PrimitiveEnumBuilder(inputMap, transform) {
prop(val, key);
}

let transform = opts.transform;
if(Array.isArray(inputMap)) {
if(transform === undefined) transform = Enum.defaultArrayTransform || Enum.identity;
if(typeof transform !== 'function') throw new Error('Invalid transform');
Expand All @@ -83,6 +92,16 @@ const Enum = function PrimitiveEnumBuilder(inputMap, transform) {
Object.keys(inputMap).forEach(key => addMapping(key, transform(inputMap[key], key)));
}

const defIdx = (opts.defaultKey === undefined) ? 0 : keys.indexOf(opts.defaultKey);
if(defIdx < 0) throw new Error('Invalid default key');

function toJSON() {
return { type: 'PrimitiveEnum', map: map, defaultKey: keys[defIdx] };
}

// Provide a string representation for comparability
const asString = '[Function: PrimitiveEnum] '+keys.join(',')+'|'+values.join(',')+'|'+defIdx;

API.__proto__ = Enum.prototype;
prop('map', Object.freeze(map));
prop('reverseMap', Object.freeze(reverseMap));
Expand All @@ -91,16 +110,10 @@ const Enum = function PrimitiveEnumBuilder(inputMap, transform) {
prop('count', keys.length);
prop('key', val => reverseMap[''+val]);
prop('value', key => map[''+key]);

Object.defineProperty(API, 'defaultKey', {
get: () => keys[index()],
set: (k) => index(keys.indexOf(''+k))
});

Object.defineProperty(API, 'defaultValue', {
get: () => values[index()],
set: (v) => index(values.indexOf(''+v))
});
prop('defaultKey', keys[defIdx]);
prop('defaultValue', values[defIdx]);
prop('toJSON', toJSON);
prop('toString', () => asString);

return Object.freeze(API);
}
Expand All @@ -114,6 +127,14 @@ function eprop(name, value, write) {
});
}

eprop('fromJSON', function(obj) {
if(typeof obj === "string") obj = JSON.parse(obj);
if(obj.type != 'PrimitiveEnum') {
throw new TypeError('Input is not a serialized PrimitiveEnum');
}
return Enum(obj.map, obj.defaultKey);
});

// Pre-built mapping transforms

// key == value
Expand All @@ -136,4 +157,11 @@ eprop('defaultObjectTransform', undefined, true);
// Fallback default transform for arrays and objects. Changing not recommended - may be hard-coded before 1.0.0
eprop('defaultTransform', Enum.identity, true);

// Undocumented method for unit-testing convenience
eprop('resetDefaultTransforms', function() {
Enum.defaultArrayTransform = Enum.sequence;
Enum.defaultObjectTransform = undefined;
Enum.defaultTransform = Enum.identity;
});

module.exports = Enum;
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "primitive-enum",
"version": "0.9.4",
"version": "0.9.5",
"description": "Lightweight enums with primitive datatypes",
"main": "index.js",
"scripts": {
Expand All @@ -12,7 +12,10 @@
},
"keywords": [
"enum",
"primitive"
"enumeration",
"immutable",
"primitive",
"constants"
],
"author": "Evan King",
"license": "MIT",
Expand Down
Loading

0 comments on commit f8ab8b3

Please sign in to comment.