"use strict";
const helpers = require("owe-helpers");
const Binding = require("./Binding");
const exposed = require("./exposed");
const proxify = require("./proxify");
const errorHandled = Symbol("errorHandled");
const boundObject = Symbol("boundObject");
const object = Symbol("object");
const route = Symbol("route");
const origin = Symbol("origin");
/**
* Represents an API node.
*/
class Api {
/**
* @constructor
* @param {object|Promise} pObject A bound object this {@link Api} should be exposing. This may also be a Promise that resolves to a bound object.
* @param {any[]} pRoute The stack of routes that led to this Api pointer.
* @param {object} [pOrigin={}] An object to use as the origin of this Api.
*/
constructor(pObject, pRoute, pOrigin) {
this[route] = pRoute || [];
this[origin] = pOrigin || {};
this[boundObject] = Promise.resolve(pObject).then(object => {
if(!Binding.isBound(object))
throw new exposed.TypeError(`Object at position '${this[route].map(helpers.string.convert).join("/")}' is not exposed.`);
return object;
}).catch(errorHandlers.route.bind(this));
}
/**
* Setter for the origin of an {@link Api}.
* @param {object} value The origin object for the new Api node.
* @return {Api} Returns a new {@link Api} with the given origin, that points at the same exposed object.
*/
origin(value) {
if(!value || typeof value !== "object")
throw new TypeError("Api origin has to be an object.");
const clone = Object.create(this);
clone[origin] = value;
return clone;
}
/**
* Routes the Api according to its exposed objects routing function.
* @param {...any} destination The destination to route to. Multiple destinations are handled like a chained {@link Api#route} call.
* @return {Api} A new {@link Api} for the object the routing function returned.
*/
route(destination) {
let api = new Api(
this[boundObject].then(object => Binding.getBinding(object).route(
this[route],
this[origin],
destination
)),
[...this[route], destination],
this[origin]
);
for(let i = 1; i < arguments.length; i++)
api = api.route(arguments[i]);
return api;
}
/**
* Closes the Api with the closing function of its exposed object.
* @param {any} [data=undefined] The data to close with.
* @return {Promise} A promise that resolves to the return value of the closing function.
*/
close(data) {
return this[boundObject].then(object => {
return Binding.getBinding(object).close(this[route], this[origin], data);
}).catch(errorHandlers.close.bind(this, data));
}
/**
* Shorthand for `this.close().then()`.
* @param {function} success The success function.
* @param {function} fail The fail function.
* @return {Promise} Result of `this.close()`.
*/
then(success, fail) {
return this.close().then(success, fail);
}
/**
* Shorthand for `this.close().catch()`.
* @param {function} fail The fail function.
* @return {Promise} Rejects if `this.close()` rejects.
*/
catch(fail) {
return this.close().catch(fail);
}
/**
* Resolves to the exposed object this {@link Api} is pointing to.
* @type {Promise}
*/
get object() {
return this[object]
|| (this[object] = this[boundObject].then(object => Binding.getBinding(object).target));
}
/**
* A proxy that returns `this.route(A).proxified` when property `A` is accessed and `this.close(B)` when called with parameter `B`.
* `then` and `catch` however are directly passed through.
* @type {Proxy}
*/
get proxified() {
return proxify(this);
}
}
Api.prototype[proxify.passthrough] = new Set(["then", "catch"]);
const errorHandlers = {
/**
* Handle routing errors.
* @private
* @param {Error} err Error.
* @return {undefined}
*/
route(err) {
try {
if(!(errorHandled in err)) {
err.type = "route";
err.route = this[route];
err[errorHandled] = true;
}
}
finally {
throw err;
}
},
/**
* Handle closing errors.
* @private
* @param {any} data The data that caused err.
* @param {Error} err Error.
* @return {undefined}
*/
close(data, err) {
try {
if(!(errorHandled in err)) {
err.type = "close";
err.route = this[route];
err.data = data;
err[errorHandled] = true;
}
}
finally {
throw err;
}
}
};
module.exports = Api;