Skip to content

Commit

Permalink
feat(data-point): Remove predefined execution order for hash and coll…
Browse files Browse the repository at this point in the history
…ection

to use more than one modifier, use a compose array

BREAKING CHANGE: No longer possible to use multiple modifiers without a compose array

#73
  • Loading branch information
Matthew Armstrong committed Feb 18, 2018
1 parent b608245 commit c62605d
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 182 deletions.
16 changes: 2 additions & 14 deletions packages/data-point/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2053,13 +2053,7 @@ A Hash entity transforms a _Hash_ like data structure. It enables you to manipul

To prevent unexpected results, **Hash** can only process **Plain Objects**, which are objects created by the Object constructor. If [Hash.value](#hash-value) does not resolve to a Plain Object it will **throw** an error.

Hash entities expose a set of reducers: [mapKeys](#hash-mapKeys), [omitKeys](#hash-omitKeys), [pickKeys](#hash-pickKeys), [addKeys](#hash-addKeys), [addValues](#hash-addValues). You may apply one or more of these reducers to a Hash entity. Keep in mind that those reducers will always be executed in a specific order:

```js
omitKeys -> pickKeys -> mapKeys -> addValues -> addKeys
```

If you want to have more control over the order of execution, you may use the [compose](#entity-compose-reducer) reducer.
Hash entities expose a set of reducers: [mapKeys](#hash-mapKeys), [omitKeys](#hash-omitKeys), [pickKeys](#hash-pickKeys), [addKeys](#hash-addKeys), [addValues](#hash-addValues). You may apply one or more of these reducers to a Hash entity.

**NOTE**: The Compose reducer is meant to operate only on Hash-type objects. If its context resolves to a non-Hash type, it will **throw an error**.

Expand Down Expand Up @@ -2387,7 +2381,7 @@ For examples of hash entities, see the [Examples](examples), on the unit tests:

A Collection entity enables you to operate over an array. Its API provides basic reducers to manipulate the elements in the array.

Collection entities expose a set of reducers that you may apply to them: [map](#collection-map), [find](#collection-find), [filter](#collection-filter). These reducers are executed in a [specific order](#collection-reducers-order). If you want to have more control over the order of execution, use the [compose](#compose-reducer) reducer.
Collection entities expose a set of reducers that you may apply to them: [map](#collection-map), [find](#collection-find), [filter](#collection-filter).

To prevent unexpected results, a **Collection Entity** can only process **Arrays**, if Collection.value does not resolve to an Array it will **throw** an error.

Expand Down Expand Up @@ -2429,12 +2423,6 @@ dataPoint.addEntities({
| *error* | [Reducer](#reducers) | reducer to be resolved in case of an error |
| *params* | `Object` | User-defined Hash that will be passed to every reducer within the context of the transform function's execution |

<a name="collection-reducers-order">The order of execution of is:</a>

```js
filter -> map -> find
```

##### <a name="collection-map">Collection.map</a>

Maps a transformation to each element in a collection.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,11 @@ function createCompose (composeSpec) {
*/
function create (spec, id) {
validateModifiers(id, spec, modifierKeys.concat('compose'))
parseCompose.validateComposeModifiers(id, spec, modifierKeys)

const entity = createBaseEntity(EntityCollection, spec, id)

const composeSpec = parseCompose.parse(spec, modifierKeys)
parseCompose.validateCompose(entity.id, composeSpec, modifierKeys)
if (composeSpec.length) {
entity.compose = createCompose(composeSpec)
const compose = parseCompose.parse(id, modifierKeys, spec)
if (compose.length) {
entity.compose = createCompose(compose)
}

return Object.freeze(entity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ test('modelFactory#create default', () => {
})

describe('parse loose modifiers', () => {
test('modelFactory#create default | checks that an entitiy containing one reducer has that respective property', () => {
test('modelFactory#create default | checks that an entity containing one reducer has that respective property', () => {
const result = modelFactory.create({
map: '$a'
})
Expand All @@ -25,19 +25,19 @@ describe('parse loose modifiers', () => {
expect(result.compose.reducer).toHaveProperty('type', 'ReducerPath')
})

test('modelFactory#create default | checks multiple reducers in an entitiy to have matching properties', () => {
const result = modelFactory.create({
map: '$a',
find: '$a',
filter: '$a'
})
// test('modelFactory#create default | checks multiple reducers in an entity to have matching properties', () => {
// const result = modelFactory.create({
// map: '$a',
// find: '$a',
// filter: '$a'
// })

expect(helpers.isReducer(result.compose)).toBe(true)
expect(result.compose).toHaveProperty('type', 'ReducerList')
expect(result.compose.reducers[0]).toHaveProperty('type', 'ReducerFilter')
expect(result.compose.reducers[1]).toHaveProperty('type', 'ReducerMap')
expect(result.compose.reducers[2]).toHaveProperty('type', 'ReducerFind')
})
// expect(helpers.isReducer(result.compose)).toBe(true)
// expect(result.compose).toHaveProperty('type', 'ReducerList')
// expect(result.compose.reducers[0]).toHaveProperty('type', 'ReducerFilter')
// expect(result.compose.reducers[1]).toHaveProperty('type', 'ReducerMap')
// expect(result.compose.reducers[2]).toHaveProperty('type', 'ReducerFind')
// })
})

describe('parse compose modifier', () => {
Expand All @@ -59,18 +59,18 @@ describe('parse compose modifier', () => {
}).toThrowErrorMatchingSnapshot()
})

test('throw error if compose is mixed with inline modifiers (map, filter, ..) ', () => {
expect(() => {
modelFactory.create(
{
compose: [{ map: '$a' }],
map: '$a',
filter: '$a'
},
['filter', 'map', 'find']
)
}).toThrow(/filter, map/)
})
// test('throw error if compose is mixed with inline modifiers (map, filter, ..) ', () => {
// expect(() => {
// modelFactory.create(
// {
// compose: [{ map: '$a' }],
// map: '$a',
// filter: '$a'
// },
// ['filter', 'map', 'find']
// )
// }).toThrow(/filter, map/)
// })

test('parses multiple modifiers, respect order', () => {
const result = modelFactory.create({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ exports[`factory#parse composed modifiers throw error if compose and loose modif

exports[`factory#parse composed modifiers throw error if compose non array 1`] = `"Entity hash:invalid compose property is expected to be an instance of Array, but found [object Object]"`;

exports[`factory#parse composed modifiers throw error if modifier is invalid 1`] = `"Modifier 'invalidKey' in hash:invalid doesn't match any of the registered Modifiers: omitKeys,pickKeys,mapKeys,addValues,addKeys"`;
exports[`factory#parse composed modifiers throw error if modifier is invalid 1`] = `"Modifier 'invalidKey' in hash:invalid doesn't match any of the valid Modifiers: omitKeys,pickKeys,mapKeys,addValues,addKeys"`;
5 changes: 1 addition & 4 deletions packages/data-point/lib/entity-types/entity-hash/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,9 @@ function createCompose (composeParse) {
*/
function create (spec, id) {
validateModifiers(id, spec, modifierKeys.concat('compose'))
parseCompose.validateComposeModifiers(id, spec, modifierKeys)

const entity = createBaseEntity(EntityHash, spec, id)

const compose = parseCompose.parse(spec, modifierKeys)
parseCompose.validateCompose(entity.id, compose, modifierKeys)
const compose = parseCompose.parse(id, modifierKeys, spec)
if (compose.length) {
entity.compose = createCompose(compose)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`parse should parse a single key 1`] = `
Array [
Object {
"spec": "$a",
"type": "map",
},
]
`;

exports[`parse should parse multiple keys inside of compose 1`] = `
Array [
Object {
"spec": "$a",
"type": "map",
},
Object {
"spec": "$b",
"type": "find",
},
]
`;

exports[`parse should throw error for invalid key inside compose 1`] = `"Modifier 'find' in id doesn't match any of the valid Modifiers: map"`;

exports[`parse should throw error when multiple keys are not inside compose 1`] = `"Entity id is invalid. When using multiple keys, they should be inside compose."`;

exports[`parse should throw error when the compose key and individual modifier keys are used together 1`] = `"Entity id is invalid; when 'compose' is defined the keys: 'map' should be inside compose."`;

exports[`parseComposeSpec should throw error if contains more than 1 key 1`] = `"Compose Modifiers may only contain one key, but found 2 (map, filter)"`;

exports[`parseComposeSpec should throw error if does not contain keys 1`] = `"Compose Modifiers may only contain one key, but found 0"`;
146 changes: 55 additions & 91 deletions packages/data-point/lib/entity-types/parse-compose/parse-compose.js
Original file line number Diff line number Diff line change
@@ -1,83 +1,72 @@
const intersection = require('lodash/intersection')

/**
* @param {string} id - Entity's Id
* @param {Object} spec
* @param {string} entityId
* @param {Array<string>} validKeys
* @param {Object} spec
* @throws if the spec is not valid
* @returns {boolean}
* @returns {Array<Object>}
*/
function validateComposeModifiers (id, spec, validKeys) {
if (!spec.compose) {
return true
}

if (!(spec.compose instanceof Array)) {
function parse (entityId, validKeys, spec) {
const specKeys = intersection(Object.keys(spec), validKeys)
if (specKeys.length > 1) {
throw new Error(
`Entity ${id} compose property is expected to be an instance of Array, but found ${
spec.compose
}`
`Entity ${entityId} is invalid. When using multiple keys, they should be inside compose.`
)
}

const specKeys = Object.keys(spec)
const invalidKeys = intersection(validKeys, specKeys)
if (invalidKeys.length !== 0) {
throw new Error(
`Entity ${id} is invalid; when 'compose' is defined the keys: '${invalidKeys.join(
', '
)}' should be inside compose.`
)
}

return true
}

module.exports.validateComposeModifiers = validateComposeModifiers
if (spec.compose) {
if (!Array.isArray(spec.compose)) {
throw new Error(
`Entity ${entityId} compose property is expected to be an instance of Array, but found ${
spec.compose
}`
)
}

/**
* @param {string} entityId
* @param {Array<Object>} compose
* @param {Array<string>} validKeys
* @throws if compose contains an invalid modifier type
* @returns {boolean}
*/
function validateCompose (entityId, compose, validKeys) {
compose.forEach(modifier => {
if (validKeys.indexOf(modifier.type) === -1) {
if (specKeys.length !== 0) {
throw new Error(
`Modifier '${
modifier.type
}' in ${entityId} doesn't match any of the registered Modifiers: ${validKeys}`
`Entity ${entityId} is invalid; when 'compose' is defined the keys: '${specKeys.join(
', '
)}' should be inside compose.`
)
}
})

return true
return parseComposeSpec(entityId, validKeys, spec.compose)
}

if (specKeys.length === 1) {
const key = specKeys[0]
return [createComposeReducer(key, spec[key])]
}

return []
}

module.exports.validateCompose = validateCompose
module.exports.parse = parse

/**
* @param {string} type
* @param {Object} spec
* @returns {Object}
* @param {string} entityId
* @param {Array<string>} modifierKeys
* @param {Array<Object>} composeSpec
* @returns {Array<Object>}
*/
function createComposeReducer (type, spec) {
return {
type,
spec
}
function parseComposeSpec (entityId, modifierKeys, composeSpec) {
return composeSpec.map(modifierSpec =>
parseModifierSpec(entityId, modifierKeys, modifierSpec)
)
}

module.exports.createComposeReducer = createComposeReducer
module.exports.parseComposeSpec = parseComposeSpec

/**
* @param {string} entityId
* @param {Array<string>} validKeys
* @param {Object} modifierSpec
* @throws if the spec does not contain exactly one key
* @throws if the spec is not valid
* @returns {Object}
*/
function parseModifierSpec (modifierSpec) {
function parseModifierSpec (entityId, validKeys, modifierSpec) {
const keys = Object.keys(modifierSpec)
if (keys.length !== 1) {
throw new Error(
Expand All @@ -88,49 +77,24 @@ function parseModifierSpec (modifierSpec) {
}

const type = keys[0]
return createComposeReducer(keys[0], modifierSpec[type])
}

module.exports.parseModifierSpec = parseModifierSpec

/**
* @param {Array<Object>} composeSpec
* @returns {Array<Object>}
*/
function parseComposeSpecProperty (composeSpec) {
return composeSpec.map(parseModifierSpec)
}

module.exports.parseComposeSpecProperty = parseComposeSpecProperty

/**
* @param {Object} entitySpec
* @param {Array<string>} modifierKeys
* @returns {Array<Object>}
*/
function parseComposeFromEntitySpec (entitySpec, modifierKeys) {
return modifierKeys.reduce((acc, modifierKey) => {
const modifierSpec = entitySpec[modifierKey]
if (modifierSpec) {
const modifier = createComposeReducer(modifierKey, modifierSpec)
acc.push(modifier)
}
if (!validKeys.includes(type)) {
throw new Error(
`Modifier '${type}' in ${entityId} doesn't match any of the valid Modifiers: ${validKeys}`
)
}

return acc
}, [])
return createComposeReducer(type, modifierSpec[type])
}

module.exports.parseComposeFromEntitySpec = parseComposeFromEntitySpec
module.exports.parseModifierSpec = parseModifierSpec

/**
* @param {Object} entitySpec
* @param {Array<string>} modifierKeys
* @returns {Array<Object>}
* @param {string} type
* @param {Object} spec
* @returns {Object}
*/
function parse (entitySpec, modifierKeys) {
return entitySpec.compose
? parseComposeSpecProperty(entitySpec.compose, modifierKeys)
: parseComposeFromEntitySpec(entitySpec, modifierKeys)
function createComposeReducer (type, spec) {
return { type, spec }
}

module.exports.parse = parse
module.exports.createComposeReducer = createComposeReducer
Loading

0 comments on commit c62605d

Please sign in to comment.