/**
* @typedef {string|number|Array<string|symbol|number>} KeyPath
*/
import { isObject, isSymbol, isArray, isUndefined, isNumber, isString } from './is.js'
/**
* convert a keyPath string to be an array
* @param {string} path
* @param {boolean} [isStrict] whether to keep square bracket keys
* @returns {array}
*/
export function makeKeyChain(path, isStrict) {
if (typeof path === 'number') {
return [path];
}
const mapping = []
const text = path.replace(/\[.*?\]/g, (matched, position) => {
const index = mapping.length
mapping.push(matched)
return `${position ? '.' : ''}{${index}}`
})
const chain = text.split('.')
chain.forEach((item, i) => {
if (/^\{\d+\}$/.test(item)) {
const index = item.substring(1, item.length - 1)
const str = mapping[index]
const key = isStrict ? str : str.substring(1, str.length - 1)
chain[i] = key
}
})
return chain
}
/**
* convert an array to be a keyPath string
* @param {array} chain the array for path, without any symbol in it
* @param {boolean} [isStrict] wether to use [] to wrap number key
* @returns {string}
*/
export function makeKeyPath(chain, isStrict) {
// if there is only one item, return the first one
// this support return a symbol
if (chain.length === 1) {
return chain[0]
}
let path = ''
for (let i = 0, len = chain.length; i < len; i ++) {
let key = chain[i]
// do not support symbols
if (isSymbol(key)) {
const symbol = key.toString()
path += '[' + symbol + ']'
}
// 1
else if (isStrict && isNumber(key)) {
path += '[' + key + ']'
}
// '1'
else if (isStrict && isString(key) && /^[0-9]+$/.test(key)) {
path += '[' + key + ']'
}
// '[1]' or '[a]' or '[a.b]'
else if (isStrict && isString(key) && /^\[.*\]$/.test(key)) {
path += key
}
// 'a.b'
else if (isString(key) && key.indexOf('.') > -1) {
path += '[' + key + ']'
}
else {
path += path ? '.' + key : key
}
}
return path
}
/**
* convert a keyPath array or string to be a keyPath string
* @param {KeyPath} keyPath
* @returns {string}
*/
export function makeKey(keyPath) {
const chain = isArray(keyPath) ? keyPath : makeKeyChain(keyPath)
const key = makeKeyPath(chain)
return key
}
/**
* parse a property's value by its keyPath
* @param {object|array} obj
* @param {KeyPath} key
*/
export function parse(obj, key) {
const chain = isArray(key) ? [...key] : makeKeyChain(key)
if (!chain.length) {
return obj
}
let target = obj
for (let i = 0, len = chain.length; i < len; i ++) {
// fallback, without error
if (!target || typeof target !== 'object') {
return
}
const key = chain[i]
// want an array
if (key === '*') {
if (!isArray(target)) {
return
}
if (i + 1 >= len) {
return target
}
const restChain = chain.slice(i + 1)
const items = target.map(item => parse(item, restChain))
return items
}
// want a value
const node = target[key]
target = node
}
return target
}
/**
* parse a property into a new object which contains only the parsed property
* @example
* var a = { a: 1, b: 2, c: 3 };
* var b = parseAs(a, 'b'); // -> { b: 2 }
* @param {object|array} obj
* @param {KeyPath} key
*/
export function parseAs(obj, key) {
const chain = isArray(key) ? [...key] : makeKeyChain(key);
if (!chain.length) {
return obj;
}
const results = isArray(obj) ? [] : {};
const keyPath = [];
let target = obj
for (let i = 0, len = chain.length; i < len; i ++) {
// fallback, without error
if (!target || typeof target !== 'object') {
return results;
}
const key = chain[i];
// want an array
if (key === '*') {
if (!isArray(target)) {
return results;
}
if (i + 1 >= len) {
return results;
}
const restChain = chain.slice(i + 1);
target.forEach((item, i) => {
const ret = parseAs(item, restChain);
assign(results, [keyPath, i], ret);
});
return results;
}
// want a value
keyPath.push(key);
const node = target[key]
target = node
}
assign(results, keyPath, target)
return results
}
/**
* assign a property's value by its keyPath
* @param {object|array} obj
* @param {KeyPath} key
* @param {any} value
* @returns {object|array}
*/
export function assign(obj, key, value) {
const chain = isArray(key) ? [...key] : makeKeyChain(key)
if (!chain.length) {
return obj
}
const tail = chain.pop()
if (!chain.length) {
obj[tail] = value
return obj
}
let target = obj
for (let i = 0, len = chain.length; i < len; i ++) {
const current = chain[i]
let next = chain[i + 1]
// at the end
if (isUndefined(next) && i === len - 1) {
next = tail
}
if (isNumber(next) && !isArray(target[current])) {
target[current] = []
}
else if (isString(next) && /^[0-9]+$/.test(next) && !isArray(target[current])) {
target[current] = []
}
else if (target[current] === null || typeof target[current] !== 'object') {
target[current] = {}
}
target = target[current]
}
target[tail] = value
return obj
}
/**
* remove a property by its keyPath
* @param {object|array} obj
* @param {KeyPath} key
* @returns {object|array}
*/
export function remove(obj, key) {
const chain = isArray(key) ? [...key] : makeKeyChain(key)
if (!chain.length) {
return obj
}
if (chain.length === 1) {
delete obj[chain[0]]
return obj
}
const tail = chain.pop()
const target = parse(obj, chain)
if (!isObject(target) && !isArray(target)) {
return obj
}
delete target[tail]
return obj
}
/**
* check whether a keyPath is in the given object,
* both string and symbol properties will be checked,
* as default, it will check:
* - both enumerable and non-enumerable properties;
* - both own and prototype-chain properties;
* if enumerable=true, it will check:
* - only enumerable properties;
* - only own properties;
* @param {KeyPath} key
* @param {*} obj
* @param {*} [enumerable]
* @returns {boolean}
*/
export function keyin(key, obj, enumerable) {
if (!obj || typeof obj !== 'object') {
return false
}
const chain = isArray(key) ? [...key] : makeKeyChain(key)
if (!chain.length) {
return false
}
const tail = chain.pop()
const has = (obj, key) => Object.prototype.propertyIsEnumerable.call(obj, key)
if (!chain.length) {
return enumerable ? has(obj, tail) : tail in obj
}
let target = obj
for (let i = 0, len = chain.length; i < len; i ++) {
const key = chain[i]
const node = enumerable ? (has(target, key) ? target[key] : null) : target[key]
if (!node || typeof node !== 'object') {
return false
}
target = node
}
return enumerable ? has(target, tail) : tail in target
}