From d3d24fa1440c981f78731c6abe56cc40564fda85 Mon Sep 17 00:00:00 2001 From: Yifeng Wang Date: Mon, 8 Jan 2018 16:56:23 +0800 Subject: [PATCH] handle custom childKey --- src/bumpover/index.js | 117 +++++---------------------------------- src/bumpover/traverse.js | 70 +++++++++++++++++++++++ src/bumpover/utils.js | 44 +++++++++++++++ src/rules.js | 3 +- test/bumpover/rule.js | 64 +++++++++++++++++++++ 5 files changed, 195 insertions(+), 103 deletions(-) create mode 100644 src/bumpover/traverse.js create mode 100644 src/bumpover/utils.js diff --git a/src/bumpover/index.js b/src/bumpover/index.js index 675719e..1f4ec86 100644 --- a/src/bumpover/index.js +++ b/src/bumpover/index.js @@ -1,103 +1,15 @@ import { deepEqual } from 'assert' import { Rules, getRule } from '../rules' import { Options } from '../options' - -// Result items can be array, object or null. -// Flatten results to array of objects. -function sanitizeResults (maybeResults) { - const results = maybeResults - .map(result => Array.isArray(result) ? result : [result]) - .reduce((a, b) => [...a, ...b], []) - .filter(result => !!result) - return results -} - -// Validate node with possible struct provided in rules. -function validateNode (node, struct) { - if (!struct) return node - try { return struct(node) } catch (e) { throw e } -} - -// Result can be array, object or null. Unify its shape to result struct. -function resolveResult (node, result, struct, childKey) { - if (!result) { - return { action: 'stop', newNode: null } - } else if (Array.isArray(result)) { - return { - action: 'next', - newNode: { - ...validateNode(node, struct), - [childKey]: sanitizeResults(result) - } - } - } else { - // Provide default action. - const { action = 'next', node } = result - const newNode = validateNode(node, struct) - return { action, newNode } - } -} - -function bumpChildren (node, rules, options, bumpFn, resolve, reject) { - const { childKey } = options - // Outlet for leaf node. - if (!node) { - resolve(null) - return - } - - if (!Array.isArray(node[childKey])) { - resolve(node) - return - } - - const children = node[childKey] - const childPromises = children.map(bumpFn) - const bumpAll = Promise.all(childPromises) - bumpAll.then(results => { - resolve({ ...node, [childKey]: sanitizeResults(results) }) - }).catch(reject) -} - -function bumpIgnoredNode (node, rule, options, bumpFn, resolve, reject) { - const { childKey } = options - // Resolve null if the ignored node is leaf. - if (!node || !node[childKey]) { - resolve(null) - return - } - // Resolve array of results. - const children = node[childKey] - const childPromises = children.map(bumpFn) - const bumpAll = Promise.all(childPromises) - bumpAll.then(results => { - resolve(sanitizeResults(results)) - }).catch(reject) -} - -function bumpRoot (node, options, bumpFn, resolve, reject) { - const { childKey, serializer, defaultValue } = options - if (!node) { - resolve(defaultValue) - return - } - - if (!Array.isArray(node[childKey])) { - resolve(serializer(node) || defaultValue) - return - } - - const children = node[childKey] - const childPromises = children.map(bumpFn) - const bumpChildren = Promise.all(childPromises) - bumpChildren.then(results => { - const output = serializer({ - ...node, - [childKey]: sanitizeResults(results) - }) - resolve(output || defaultValue) - }).catch(reject) -} +import { + bumpChildren, + bumpIgnoredNode, + bumpRoot +} from './traverse' +import { + getChildKey, + resolveResult +} from './utils' export class Bumpover { constructor (rules = [], options = {}) { @@ -126,7 +38,7 @@ export class Bumpover { if (!rule) { const { ignoreUnknown, onUnmatch } = options if (ignoreUnknown) { - bumpIgnoredNode(node, rule, options, bumpNode, resolve, reject) + bumpIgnoredNode(node, options, bumpNode, resolve, reject) return } else { onUnmatch(node) @@ -137,7 +49,7 @@ export class Bumpover { } rule.update(node).then(result => { - const { childKey } = options + const childKey = getChildKey(node, rules, options) const { action, newNode } = resolveResult( node, result, rule.struct, childKey ) @@ -170,16 +82,17 @@ export class Bumpover { if (ignoreUnknown) resolve(defaultValue) else { this.options.onUnmatch(rootNode) - bumpRoot(rootNode, options, bumpNode, resolve, reject) + bumpRoot(rootNode, [], options, bumpNode, resolve, reject) } } else { rule.update(rootNode).then(result => { - const { childKey, serializer, defaultValue } = options + const { serializer, defaultValue } = options + const childKey = getChildKey(rootNode, rules, options) const { action, newNode } = resolveResult( rootNode, result, rule.struct, childKey ) if (action === 'next') { - bumpRoot(newNode, options, bumpNode, resolve, reject) + bumpRoot(newNode, rules, options, bumpNode, resolve, reject) } else if (action === 'stop') { resolve(serializer(newNode) || defaultValue) } else reject(new Error(`Unknown action:\n${action}`)) diff --git a/src/bumpover/traverse.js b/src/bumpover/traverse.js new file mode 100644 index 0000000..872b5d0 --- /dev/null +++ b/src/bumpover/traverse.js @@ -0,0 +1,70 @@ +import { + getChildKey, + sanitizeResults +} from './utils' + +// Recursively bump node. +export function bumpChildren (node, rules, options, bumpFn, resolve, reject) { + const childKey = getChildKey(node, rules, options) + // Outlet for leaf node. + if (!node) { + resolve(null) + return + } + + if (!Array.isArray(node[childKey])) { + resolve(node) + return + } + + const children = node[childKey] + const childPromises = children.map(bumpFn) + const bumpAll = Promise.all(childPromises) + bumpAll.then(results => { + resolve({ ...node, [childKey]: sanitizeResults(results) }) + }).catch(reject) +} + +// Recursively bump node. Since ignored nodes doesn't have their `rules`, +// we don't pass in `rules` here, other args remains the same. +export function bumpIgnoredNode (node, options, bumpFn, resolve, reject) { + const childKey = getChildKey(node, [], options) + // Resolve null if the ignored node is leaf. + if (!node || !node[childKey]) { + resolve(null) + return + } + // Resolve array of results. + const children = node[childKey] + const childPromises = children.map(bumpFn) + const bumpAll = Promise.all(childPromises) + bumpAll.then(results => { + resolve(sanitizeResults(results)) + }).catch(reject) +} + +// Recursively bump root node. +export function bumpRoot (node, rules, options, bumpFn, resolve, reject) { + const { serializer, defaultValue } = options + const childKey = getChildKey(node, rules, options) + if (!node) { + resolve(defaultValue) + return + } + + if (!Array.isArray(node[childKey])) { + resolve(serializer(node) || defaultValue) + return + } + + const children = node[childKey] + const childPromises = children.map(bumpFn) + const bumpChildren = Promise.all(childPromises) + bumpChildren.then(results => { + const output = serializer({ + ...node, + [childKey]: sanitizeResults(results) + }) + resolve(output || defaultValue) + }).catch(reject) +} diff --git a/src/bumpover/utils.js b/src/bumpover/utils.js new file mode 100644 index 0000000..ef2cf37 --- /dev/null +++ b/src/bumpover/utils.js @@ -0,0 +1,44 @@ +import { getRule } from '../rules' + +// Validate node with possible struct provided in rules. +function validateNode (node, struct) { + if (!struct) return node + try { return struct(node) } catch (e) { throw e } +} + +// Result items can be array, object or null. +// Flatten results to array of objects. +export function sanitizeResults (maybeResults) { + const results = maybeResults + .map(result => Array.isArray(result) ? result : [result]) + .reduce((a, b) => [...a, ...b], []) + .filter(result => !!result) + return results +} + +// Get child key by rules and options +export function getChildKey (node, rules, options) { + const rule = getRule(node, rules) + if (rule && rule.childKey) return rule.childKey + else return options.childKey +} + +// Result can be array, object or null. Unify its shape to result struct. +export function resolveResult (node, result, struct, childKey) { + if (!result) { + return { action: 'stop', newNode: null } + } else if (Array.isArray(result)) { + return { + action: 'next', + newNode: { + ...validateNode(node, struct), + [childKey]: sanitizeResults(result) + } + } + } else { + // Provide default action. + const { action = 'next', node } = result + const newNode = validateNode(node, struct) + return { action, newNode } + } +} diff --git a/src/rules.js b/src/rules.js index 9ea03d3..63ce890 100644 --- a/src/rules.js +++ b/src/rules.js @@ -3,7 +3,8 @@ import { struct } from 'superstruct' const Rule = struct({ match: 'function', update: 'function', - struct: 'function?' + struct: 'function?', + childKey: 'string?' }) export const Rules = struct([Rule]) diff --git a/test/bumpover/rule.js b/test/bumpover/rule.js index ee4ae0d..56ef5ef 100644 --- a/test/bumpover/rule.js +++ b/test/bumpover/rule.js @@ -377,3 +377,67 @@ test('invalid action on node', async t => { await t.throws(bumper.bump(input)) }) + +test('custom child key', async t => { + const input = { + name: 'div', + props: {}, + children: [ + { + name: 'span', + props: {}, + foo: [ + { name: 'span', props: {}, children: [] }, + { name: 'small', props: {}, children: [] }, + { name: 'span', props: {}, children: [] } + ] + } + ] + } + + const expected = { + name: 'div', + props: {}, + children: [ + { + name: 'span', + props: { + fontSize: '16px' + }, + foo: [ + { + name: 'span', + props: { + fontSize: '16px' + }, + children: [] + }, + { name: 'small', props: {}, children: [] }, + { + name: 'span', + props: { + fontSize: '16px' + }, + children: [] + } + ] + } + ] + } + + const rules = [ + { + match: ({ name }) => name === 'span', + update: (node) => new Promise((resolve, reject) => { + resolve({ + node: { ...node, props: { fontSize: '16px' } } + }) + }), + childKey: 'foo' + } + ] + + const bumper = new Bumpover(rules) + + return bumper.bump(input).then(actual => t.deepEqual(actual, expected)) +})