set.js

import isPlainObject from './isPlainObject';
import _each from 'lodash/each';
import isInteger, { RANGE_INDEX } from './isInteger';

// attention: since mobx will wrap the object in a Proxy, the source value will be different from the wrapped one
// Here must return the obj[index] of obj[key], otherwise the child node will not be mounted to the root object

export const addEntry = (obj, key, value, numberAsArrayIndex) => {
    if (numberAsArrayIndex && isInteger(key, { range: RANGE_INDEX })) {
        if (Array.isArray(obj)) {
            const index = parseInt(key, 10);

            if (obj.length <= index) {
                const numToFill = index - obj.length;
                if (numToFill > 0) {
                    for (let i = 0; i < numToFill; i++) {
                        obj.push(undefined);
                    }
                }

                obj.push(value);
            } else {
                obj[index] = value;
            }

            return obj[index];
        }
    }

    obj[key] = value;
    return obj[key];
};

/**
 * Set a value by dot-separated path or key array into a collection
 * Does not support '[i]', e.g. 'a[0].b.c' style accessor, use [ 'a',  0, 'b', 'c' ] instead, different from lodash/set
 * @alias  object.set
 * @param {Object} collection - The collection
 * @param {string} keyPath - A dot-separated path (dsp) or a key array, e.g. settings.xxx.yyy, or ['setting', 'xxx', 'yyy']
 * @param {Object} value - The default value if the path does not exist
 * @returns {*}
 */
const _set = (collection, keyPath, value, options) => {
    options = { numberAsArrayIndex: true, keyPathSeparator: '.', ...options };

    if (collection == null || typeof collection !== 'object') {
        return collection;
    }

    if (keyPath == null) {
        return collection;
    }

    if (isPlainObject(keyPath) && typeof value === 'undefined') {
        // extract all key value pair and set
        _each(keyPath, (v, k) => _set(collection, k, v, options));
        return collection;
    }

    // break the path into nodes array
    let nodes = Array.isArray(keyPath) ? keyPath.concat() : keyPath.split(options.keyPathSeparator);
    const length = nodes.length;

    if (length > 0) {
        const lastIndex = length - 1;

        let index = 0;
        let nested = collection;

        while (nested != null && index < lastIndex) {
            const key = nodes[index++];

            let next = nested[key];
            if (next == null || typeof next !== 'object') {
                // peek next node, see if it is integer
                const nextKey = nodes[index];

                if (options.numberAsArrayIndex && isInteger(nextKey, { range: RANGE_INDEX })) {
                    next = addEntry(nested, key, [], options.numberAsArrayIndex);
                } else {
                    next = addEntry(nested, key, {}, options.numberAsArrayIndex);
                }
            }

            nested = next;
        }

        const lastKey = nodes[lastIndex];
        addEntry(nested, lastKey, value, options.numberAsArrayIndex);
    }

    return collection;
};

export default _set;