mirror of
https://github.com/lowdefy/lowdefy.git
synced 2025-02-11 14:20:07 +08:00
feat(set): init @lowdefy/set
This commit is contained in:
parent
dab50c334c
commit
76585f5508
13
packages/set/.babelrc
Normal file
13
packages/set/.babelrc
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": "12",
|
||||
"esmodules": true
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
19
packages/set/jest.config.js
Normal file
19
packages/set/jest.config.js
Normal file
@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: 'coverage',
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
coveragePathIgnorePatterns: ['/dist/'],
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
coverageReporters: ['text'],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: 'node',
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
testPathIgnorePatterns: ['/dist/'],
|
||||
};
|
52
packages/set/package.json
Normal file
52
packages/set/package.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "@lowdefy/set",
|
||||
"version": "1.0.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "",
|
||||
"homepage": "https://lowdefy.com",
|
||||
"bugs": {
|
||||
"url": "https://github.com/lowdefy/lowdefy/issues"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Sam Tolmay",
|
||||
"url": "https://github.com/SamTolmay"
|
||||
},
|
||||
{
|
||||
"name": "Gerrie van Wyk",
|
||||
"url": "https://github.com/Gervwyk"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lowdefy/lowdefy.git"
|
||||
},
|
||||
"main": "dist/set.js",
|
||||
"scripts": {
|
||||
"build": "babel src --out-dir dist",
|
||||
"test": "jest --coverage",
|
||||
"prepare": "yarn build",
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lowdefy/type": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.8.4",
|
||||
"@babel/compat-data": "7.9.6",
|
||||
"@babel/core": "7.9.6",
|
||||
"@babel/preset-env": "7.9.6",
|
||||
"babel-jest": "24.9.0",
|
||||
"eslint": "6.8.0",
|
||||
"eslint-config-airbnb": "18.2.0",
|
||||
"eslint-config-prettier": "6.12.0",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "6.3.1",
|
||||
"eslint-plugin-prettier": "3.1.4",
|
||||
"eslint-plugin-react": "7.21.2",
|
||||
"eslint-plugin-react-hooks": "4.1.2",
|
||||
"jest": "24.9.0",
|
||||
"jest-diff": "24.9.0",
|
||||
"prettier": "2.1.2"
|
||||
}
|
||||
}
|
152
packages/set/src/set.js
Normal file
152
packages/set/src/set.js
Normal file
@ -0,0 +1,152 @@
|
||||
/* eslint-disable no-plusplus */
|
||||
/* eslint-disable no-use-before-define */
|
||||
/* eslint-disable no-param-reassign */
|
||||
|
||||
/*
|
||||
Copyright 2020 Lowdefy, Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Source:
|
||||
// https://github.com/jonschlinkert/set-value/blob/master/index.js
|
||||
// https://www.npmjs.com/package/set-value
|
||||
|
||||
// The MIT License (MIT)
|
||||
|
||||
// Copyright (c) 2014-2018, Jon Schlinkert.
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import type from '@lowdefy/type';
|
||||
|
||||
function isValidKey(key) {
|
||||
return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
|
||||
}
|
||||
|
||||
function isObjectOrFunction(val) {
|
||||
return val !== null && (typeof val === 'object' || typeof val === 'function');
|
||||
}
|
||||
|
||||
function result(target, path, value, merge) {
|
||||
if (merge && isObjectOrFunction(target[path]) && isObjectOrFunction(value)) {
|
||||
target[path] = merge({}, target[path], value);
|
||||
} else {
|
||||
target[path] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
set.memo = {};
|
||||
|
||||
function set(target, path, value, options) {
|
||||
if (!type.isObject(target)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
const opts = options || {};
|
||||
if (!type.isArray(path) && !type.isString(path)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
let { merge } = opts;
|
||||
if (merge && typeof merge !== 'function') {
|
||||
merge = Object.assign;
|
||||
}
|
||||
|
||||
const keys = (type.isArray(path) ? path : split(path, opts)).filter(isValidKey);
|
||||
const len = keys.length;
|
||||
const orig = target;
|
||||
|
||||
if (!options && keys.length === 1) {
|
||||
result(target, keys[0], value, merge);
|
||||
return target;
|
||||
}
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const prop = keys[i];
|
||||
const propUp = keys[i + 1]; // changed to set an array value where the array was undefined be assigning value
|
||||
if (!isObjectOrFunction(target[prop]) && !type.isInt(parseInt(propUp, 10))) {
|
||||
// changed
|
||||
target[prop] = {};
|
||||
} else if (!isObjectOrFunction(target[prop]) && type.isInt(parseInt(propUp, 10))) {
|
||||
// added
|
||||
target[prop] = []; // added
|
||||
}
|
||||
|
||||
if (i === len - 1) {
|
||||
result(target, prop, value, merge);
|
||||
break;
|
||||
}
|
||||
|
||||
target = target[prop];
|
||||
}
|
||||
|
||||
return orig;
|
||||
}
|
||||
|
||||
function createKey(pattern, options) {
|
||||
let id = pattern;
|
||||
if (typeof options === 'undefined') {
|
||||
return `${id}`;
|
||||
}
|
||||
const keys = Object.keys(options);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
id += `;${key}=${String(options[key])}`;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export function split(path, options) {
|
||||
const id = createKey(path, options);
|
||||
if (set.memo[id]) return set.memo[id];
|
||||
|
||||
const char = options && options.separator ? options.separator : '.';
|
||||
let keys = [];
|
||||
const res = [];
|
||||
|
||||
if (options && typeof options.split === 'function') {
|
||||
keys = options.split(path);
|
||||
} else {
|
||||
keys = path.split(char);
|
||||
}
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let prop = keys[i];
|
||||
while (prop && prop.slice(-1) === '\\' && keys[i + 1]) {
|
||||
prop = prop.slice(0, -1) + char + keys[++i];
|
||||
}
|
||||
res.push(prop);
|
||||
}
|
||||
set.memo[id] = res;
|
||||
return res;
|
||||
}
|
||||
|
||||
export default set;
|
277
packages/set/test/set.test.js
Normal file
277
packages/set/test/set.test.js
Normal file
@ -0,0 +1,277 @@
|
||||
import typeTest from '@lowdefy/type';
|
||||
|
||||
import set, { split } from '../src/set';
|
||||
|
||||
test('setNestedValue - set a nested value in array object', () => {
|
||||
const objOne = {
|
||||
a: [{ b: 2 }, { b: 5, c: 4 }],
|
||||
};
|
||||
expect(set(objOne, 'a.1.b', 10)).toEqual({ a: [{ b: 2 }, { b: 10, c: 4 }] });
|
||||
});
|
||||
|
||||
test('setNestedValue - set a nested object in array', () => {
|
||||
const objOne = {
|
||||
a: [{ b: 2 }, { b: 5, c: 4 }],
|
||||
};
|
||||
expect(set(objOne, 'a.1', { a: 2 })).toEqual({ a: [{ b: 2 }, { a: 2 }] });
|
||||
});
|
||||
|
||||
test('setNestedObject - set a.b', () => {
|
||||
const objOne = {
|
||||
a: { b: 1 },
|
||||
};
|
||||
expect(set(objOne, 'a.b', 5)).toEqual({ a: { b: 5 } });
|
||||
});
|
||||
|
||||
test('setNestedObject - set a', () => {
|
||||
const objOne = {
|
||||
a: 1,
|
||||
};
|
||||
expect(set(objOne, 'a', 5)).toEqual({ a: 5 });
|
||||
});
|
||||
|
||||
test('setNestedObject - set b when b is undefined', () => {
|
||||
const objOne = {
|
||||
a: 1,
|
||||
};
|
||||
expect(set(objOne, 'b', 5)).toEqual({ a: 1, b: 5 });
|
||||
});
|
||||
|
||||
test('setNestedObject - set b.c when b.c is undefined', () => {
|
||||
const objOne = {
|
||||
a: 1,
|
||||
};
|
||||
expect(set(objOne, 'b.c', 5)).toEqual({ a: 1, b: { c: 5 } });
|
||||
});
|
||||
|
||||
test('setNestedObject - set b.0.c when b.0.c is undefined', () => {
|
||||
const objOne = {
|
||||
a: 1,
|
||||
};
|
||||
expect(set(objOne, 'b.0.c', 5)).toEqual({ a: 1, b: [{ c: 5 }] });
|
||||
});
|
||||
|
||||
test('setNestedObject - set b.0.c when b.0.c is undefined (but b as empty array is)', () => {
|
||||
const objOne = {
|
||||
a: 1,
|
||||
b: [],
|
||||
};
|
||||
expect(set(objOne, 'b.0.c', 5)).toEqual({ a: 1, b: [{ c: 5 }] });
|
||||
});
|
||||
|
||||
test('setNestedObject - set b.1.c when b.1.c is undefined', () => {
|
||||
const objOne = {
|
||||
a: 1,
|
||||
b: [{ a: 10 }],
|
||||
};
|
||||
expect(set(objOne, 'b.1.c', 5)).toEqual({ a: 1, b: [{ a: 10 }, { c: 5 }] });
|
||||
});
|
||||
|
||||
test('setNestedObject - set b.3.c when b.3.c is undefined', () => {
|
||||
const objOne = {
|
||||
a: 1,
|
||||
b: [{ a: 10 }],
|
||||
};
|
||||
expect(set(objOne, 'b.3.c', 5)).toEqual({ a: 1, b: [{ a: 10 }, undefined, undefined, { c: 5 }] });
|
||||
});
|
||||
|
||||
test('setNestedObject - set a.b.3.c when a.b.3.c is undefined', () => {
|
||||
const objOne = {
|
||||
a: { b: [{ a: 10 }] },
|
||||
};
|
||||
expect(set(objOne, 'a.b.3.c', 5)).toEqual({
|
||||
a: { b: [{ a: 10 }, undefined, undefined, { c: 5 }] },
|
||||
});
|
||||
});
|
||||
|
||||
test('setNestedObject - set a.d.b.c.2 when a is undefined', () => {
|
||||
const objOne = {};
|
||||
expect(set(objOne, 'a.d.b.c.2', 5)).toEqual({
|
||||
a: { d: { b: { c: [undefined, undefined, 5] } } },
|
||||
});
|
||||
});
|
||||
|
||||
test('setNestedObject - set a.0.b.0.c when a is undefined', () => {
|
||||
const objOne = {};
|
||||
expect(set(objOne, 'a.0.b.0.c', 5)).toEqual({ a: [{ b: [{ c: 5 }] }] });
|
||||
});
|
||||
|
||||
// Tests from set-value github
|
||||
|
||||
test('setNestedObject - should return non-objects', () => {
|
||||
let res = set('foo', 'a.b', 'c');
|
||||
expect(res).toEqual('foo');
|
||||
res = set(null, 'a.b', 'c');
|
||||
expect(res).toEqual(null);
|
||||
});
|
||||
|
||||
test('setNestedObject - should create a nested property if it does not already exist', () => {
|
||||
const o = {};
|
||||
set(o, 'a.b', 'c');
|
||||
expect(o.a.b).toEqual('c');
|
||||
});
|
||||
|
||||
test('setNestedObject - should merge an existing value with the given value', () => {
|
||||
const o = { a: { b: { c: 'd' } } };
|
||||
set(o, 'a.b', { y: 'z' }, { merge: true });
|
||||
expect(o.a.b).toEqual({ c: 'd', y: 'z' });
|
||||
});
|
||||
|
||||
test('setNestedObject - should update an object value', () => {
|
||||
const o = {};
|
||||
set(o, 'a', { b: 'c' });
|
||||
set(o, 'a', { c: 'd' }, { merge: true });
|
||||
expect(o).toEqual({ a: { b: 'c', c: 'd' } });
|
||||
set(o, 'a', 'b');
|
||||
expect(o.a).toEqual('b'); // o.a, 'b'
|
||||
});
|
||||
|
||||
test('setNestedObject - should extend an array', () => {
|
||||
const o = { a: [] };
|
||||
expect(typeTest.isArray(o.a)).toEqual(true);
|
||||
set(o, 'a.0', { y: 'z' });
|
||||
expect(o.a[0]).toEqual({ y: 'z' });
|
||||
});
|
||||
|
||||
test('setNestedObject - should extend a function', () => {
|
||||
function log() {}
|
||||
const warning = function () {};
|
||||
const o = {};
|
||||
set(o, 'helpers.foo', log);
|
||||
set(o, 'helpers.foo.warning', warning);
|
||||
expect(typeTest.isFunction(o.helpers.foo)).toEqual(true);
|
||||
expect(typeTest.isFunction(o.helpers.foo.warning)).toEqual(true);
|
||||
});
|
||||
|
||||
test('setNestedObject - should extend an object in an array', () => {
|
||||
const o = { a: [{}, {}, {}] };
|
||||
set(o, 'a.0.a', { y: 'z' });
|
||||
set(o, 'a.1.b', { y: 'z' });
|
||||
set(o, 'a.2.c', { y: 'z' });
|
||||
expect(typeTest.isArray(o.a)).toEqual(true);
|
||||
expect(o.a[0].a).toEqual({ y: 'z' });
|
||||
expect(o.a[1].b).toEqual({ y: 'z' });
|
||||
expect(o.a[2].c).toEqual({ y: 'z' });
|
||||
});
|
||||
|
||||
test('setNestedObject - should create a deeply nested property if it does not already exist', () => {
|
||||
const o = {};
|
||||
set(o, 'a.b.c.d.e', 'c');
|
||||
expect(o.a.b.c.d.e).toEqual('c');
|
||||
});
|
||||
|
||||
test('setNestedObject - should not create a nested property if it does already exist', () => {
|
||||
const first = { name: 'Halle' };
|
||||
const o = { a: first };
|
||||
set(o, 'a.b', 'c');
|
||||
expect(o.a.b).toEqual('c');
|
||||
expect(o.a).toEqual(first);
|
||||
expect(o.a.name).toEqual('Halle');
|
||||
});
|
||||
|
||||
test('setNestedObject - should support immediate properties', () => {
|
||||
const o = {};
|
||||
set(o, 'a', 'b');
|
||||
expect(o.a).toEqual('b');
|
||||
});
|
||||
|
||||
test('setNestedObject - should support immediate properties', () => {
|
||||
const o = {};
|
||||
set(o, 'a.locals.name', { first: 'Brian' });
|
||||
set(o, 'b.locals.name', { last: 'Woodward' });
|
||||
set(o, 'b.locals.name.last', 'Woodward');
|
||||
expect(o).toEqual({
|
||||
a: { locals: { name: { first: 'Brian' } } },
|
||||
b: { locals: { name: { last: 'Woodward' } } },
|
||||
});
|
||||
});
|
||||
|
||||
test('setNestedObject - should add the property even if a value is not defined', () => {
|
||||
const fixture = {};
|
||||
expect(set(fixture, 'a.locals.name')).toEqual({ a: { locals: { name: undefined } } });
|
||||
expect(set(fixture, 'b.locals.name')).toEqual({
|
||||
b: { locals: { name: undefined } },
|
||||
a: { locals: { name: undefined } },
|
||||
});
|
||||
});
|
||||
|
||||
test('setNestedObject - should set the specified property.', () => {
|
||||
expect(set({ a: 'aaa', b: 'b' }, 'a', 'bbb')).toEqual({ a: 'bbb', b: 'b' });
|
||||
});
|
||||
|
||||
test('setNestedObject - should support passing an array as the key.', () => {
|
||||
const actual = set({ a: 'a', b: { c: 'd' } }, ['b', 'c', 'd'], 'eee');
|
||||
expect(actual).toEqual({ a: 'a', b: { c: { d: 'eee' } } });
|
||||
});
|
||||
|
||||
test('setNestedObject - should set a deeply nested value.', () => {
|
||||
const actual = set({ a: 'a', b: { c: 'd' } }, 'b.c.d', 'eee');
|
||||
expect(actual).toEqual({ a: 'a', b: { c: { d: 'eee' } } });
|
||||
});
|
||||
|
||||
test('setNestedObject - should return the entire object if no property is passed.', () => {
|
||||
expect(set({ a: 'a', b: { c: 'd' } })).toEqual({ a: 'a', b: { c: 'd' } });
|
||||
});
|
||||
|
||||
test('setNestedObject - should set a value only.', () => {
|
||||
expect(set({ a: 'a', b: { c: 'd' } }, 'b.c')).toEqual({ a: 'a', b: { c: undefined } });
|
||||
});
|
||||
|
||||
test('setNestedObject - should set non-plain objects', (done) => {
|
||||
const o = {};
|
||||
|
||||
set(o, 'a.b', new Date('July 20, 69 00:20:18 GMT+00:00'));
|
||||
const firstDate = o.a.b.getTime();
|
||||
|
||||
setTimeout(() => {
|
||||
set(o, 'a.b', new Date('July 20, 69 00:20:18 GMT+00:00'));
|
||||
const secondDate = o.a.b.getTime();
|
||||
|
||||
expect(firstDate).toEqual(secondDate);
|
||||
done();
|
||||
}, 10);
|
||||
});
|
||||
|
||||
test('setNestedObject - should not split escaped dots.', () => {
|
||||
const o = {};
|
||||
set(o, 'a\\.b.c.d.e', 'c', { escape: true });
|
||||
expect(o['a.b'].c.d.e).toEqual('c');
|
||||
});
|
||||
|
||||
test('setNestedObject - should work with multiple escaped dots.', () => {
|
||||
const obj1 = {};
|
||||
set(obj1, 'e\\.f\\.g', 1, { escape: true });
|
||||
expect(obj1['e.f.g']).toEqual(1);
|
||||
const obj2 = {};
|
||||
set(obj2, 'e\\.f.g\\.h\\.i.j', 1, { escape: true });
|
||||
expect(obj2).toEqual({ 'e.f': { 'g.h.i': { j: 1 } } });
|
||||
});
|
||||
|
||||
// Test split
|
||||
|
||||
test('setNestedObject - split on dot', () => {
|
||||
expect(split('a.b.c.d.e')).toEqual(['a', 'b', 'c', 'd', 'e']);
|
||||
});
|
||||
|
||||
test('setNestedObject - split should not split on escape dots', () => {
|
||||
expect(split('e\\.f\\.g')).toEqual(['e.f.g']);
|
||||
});
|
||||
|
||||
test('setNestedObject - split path with number', () => {
|
||||
expect(split('a.0.a')).toEqual(['a', '0', 'a']);
|
||||
});
|
||||
|
||||
test('setNestedObject - object pointers - we maintain the reference', () => {
|
||||
const objA = {};
|
||||
const objB = [{ b: 1 }];
|
||||
set(objA, 'a', objB);
|
||||
expect(objA).toEqual({ a: [{ b: 1 }] });
|
||||
expect(objB).toEqual([{ b: 1 }]);
|
||||
|
||||
set(objA, 'a.0.c', 1); // this is an edit on objB
|
||||
expect(objA).toEqual({ a: [{ b: 1, c: 1 }] });
|
||||
|
||||
// expect(objB).toEqual([{b: 1}]);
|
||||
// or - we maintain the reference
|
||||
expect(objB).toEqual([{ b: 1, c: 1 }]);
|
||||
});
|
Loading…
Reference in New Issue
Block a user