diff --git a/index.js b/index.js index ec6a8a5..ecf8371 100644 --- a/index.js +++ b/index.js @@ -7,10 +7,12 @@ 'use strict'; +const { deleteProperty } = Reflect; +const isPrimitive = require('is-primitive'); const isPlainObject = require('is-plain-object'); -const isObject = val => { - return (typeof val === 'object' && val !== null) || typeof val === 'function'; +const isObject = value => { + return (typeof value === 'object' && value !== null) || typeof value === 'function'; }; const isUnsafeKey = key => { @@ -18,8 +20,8 @@ const isUnsafeKey = key => { }; const validateKey = key => { - if (typeof key !== 'string' && typeof key !== 'number') { - key = String(key); + if (!isPrimitive(key)) { + throw new TypeError('Object keys must be strings or symbols'); } if (isUnsafeKey(key)) { @@ -27,7 +29,7 @@ const validateKey = key => { } }; -const toString = input => { +const toStringKey = input => { return Array.isArray(input) ? input.flat().map(String).join(',') : input; }; @@ -43,71 +45,74 @@ const createMemoKey = (input, options) => { }; const memoize = (input, options, fn) => { - const key = toString(options ? createMemoKey(input, options) : input); + const key = toStringKey(options ? createMemoKey(input, options) : input); validateKey(key); - const val = setValue.cache.get(key) || fn(); - setValue.cache.set(key, val); - return val; + const value = setValue.cache.get(key) || fn(); + setValue.cache.set(key, value); + return value; }; -const isNumber = value => { - if (value.trim() !== '') { - const number = Number(value); - return { is: Number.isInteger(number), number }; - } - return { is: false }; -}; - -const splitString = (input, options) => { - const opts = options || {}; - const sep = opts.separator || '.'; - const preserve = sep === '/' ? false : opts.preservePaths; +const splitString = (input, options = {}) => { + const sep = options.separator || '.'; + const preserve = sep === '/' ? false : options.preservePaths; - if (typeof input === 'symbol') { + if (typeof input === 'string' && preserve !== false && /\//.test(input)) { return [input]; } - if (typeof opts.split === 'function') { - return opts.split(input); - } + const parts = []; + let part = ''; - const keys = Array.isArray(input) ? input : input.split(sep); - - if (typeof input === 'string' && preserve !== false && /\//.test(input)) { - return [input]; - } + const push = part => { + let number; + if (part.trim() !== '' && Number.isInteger((number = Number(part)))) { + parts.push(number); + } else { + parts.push(part); + } + }; - for (let i = 0; i < keys.length; i++) { - if (typeof keys[i] !== 'string') break; - const { is, number } = isNumber(keys[i]); + for (let i = 0; i < input.length; i++) { + const value = input[i]; - if (is) { - keys[i] = number; + if (value === '\\') { + part += input[++i]; continue; } - while (keys[i] && i < keys.length && keys[i].endsWith('\\') && typeof keys[i + 1] === 'string') { - keys[i] = keys[i].slice(0, -1) + sep + keys.splice(i + 1, 1); + if (value === sep) { + push(part); + part = ''; + continue; } + + part += value; + } + + if (part) { + push(part); } - return keys; + return parts; }; const split = (input, options) => { + if (options && typeof options.split === 'function') return options.split(input); + if (typeof input === 'symbol') return [input]; + if (Array.isArray(input)) return input; return memoize(input, options, () => splitString(input, options)); }; -const setProp = (obj, prop, value, options) => { +const assignProp = (obj, prop, value, options) => { validateKey(prop); // Delete property when "value" is undefined if (value === undefined) { - delete obj[prop]; + deleteProperty(obj, prop); } else if (options && options.merge) { - const merge = options.merge === true ? Object.assign : options.merge; + const merge = options.merge === 'function' ? options.merge : Object.assign; // Only merge plain objects if (merge && isPlainObject(obj[prop]) && isPlainObject(value)) { @@ -123,28 +128,25 @@ const setProp = (obj, prop, value, options) => { return obj; }; -const setValue = (obj, path, value, options) => { - if (!path) return obj; - if (!isObject(obj)) return obj; +const setValue = (target, path, value, options) => { + if (!path || !isObject(target)) return target; const keys = split(path, options); - const len = keys.length; - const target = obj; + let obj = target; - for (let i = 0; i < len; i++) { + for (let i = 0; i < keys.length; i++) { const key = keys[i]; const next = keys[i + 1]; validateKey(key); if (next === undefined) { - setProp(obj, key, value, options); + assignProp(obj, key, value, options); break; } if (typeof next === 'number' && !Array.isArray(obj[key])) { - obj[key] = []; - obj = obj[key]; + obj = obj[key] = []; continue; } @@ -158,6 +160,7 @@ const setValue = (obj, path, value, options) => { return target; }; +setValue.split = split; setValue.cache = new Map(); setValue.clear = () => { setValue.cache = new Map(); diff --git a/package.json b/package.json index 3865c1f..14a366e 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,12 @@ "test": "mocha" }, "dependencies": { - "is-plain-object": "^2.0.4" + "is-plain-object": "^2.0.4", + "is-primitive": "^3.0.1" }, "devDependencies": { "gulp-format-md": "^2.0.0", - "mocha": "^8.3.2", + "mocha": "^9.1.1", "split-string": "^6.1.0" }, "keywords": [