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;