Initial commit

master
Sven Slootweg 4 years ago
commit 6be177aaaf

@ -0,0 +1,3 @@
{
"extends": "@joepie91/eslint-config"
}

1
.gitignore vendored

@ -0,0 +1 @@
node_modules

@ -0,0 +1,97 @@
# merge-by-template
__This module is still a work-in-progress!__ It's already usable in production code, but the documentation is still rough, and its API might still change in the future.
## Example
A runnable version of this example (and more examples) can be found in `example.js` in this project's repository.
First, you define a merging template, which results in a custom merging function:
```js
const mergeByTemplate = require("merge-by-template");
let mergeConfiguration = mergeByTemplate.createMerger({
/* `null` makes it explicit that this property should be overridden as a single value,
despite being an object - but leaving the property out entirely would have had the
same result, so this is strictly for readability */
database: null,
scripts: {},
accessList: [],
powerLevel: (a, b) => a + b
});
```
Then, you use that with two or more input values (eg. objects):
```js
let defaultConfiguration = {
database: {
type: "socket",
path: "/default"
},
scripts: {
test: "echo 'no test configured'",
publish: "npm publish"
},
accessList: [
"maintainer-bot"
],
powerLevel: 8999
};
let customConfiguration = {
database: {
hostname: "localhost",
port: "1234",
username: "hello",
password: "world"
},
scripts: {
test: "node test.js",
build: "node build.js"
},
accessList: [
"real-person"
],
powerLevel: 2
};
console.log(mergeConfiguration([ defaultConfiguration, customConfiguration ]));
```
... and the result of that is a value that's been deep-merged according to your specifications:
```js
{
database: {
hostname: 'localhost',
port: '1234',
username: 'hello',
password: 'world'
},
scripts: {
test: 'node test.js',
publish: 'npm publish',
build: 'node build.js'
},
accessList: [ 'maintainer-bot', 'real-person' ],
powerLevel: 9001
}
```
## Rules
This section will be expanded in the future.
__The basic principle:__ Regardless of how many values you pass into the custom merging function, it will always merge them per 2. So if you pass in `[ a, b, c ]` then it will first merge `b` onto `a`, and then merge `c` onto the result of the `b -> a` merger. The B side always takes precedence in the default merging strategies.
For now, a quick listing of rule syntax:
- __No rule specified (or explicit `null` or `undefined` specified):__ One value overrides the other in full, even if that value is an object or array.
- __Object specified:__ The input values are expected to be plain objects, and each property will be merged/overridden individually.
- __Empty object:__ This means all properties are merged according to "No rule specified", ie. the value of one object's property overrides the other.
- __Object with rules:__ Each specified property is merged according to whatever rule syntax is used for that property. Unspecified properties are overridden according to "No rule specified".
- __Empty array specified:__ The input values are expected to be arrays, and they will be concatenated together.
- __Array with items specified:__ Each item is treated as a positional rule. So if you specify an array with two rules, the first item of each input array gets merged according to the first rule, the second item of each input array according to the second rule, and so on. Any surplus items for which no rule exists, are overridden according to "No rule specified".
- __Function specified:__ The function is called with `(a, b)` as arguments, and is expected to return whatever the merge result should be. This lets you implement any custom merging logic, at any level in the data structure.

@ -0,0 +1,84 @@
"use strict";
const mergeByTemplate = require("./");
//----- Array merging ------//
// TODO: Document that the array-merging behaviour changes depending on whether the array in the template contains any items!
let mergeArray = mergeByTemplate.createMerger([ {
prop: (a, b) => a + b
} ]);
let arrayA = [
{ prop: 1 },
{ prop: 2 }
];
let arrayB = [
{ prop: 3 },
{ prop: 4 }
];
console.log(mergeArray([ arrayA, arrayB ])); // [ { prop: 4 }, { prop: 6 } ]
// TODO: Document merge functions, also for arrays
//----- Object merging ------//
let mergeConfiguration = mergeByTemplate.createMerger({
// `null` makes it explicit that this property should be overridden as a single value, despite being an object - but leaving the property out entirely would have had the same result, so this is strictly for readability
database: null,
scripts: {},
accessList: [],
powerLevel: (a, b) => a + b
});
let defaultConfiguration = {
database: {
type: "socket",
path: "/default"
},
scripts: {
test: "echo 'no test configured'",
publish: "npm publish"
},
accessList: [
"maintainer-bot"
],
powerLevel: 8999
};
let customConfiguration = {
database: {
hostname: "localhost",
port: "1234",
username: "hello",
password: "world"
},
scripts: {
test: "node test.js",
build: "node build.js"
},
accessList: [
"real-person"
],
powerLevel: 2
};
console.log(mergeConfiguration([ defaultConfiguration, customConfiguration ])); /*
{
database: {
hostname: 'localhost',
port: '1234',
username: 'hello',
password: 'world'
},
scripts: {
test: 'node test.js',
publish: 'npm publish',
build: 'node build.js'
},
accessList: [ 'maintainer-bot', 'real-person' ],
powerLevel: 9001
}
*/

@ -0,0 +1,132 @@
"use strict";
const util = require("util");
const range = require("range").range;
const fromEntries = require("fromentries");
const { validateArguments, validateValue } = require("@validatem/core");
const isArray = require("@validatem/is-array");
const isPlainObject = require("@validatem/is-plain-object");
const defaultTo = require("@validatem/default-to");
const hasLengthOf = require("@validatem/has-length-of");
const removeNullishItems = require("@validatem/remove-nullish-items");
const virtualProperty = require("@validatem/virtual-property");
const wrapPath = require("@validatem/wrap-path");
/* NOTE: In some cases below, we explicitly check for `undefined` only, rather than for both `undefined` and `null`. This is to allow explicitly overriding existent values with `null` during a merge. */
function wrapValidationPath(basePathSegments, lastProperty, rules) {
let combinedPath = basePathSegments.concat(virtualProperty(lastProperty));
return wrapPath(combinedPath, rules);
}
function mapToObject(items, mapper) {
return fromEntries(items.map(mapper));
}
function combineKeys(...objects) {
let allKeys = new Set();
for (let object of objects) {
for (let key of Object.keys(object)) {
allKeys.add(key);
}
}
return Array.from(allKeys);
}
let optionalArray = [ defaultTo([]), isArray ];
let optionalObject = [ defaultTo({}), isPlainObject ];
function mergeArray(subTemplate, aInput, bInput, path) {
let aItems = validateValue(aInput, wrapValidationPath(path, "a", [ optionalArray ]));
let bItems = validateValue(bInput, wrapValidationPath(path, "b", [ optionalArray ]));
let valueRule = subTemplate[0];
if (valueRule == null) {
/* No object merging rule specified, so just concatenate the items. */
return aItems.concat(bItems);
} else {
/* Object merging rule specified, so we should invoke that merging rule for each pair of objects. */
let itemCount = Math.max(aItems.length, bItems.length);
return range(0, itemCount).map((i) => {
return mergeValue(valueRule, aItems[i], bItems[i], path.concat([ i ]));
});
}
}
function mergeObject(subTemplate, aInput, bInput, path) {
let a = validateValue(aInput, wrapValidationPath(path, "a", [ optionalObject ]));
let b = validateValue(bInput, wrapValidationPath(path, "b", [ optionalObject ]));
let allKeys = combineKeys(a, b, subTemplate);
return mapToObject(allKeys, (key) => {
let rule = subTemplate[key];
let value = mergeValue(rule, a[key], b[key], path.concat([ key ]));
return [ key, value ];
});
}
function mergeValue(rule, a, b, path) {
if (rule == null) {
if (b !== undefined) {
return b;
} else {
return a;
}
} else if (typeof rule === "function") {
if (a === undefined) {
return b;
} else if (b === undefined) {
return a;
} else {
return rule(a, b);
}
} else if (typeof rule === "object") {
if (Array.isArray(rule)) {
return mergeArray(rule, a, b, path);
} else {
return mergeObject(rule, a, b, path);
}
} else {
throw new Error(`Unrecognized rule: ${util.inspect(rule)}`);
}
}
module.exports = {
createMerger: function createMerger(template) {
return function merge(_items) {
let [ items ] = validateArguments(arguments, [
[ "items", [
isArray,
removeNullishItems,
hasLengthOf(2)
]]
]);
return items.slice(1).reduce((merged, item) => {
return mergeValue(template, merged, item, []);
}, items[0]);
};
},
anyProperty: function (template) {
/* Used for cases where an object is used like a key->value map */
return function merge(aInput, bInput, path) {
let a = validateValue(aInput, wrapValidationPath(path, "a", [ optionalObject ]));
let b = validateValue(bInput, wrapValidationPath(path, "b", [ optionalObject ]));
let allKeys = combineKeys(a, b);
return mapToObject(allKeys, (key) => {
let value = mergeValue(template, a[key], b[key], path.concat([ key ]));
return [ key, value ];
});
};
}
};

@ -0,0 +1,25 @@
{
"name": "merge-by-template",
"version": "0.1.0",
"main": "index.js",
"repository": "git@git.cryto.net:joepie91/merge-by-template.git",
"author": "Sven Slootweg <admin@cryto.net>",
"license": "MIT",
"devDependencies": {
"@joepie91/eslint-config": "^1.1.0",
"eslint": "^6.8.0"
},
"dependencies": {
"@validatem/core": "^0.3.3",
"@validatem/default-to": "^0.1.0",
"@validatem/has-length-of": "^0.1.0",
"@validatem/is-array": "^0.1.1",
"@validatem/is-plain-object": "^0.1.1",
"@validatem/remove-nullish-items": "^0.1.0",
"@validatem/virtual-property": "^0.1.0",
"@validatem/wrap-path": "^0.1.0",
"default-value": "^1.0.0",
"fromentries": "^1.2.0",
"range": "^0.0.3"
}
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save