Source: access.js

var assign = require('./assign');

/**
 * The given object (if <var>mutate</var> evals to `true`)
 * or a copy of each own property of the given object.
 *
 * @typedef {!object} just.access~handler_this
 */

/**
 * A function to call when {@link just.access} reaches the deep property of an object.
 *
 * @typedef {function} just.access~handler
 * @this just.access~handler_this
 * @param {!object} lastObject - The object containing the <var>lastKey</var>.
 * @param {string} lastKey - The last value given in <var>path</var>.
 * @param {boolean} hasProperty - `false` if some key of <var>path</var>
 *     was created, `true` otherwise.
 * @param {string[]} path - The given keys.
 * @return {*} The return value for {@link just.access|the main function}.
 */

/**
 * Accesses to a deep property in a new <var>object</var>
 * (or <var>object</var> if <var>mutate</var> evals to `true`).
 *
 * @namespace
 * @memberof just
 * @param {!object} object - The base object.
 * @param {string[]} [path=[path]] - The ordered keys.
 * @param {just.access~handler} [handler=returnValue] - A custom function.
 * @param {object} opts
 * @param {boolean} [opts.mutate=false] - If `true`, it will use
 *     the given object as the base object, otherwise it will
 *     copy all the owned properties to a new object.
 * @param {boolean} [opts.override=true] - If `true`, and the
 *     current value is different to `null` or `undefined`,
 *     the function will throw a TypeError.
 *     If `false`, the current value will be overriden by
 *     an empty object if it's not an object nor `undefined`.
 *
 * @throws {TypeError} If some property causes access problems.
 *
 * @example <caption>Accessing to some existent property</caption>
 * just.access({a: {b: {c: {d: 4}}}}, ['a', 'b', 'c', 'd'], function (currentObject, currentKey, hasProperty, path) {
 *     return hasProperty ? currentObject[currentKey] : null;
 * }); // returns 4.
 *
 * @example <caption>Accessing to some property with a non-JSON-like-object as a value</caption>
 * just.access({a: 1}, ['a', 'b', 'c']); // throws TypeError.
 * just.access({a: 1}, ['a', 'b', 'c'], null, {
 *     'override': true
 * }); // returns undefined.
 * // Doesn't throw because it replaces `1` with an empty object
 * // and keeps accessing to the next properties.
 *
 * @example <caption>Accessing to some non-existent property</caption>
 * var obj = {z: 1, prototype: [...]};
 * var newObj = just.access(obj, 'a.b.c'.split('.'), function (currentObject, currentKey, hasProperty, path) {
 *
 *     if (!hasProperty) {
 *         currentObject[currentKey] = path.length;
 *     }
 *
 *     // At this point:
 *     //     `obj` is {z: 1},
 *     //     `currentObject` has a value in `currentKey`,
 *     //     and `this` has all the added keys (even the ones modified in `currentObject`).
 *     return this;
 *
 * }); // returns {z: 1, a: {b: {c: 3}}}
 *
 * // if you want the prototype chain of obj, just copy it.
 * just.assign(newObj.prototype, obj.prototype);
 *
 * @example <caption>Modifying the base object</caption>
 * var obj = {a: {b: false}, b: {b: false}, prototype: [...]};
 *
 * just.access(obj, 'a.b'.split('.'), function (currentObject, currentKey, hasProperty, path) {
 *     currentObject[currentKey] = 2;
 * }, true);
 *
 * // now `obj` is {a: {a: true}, b: {b: true}, prototype: [...]}.
 *
 * @return {*} If <var>handler</var> is given: the returned value of that function,
 *         otherwise: the last value of <var>path</var> in the copied object.
 */
function access (object, path, handler, opts) {

    var options = assign({
        'override': true,
        'mutate': false
    }, opts);
    var properties = (Array.isArray(path)
        ? path
        : [path]
    );
    var initialObject = (options.mutate
        ? object
        : assign({}, object)
    );
    var currentObject = initialObject;
    var isNewProperty = false;
    var lastKey = properties[properties.length - 1];
    var hasProperty;

    properties.slice(0, -1).forEach(function (key, i) {

        var value = currentObject[key];

        if (!(value instanceof Object)) {

            if (typeof value !== 'undefined' && value !== null && !options.override) {

                throw new TypeError(key + ' is not an object.');

            }

            isNewProperty = true;
            currentObject[key] = value = {};

        }

        currentObject = value;

    });

    hasProperty = lastKey in currentObject && !isNewProperty;

    return (handler
        ? handler.call(initialObject, currentObject, lastKey, hasProperty, properties)
        : currentObject[lastKey]
    );

}

module.exports = access;