Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Socket
Sign inDemoInstall

clues

Package Overview
Dependencies
Maintainers
2
Versions
158
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

clues - npm Package Compare versions

Comparing version 4.0.0-rc2 to 4.0.0

circle.yml

4

bower.json
{
"name": "clues",
"version": "3.3.13",
"version": "3.5.45",
"main": "clues.js",

@@ -16,2 +16,2 @@ "scripts": [

]
}
}
(function(self) {
'use strict';
if (typeof module !== 'undefined') {

@@ -10,9 +11,26 @@ clues.Promise = require('bluebird');

var reArgs = /function.*?\(([^)]*?)\).*/;
var reArgs = /^\s*function.*?\(([^)]*?)\).*/;
var reEs6 = /^\s*\({0,1}([^)]*?)\){0,1}\s*=>/;
var reEs6Class = /^\s*[a-zA-Z0-9\-$_]+\((.*?)\)\s*{/;
var createEx = (e,fullref,caller,ref,value,report) => {
if (e.fullref) return e;
let result = {ref : e.ref || ref || fullref, message: e.message || e, fullref: e.fullref || fullref, caller: e.caller || caller, stack: e.stack || '', error: true, notDefined: e.notDefined, report: e.report || report, value: e.value || value};
return result;
};
var reject = (e,fullref,caller,ref) => clues.reject(createEx(e || {},fullref,caller,ref));
var isPromise = f => f && f.then && typeof f.then === 'function';
var noop = d => d;
function matchArgs(fn) {
if (!fn.__args__) {
var match = reArgs.exec(fn.prototype.constructor.toString());
fn.__args__ = match[1].replace(/\s/g,'')
var match = fn.prototype && fn.prototype.constructor.toString() || fn.toString();
match = match.replace(/^\s*async/,'');
match = reArgs.exec(match) || reEs6.exec(match) || reEs6Class.exec(match);
fn.__args__ = !match ? [] : match[1].replace(/\s/g,'')
.split(',')
.filter(function(d) {
if (d === '$private')
fn.private = true;
if (d === '$prep')
fn.prep = true;
return d.length;

@@ -24,3 +42,77 @@ });

function clues(logic,fn,$global,caller,fullref,dependencies) {
// fast promise rejection
const Rejection = clues.Promise.reject();
Rejection.suppressUnhandledRejections();
clues.reject = d => Object.create(Rejection,{_fulfillmentHandler0: {value: d}});
function clues(logic,fn,$global,caller,fullref) {
try {
let rawCluesResult = _rawClues(logic,fn,$global,caller,fullref);
return clues.Promise.resolve(rawCluesResult);
}
catch (e) {
return reject(e,fullref,caller);
}
}
function promiseHelper(val, success, error, _finally, _errorMessage) {
if (isPromise(val)) {
// if it's already resolve, we can just use that direct
if (!val.isFulfilled) val = clues.Promise.resolve(val);
if (val.isFulfilled()) return promiseHelper(val.value(), success, error, _finally);
if (val.isRejected()) return promiseHelper(null, success, error, _finally, val.reason());
let result = val;
if (success) result = result.then(success);
if (error) result = result.catch(error);
if (_finally) result = result.finally(_finally);
return result;
}
if (_errorMessage) {
let result = reject(_errorMessage);
if (error) {
result = error(_errorMessage);
}
if (_finally) _finally(val);
return result;
}
let result = null;
try {
result = success(val);
if (isPromise(result) && result.isRejected && result.isRejected()) {
result = promiseHelper(null, success, error, _finally, result.reason());
}
}
catch (e) { result = promiseHelper(null, success, error, _finally, e); }
if (_finally) _finally(val);
return result;
}
function storeRef(logic, ref, value, fullref, caller) {
if (ref) {
try {
logic[ref] = value;
}
catch (e) {
try {
Object.defineProperty(logic,ref,{value: value, enumerable: true, configurable: true, writable: true});
return value;
}
catch (e) {
return reject({ref : ref, message: 'Object immutable', fullref:fullref,caller: caller, stack: e.stack || '', value: value,error:true});
}
}
}
return value;
}
function expandFullRef(fullref, next) {
var separator = fullref && fullref[fullref.length-1] !== '(' && '.' || '';
return (fullref ? fullref+separator : '')+next;
}
function _rawClues(logic,fn,$global,caller,fullref) {
var args,ref;

@@ -30,7 +122,5 @@

if (typeof logic === 'function' || (logic && typeof logic.then === 'function'))
return clues({},logic,$global,caller,fullref,dependencies)
.then(function(logic) {
return clues(logic,fn,$global,caller,fullref,dependencies);
});
if (typeof logic === 'function' || isPromise(logic))
return promiseHelper(_rawClues({},logic,$global,caller,fullref),
logic => _rawClues(logic,fn,$global,caller,fullref));

@@ -43,17 +133,32 @@ if (typeof fn === 'string') {

var next = ref.slice(0,dot);
return clues(logic,next,$global,caller,fullref,dependencies)
.then(function(d) {
let handleError = e => {
if (e && e.notDefined && logic && logic.$external && typeof logic.$external === 'function') {
if (!logic[ref]) {
try {
return storeRef(logic, ref, _rawClues(logic,function() { return logic.$external.call(logic,ref); },$global,ref,expandFullRef(fullref, ref)), fullref, caller);
}
catch (e) {
fullref = expandFullRef(fullref, ref);
return storeRef(logic, ref, reject({message:e.message || e, value: value}, fullref, ref, ref), fullref, caller);
}
}
return logic[ref];
}
else {
return reject(e);
}
};
return promiseHelper(_rawClues(logic,next,$global,caller,fullref),
d => {
logic = d;
fullref = expandFullRef(fullref, next);
ref = ref.slice(dot+1);
fullref = (fullref ? fullref+'.' : '')+next;
return clues(logic,ref,$global,caller,fullref,dependencies);
})
.catch(function(e) {
if (e && e.notDefined && logic && logic.$external && typeof logic.$external === 'function')
return logic[ref] = logic[ref] || clues(logic,function() { return logic.$external.call(logic,ref); },$global,caller,(fullref ? fullref+'.' : '')+ref);
else throw e;
});
return promiseHelper(_rawClues(logic,ref,$global,caller,fullref), a => a, handleError);
},
handleError);
}
fullref = (fullref ? fullref+'^' : '')+ref;
fullref = expandFullRef(fullref, ref);
fn = logic ? logic[ref] : undefined;

@@ -64,6 +169,6 @@ if (fn === undefined) {

else if ($global[ref] && caller && caller !== '__user__')
return clues($global,ref,$global,caller,fullref,dependencies);
return _rawClues($global,ref,$global,caller,fullref);
else if (logic && logic.$property && typeof logic.$property === 'function')
fn = logic[ref] = function() { return logic.$property.call(logic,ref); };
else return clues.Promise.rejected({ref : ref, message: ref+' not defined', fullref:fullref,caller: caller, notDefined:true});
else return reject({message: ref+' not defined', notDefined:true},fullref,caller,ref);
}

@@ -78,3 +183,4 @@ }

if (fn.length === 1) fn = fn[0];
return clues(obj,fn,$global,caller,fullref,dependencies);
var result = _rawClues(obj,fn,$global,caller,fullref);
return storeRef(logic, ref, result, fullref, caller);
}

@@ -90,76 +196,141 @@ args = fn.slice(0,fn.length-1);

if (typeof fn === 'function')
args = (args || matchArgs(fn));
// If fn name is private or promise private is true, reject when called directly
if (fn && (!caller || caller == '__user__') && ((typeof(fn) === 'function' && fn.name == 'private') || (fn.then && fn.private)))
return clues.Promise.rejected({ref : ref, message: ref+' not defined', fullref:fullref,caller: caller, notDefined:true});
if (fn && (!caller || caller == '__user__') && ((typeof(fn) === 'function' && (fn.private || fn.name == '$private' || fn.name == 'private')) || (fn.then && fn.private)))
return reject({message: ref+' not defined', notDefined:true }, fullref, caller, ref);
// If the logic reference is not a function, we simply return the value
if (typeof fn !== 'function' || (ref && ref[0] === '$')) {
if (fn && dependencies && dependencies.indexOf(fn) !== -1)
return clues.Promise.rejected({ref: ref, message: 'recursive', fullref: fullref, caller: caller});
return clues.Promise.resolve(fn);
if (typeof fn !== 'function' || ((ref && ref[0] === '$') && !fn.prep && fn.name !== '$prep')) {
// If the value is a promise we wait for it to resolve to inspect the result
if (isPromise(fn))
return promiseHelper(fn, d => {
return (typeof d == 'function' || (d && typeof d == 'object' && d.length)) ? _rawClues(logic,d,$global,caller,fullref) : d;
});
else
return fn;
}
// Shortcuts to define empty objects with $property or $external
if (fn.name == '$property') return logic[ref] = clues.Promise.resolve({$property: fn.bind(logic)});
if (fn.name == '$external') return logic[ref] = clues.Promise.resolve({$external: fn.bind(logic)});
if (fn.name === '$property' || (args[0] === '$property' && args.length === 1)) return storeRef(logic, ref, {$property: fn.bind(logic)}, fullref, caller);
if (fn.name === '$external' || (args[0] === '$external' && args.length === 1)) return storeRef(logic, ref, {$external: fn.bind(logic)}, fullref, caller);
if (fn.name === '$service') return fn;
let argsHasPromise = false, errorArgs = null;
args = args.map(function(arg) {
if (arg === null || arg === undefined) return arg;
var optional,showError,res;
if ((optional = (arg[0] === '_'))) arg = arg.slice(1);
if ((showError = (arg[0] === '_'))) arg = arg.slice(1);
args = (args || matchArgs(fn))
.map(function(arg) {
var optional,showError,res;
if (optional = (arg[0] === '_')) arg = arg.slice(1);
if (showError = (arg[0] === '_')) arg = arg.slice(1);
if (arg[0] === '$' && logic[arg] === undefined) {
if (arg === '$caller')
res = clues.Promise.resolve(caller);
else if (arg === '$fullref')
res = clues.Promise.resolve(fullref);
else if (arg === '$global')
res = clues.Promise.resolve($global);
if (arg[0] === '$' && logic[arg] === undefined) {
if (arg === '$caller') return caller;
else if (arg === '$fullref') return fullref;
else if (arg === '$global') return $global;
else if (arg === '$private') {
fn.private = true;
return true;
}
else if (arg === '$prep') {
fn.prep = true;
return true;
}
}
return res || clues.Promise.resolve()
.then(function() {
dependencies = !clues.ignoreRecursive && (dependencies || []).concat(value);
return clues(logic,arg,$global,ref || 'fn',fullref,dependencies);
})
.then(null,function(e) {
if (optional) return (showError) ? e : undefined;
else throw e;
let processError = e => {
if (optional) return (showError) ? e : undefined;
let rejection = reject(e);
if (!errorArgs) errorArgs = rejection;
return rejection;
};
try {
res = promiseHelper(_rawClues(logic,arg,$global,ref || 'fn',fullref + '('), d => d, processError);
}
catch (e) {
res = processError(e);
}
if (!argsHasPromise && isPromise(res)) argsHasPromise = true;
return res;
});
var inputs = errorArgs ? errorArgs : (argsHasPromise ? clues.Promise.all(args) : args),
wait = Date.now(),
duration,
hasHandledError = false;
let solveFn = args => {
duration = Date.now();
let result = null;
try {
result = fn.apply(logic || {}, args);
}
catch (e) {
// If fn is a class we solve for the constructor variables (if defined) and return a new instance
if (e instanceof TypeError && /^Class constructor/.exec(e.message)) {
let constructorArgs = (/constructor\s*\((.*?)\)/.exec(fn.toString()) || [])[1];
constructorArgs = constructorArgs ? constructorArgs.split(',').map(d => d.trim()) : [];
constructorArgs.push(function() {
let newObj = new (Function.prototype.bind.apply(fn,[null].concat(Array.prototype.slice.call(arguments))));
return newObj;
});
});
var inputs = clues.Promise.all(args),
wait = new Date(),
duration;
result = _rawClues(logic,constructorArgs,$global,ref || 'fn',fullref);
}
else {
throw e;
}
}
var value = inputs
.then(function(args) {
duration = new Date();
return fn.apply(logic || {}, args);
})
.then(function(d) {
if (typeof $global.$duration === 'function')
$global.$duration(fullref,[(new Date()-duration),(new Date())-wait]);
return (typeof d == 'string' || typeof d == 'number') ? d : clues(logic,d,$global,caller,fullref,dependencies);
},function(e) {
if (typeof e !== 'object')
e = { message : e};
e.error = true;
e.ref = e.ref || ref;
e.fullref = e.fullref || fullref;
e.caller = e.caller || caller || '';
throw e;
});
if (isPromise(result) && !result.isFulfilled) result = clues.Promise.resolve(result); // wrap non-bluebird promise
return result;
};
if (fn.name == 'private')
let handleError = e => {
if (hasHandledError) return reject(e);
hasHandledError = true;
let wrappedEx = createEx(e || {}, fullref, caller, ref, value, true);
if (e && e.stack && typeof $global.$logError === 'function') $global.$logError(wrappedEx, fullref);
return storeRef(logic, ref, reject(wrappedEx), fullref, caller);
};
let captureTime = () => {
if (typeof $global.$duration === 'function')
$global.$duration(fullref || ref || (fn && fn.name),[(Date.now()-duration),(Date.now())-wait],ref);
};
var value = null;
if (errorArgs) {
args.forEach(input => input && input.suppressUnhandledRejections && input.suppressUnhandledRejections());
value = handleError(errorArgs.reason());
}
else if (isPromise(inputs)) {
value = inputs.then(d => solveFn(d)).catch(e => {
return handleError(e);
}).finally(captureTime);
}
else {
try { value = solveFn(inputs); }
catch (e) { value = handleError(e); }
}
value = promiseHelper(value, noop, handleError, captureTime);
value = promiseHelper(value,
d => (typeof d == 'string' || typeof d == 'number') ? d : _rawClues(logic,d,$global,caller,fullref),
e => reject(e, fullref, caller, ref)
);
if (fn.name == 'private' || fn.name == '$private' || fn.private) {
if (!isPromise(value)) value = clues.Promise.resolve(value);
value.private = true;
}
if (ref)
logic[ref] = value;
return value;
return storeRef(logic, ref, value, fullref, caller);
}
})(this);
})(this);
{
"name": "clues",
"version": "4.0.0-rc2",
"version": "4.0.0",
"description": "Lightweight logic tree solver using promises.",

@@ -17,11 +17,11 @@ "keywords": [

"dependencies": {
"bluebird": "~3.1.1"
"bluebird": "~3.5.0"
},
"devDependencies": {
"mocha": "~2.3.4"
"tap": "~10.3.2"
},
"license": "MIT",
"scripts": {
"test": "./node_modules/mocha/bin/mocha"
"test": "tap test --jobs=10 -Rspec --coverage-report=html --no-browser"
}
}
[![NPM Version][npm-image]][npm-url]
[![NPM Downloads][downloads-image]][downloads-url]
[![Test Coverage][circle-image]][circle-url]
[![Coverage][coverage-image]][coverage-url]
**clues.js** is a lean-mean-promisified-getter-machine that crunches through any javascript objects, including complex trees, functions, values and promises. Clues consists of a single getter function (just over 100 loc) that dynamically resolves dependency trees and memoizes resolutions (lets call them derived facts) along the way.
**clues.js** is a lean-mean-promisified-getter-machine that crunches through nested javascript objects, resolving functions (including ES6 arrow functions), values and promises. Clues consists of a single getter function (~300 loc) that dynamically resolves dependency trees and memoizes resolutions (derived facts) along the way. It handles the ordering of execution and allows you to think more about the logic of determining the result of a calculation, whether the inputs to those calculations are known values, functions, or asynchronous calls.
*[Prior versions](https://github.com/ZJONSSON/clues/tree/v2) of `clues` were based on internal scaffolding holding separate logic and fact spaces within a `clues` object. Clues 3.x is a major rewrite into a simple superpowered getter function. Clues apis might be backwards compatible - as long as you merge logic and facts into a single facts/logic object and use the new getter function directly for any resolutions. Other libraries exploring similar concepts include [curran/reactive-model](https://github.com/curran/reactive-model) and [ZJONSSON/instinct](https://github.com/ZJONSSON/instinct.js)*
[Intro presentation](http://zjonsson.github.io/clues)
#### Changes in version 4.x
* Upgrade Bluebird 3.x affects cancellation mechanics
Example:
```js
var obj = {
miles : 220,
hours : Promise.delay(100).then(d => 2.3),
minutes : hours => hours * 60,
mph : (miles,hours) => miles / hours,
car : {
model: 'Tesla'
}
};
// get mph directly (second argument references what we want to solve for)
clues(obj,'mph')
.then(console.log);
// get multiple properties through a function as second argument
// - the fn is only executed when all the arguments have been resolved
clues(obj,function(minutes,mph,carᐅmodel) {
console.log(`Drove for ${minutes} at ${mph} miles/hour in a ${carᐅmodel}`);
});
```
Clues recursively solves for properties of nested object. Whenever `clues` hits a property that is an unresolved function it will parse the argument names (if any) and attempt to resolve the argument values (from properties within same scope that have the same name as each argument). Any property requested, either directly or indirectly, will be immediately morphed into a promise on its own resolution or the solved value (in the case where it was not asynchronous). If any requested unresolved function requires other properties as inputs, those required properties will also be replaced with promises on their resolution etc.. Once all dependencies of any given function have been resolved, the function will be evaluated and the corresponding promise resolved (or rejected) by the outcome.
#### Updates in 4.0
Clues 4.0 contains a number of significant changes:
* Internal value resolution only uses promises when it absolutely has to.
* If previously resolved or reject Bluebird promises are used, their values are introspected and used immediately rather than deferred. This eases a lot of pressure on `nextTick` promise resolution.
* As a result, `{ k: () => 5 }` will be turned into `{ k: 5 }` if you ask Clues to solve for `k`
* For performance reasons, throwing an object (e.g. `throw { message: 'ERROR_OCCURRED', details: 5 }`) will now only retain the keys `message` and `value`. `value` can contain any object with whatever details you want bubbled all the way up.
#### Function signature
The basic function signature is simple and **always** returns a promise:
#### `clues(obj,fn,[$global])`
##### logic/facts (first argument)
The first argument of the clues function should be the object containing the logic/facts requested (or a function that delivers this object). The logic/facts object is any Javascript object, containing any mix of the following properties (directly and/or through prototype chains):
The first argument of the clues function should be the object containing the logic/facts requested (or a function/promise that delivers this object). The logic/facts object is any Javascript object, containing any mix of the following properties (directly and/or through prototype chains):
* Static values (strings, numbers, booleans, etc)
* Functions (i.e. logic) returning anything else in this list
* Functions (i.e. logic) returning anything else in this list (supports async/await)
* Promises returning anything else in this list

@@ -39,5 +73,21 @@ * Other javascript objects (child scopes)

clues(obj,['person',console.log]); // by array defined function
var person = await clues(obj,'person') // Using await inside async function
```
There are only a few restrictions and conventions you must into account when defining property names:
* Any property name starting with a [$](#-at-your-service) bypasses the main function cruncher (great for services)
* [`$property`](#property---lazily-create-children-by-missing-reference) and [`$external`](#external-property-for-undefined-paths) are special handlers for missing properties (if they are functions)
* Any function named in-line as `$property` or `$external` (or as only argument name of a function) will act as shorthands
* `$global` will always return the full global object provided, in any context.
* `$caller` and `$fullref` are reserved to provide access to the current state of the clues solver when it hits a function for the first time.
* Property names really should never start with an underscore (see [optional variables](#making-arguments-optional-with-the-underscore-prefix))
* Any [array whose last element is a function](#using-special-arrays-to-define-functions) will be evaluated as a function... Angular style
* Any function explicitly named [`$private`](#making-anonymous-functions-private) (regardless of the property name) will not be accessible directly
* ES6 arrow functions will be resoleved as regular functions with same `this` context
That's pretty much it.
##### global (optional third argument)
The third argument is an optional [global object](#global-variables), whose properties are available from any scope. The global object itself is handled as a logic/facts object and will be traversed as required.
The third argument is an optional [global object](#global-variables), whose properties are available from any scope. The global object itself is handled as a logic/facts object and will be traversed as required. Examples of variables suited for global scope are user inputs and services.

@@ -54,40 +104,6 @@ `clues(obj,'person',{userid:'admin',res:res}).then(console.log);`

`clues(obj,'person',{userid:'admin',res:res},'__user__','top.child') `
### the mean function resolution machine
Whenever `clues` hits a property that is an unresolved function it will parse the argument names (if any) and attempt to resolve the argument values (from properties within same scope that have the same name as each argument). Any property requested, either directly or indirectly, will be immediately morphed into a promise on its own resolution. If any requested unresolved function requires other properties as inputs, those required properties will also be replaced with promises on their resolution etc.. Once all dependencies of any given function have been resolved, the function will be evaluated and the corresponding promise resolved (or rejected) by the outcome.
```js
var obj = {
miles : 220,
hours : Promise.fulfilled(2.3), // we can also define as promise
minutes : function(hours) {
return hours * 60;
},
mph : function(miles,hours) {
return miles / hours;
}
};
// get mph directly
clues(obj,'mph').then(console.log);
// get multiple properties through a function call
clues(obj,function(minutes,mph) {
console.log('Drove for '+minutes+' min at '+mph+' mph');
});
```
There are only a few restrictions and conventions you must into account when defining property names.
* Any property name starting with a [$](#-at-your-service) bypasses the main function cruncher (great for services)
* [`$property`](#property---lazily-create-children-by-missing-reference) and [`$external`](#external-property-for-undefined-paths) are special handlers for missing properties (if they are functions)
* Any function named in-line as `$property` or `$external` will act as shorthands
* `$global` will always return the full global object provided, in any context.
* `$caller` and `$fullref` are reserved to provide access to the current state of the clues solver when it hits a function for the first time.
* Property names really should never start with an underscore (see [optional variables](#making-arguments-optional-with-the-underscore-prefix))
* Any [array whose last element is a function](#using-special-arrays-to-define-functions) will be evaluated as a function... Angular style
* Any function explicitly named [`private`](#making-anonymous-functions-private) (regardless of the property name) will not be accessible directly
That's pretty much it.
### reusing logic through prototypes
Since `clues` modifies any functional property from being a reference to the function to a reference to the promise of their outcome, a logic/facts object lazily transforms from containing mostly logic functions to containing resolved set of facts (in the form of resolved promises).
Since `clues` modifies any object property referencing a function to the promise of the outcome, a logic/facts object lazily transforms from containing mostly logic functions to containing resolved set of facts (possibly in the form of resolved promises).

@@ -110,5 +126,3 @@ An entirely fresh logic/facts object is required for a different context (i.e. different set known initial facts). A common pattern is to define all logic in a static object (as the prototype) and then only provide instances of this logic/facts object to the `clues` function, each time a new context is required. This way, the original logic functions themselves are never "overwritten", as the references in the clones switch from pointing to a function in the prototype to a promise on its resolution as each property is lazily resolved on demand.

},
mph : function(miles,hours) {
return miles / hours;
}
mph : (miles,hours) => miles / hours
};

@@ -138,3 +152,3 @@

### handling rejection
Errors (i.e. rejected promises in `clues`) will include a `ref` property showing which logic function (by property name) is raising the error. If the thrown error is not an object (i.e. a string), the resulting error will be a (generic) object with `message` showing the thrown message and `ref` the logic function. If the erroring function was called by a named logic function, the name of that function will show up in the `caller` property of the response. The rejection object will also contain `fullref` a property that shows the full path of traversal (through arguments and dots) to the function that raised the error. The rejection handling by `clues` will not force errors into formal Error Objects, which can be useful to distinguish between javascript errors (which are Error Object with `.stack`) and customer 'string' error messages (which may not have `.stack`).
Errors (i.e. rejected promises in `clues`) will include a `ref` property showing which logic function (by property name) is raising the error. If the thrown error is not an object (i.e. a string), the resulting error will be a (generic) object with `message` showing the thrown message and `ref`, the name of the logic function. If the erroring function was called by a named logic function, the name of that function will show up in the `caller` property of the response. The rejection object will also contain `fullref`, a property that shows the full path of traversal (through arguments and dots) to the function that raised the error. The rejection handling by `clues` will not force errors into formal Error Objects, which can be useful to distinguish between javascript errors (which are Error Objects with a defined `.stack` property) and customer 'string' error messages (which may not have `.stack`). You can pass more information on the error by rejecting with an object with both `.message` and `.value` in it - `.value` can contain an object with whatever information you like in it.

@@ -149,4 +163,9 @@ Example: passing thrown string errors to the client while masking javascript error messages

You can provide a function called `$logError` in the `$global` object to record any true javascript errors (with `.stack`) when they occur. The `$logError` function will be called with the error object as first argument and `fullref` as the second argument. For proper logging it is important to capture the error at source, as the dependencies might be optional - in which case the error never reaches the original request.
### making arguments optional with the underscore prefix
If any argument to a function resolves as a rejected promise (i.e. errored) then the function will not run and simply be rejected as well. But sometimes we want to continue nevertheless. Any argument to any function can be made optional by prefixing the argument name with an underscore. If the resolution of the optional argument returns a rejected promise (or the optional argument does not exist), then the value of this argument to the function will simply be `undefined`.
If any argument to a function resolves as a rejected promise (i.e. errored) then the function will not run and will also be rejected. But sometimes we want to continue nevertheless (example: user input that is optional). Any argument to any function can be made optional by prefixing the argument name with an underscore. If the resolution of the optional argument returns a rejected promise (or the optional argument does not exist), then the value of this argument to the function will simply be `undefined`.
In the following example we include a property `badCall` that returns a rejection. Adding underscores to any reference to `badCall` will make the dependency optional:
```js

@@ -175,3 +194,3 @@ var obj = {

### using special arrays to define functions
Functions can be defined in array form, where the function itself is placed in the last element, with the prior elements representing the full argument names required for the function to run. This allows the definition of more complex arguments (including argument names with dots in them) and also allows for code minification (angular style)
Users of Angular might be familiar with the array definition of function dependencies. Clues accepts a similar construct with functions defined in array form, where the function itself is placed in the last element and prior elements represent the full argument names required for the function to run. This allows the definition of more complex arguments (including argument names with dots in them) and also allows for code minification (angular style)

@@ -198,3 +217,3 @@ In the following example, the local variable `a` stands for the `input1` fact and `b` is the `input2` fact

},
fourthItem : ['drawer.items.3',String]
fourthItem : ['drawer.items.3',String] // `String` is a native js function that converts the first argument to string
};

@@ -219,6 +238,6 @@

```
It is worth noting that children do not inherit anything from parents. If you really want your children to listen to their parents (or their cousins) you have to get creative, passing variables down explicitly or providing a root reference in the globals (see [appendix](#moar-stuff-a-listening-to-your-parents))
It is worth noting that children do not inherit anything from parents. If you really want your children to listen to their parents (or their cousins) you have to get creative, passing variables down explicitly or providing a root reference in the globals (see [appendix](#moar-stuff-a-listening-to-your-parents)).
##### ᐅ as an alias for a dot
Clues provides an alias for dots (ᐅ - unicode U+07CD) in nested paths. Using this alias, nested arguments can be defined directly in the function signature. The downside to this approach is that argument names can become more cumbersome.
### ᐅ as an alias for a dot
Clues provides an alias for dots (ᐅ - unicode U+1405) in nested paths. Using this alias, nested arguments can be defined directly in the function signature. The downside to this approach is that argument names can become more cumbersome.

@@ -259,4 +278,6 @@ Here is the second example using the alias

### global variables
The third parameter to the `clues` function is an optional global object, whose properties are accessible n any scope (as a fallback). This makes it particularly easy to provide services or general inputs (from the user) without having to 'drag those properties manually' through the tree to ensure they exist in each scope where they are needed.
The third parameter to the `clues` function is an optional global object, whose properties are accessible in any scope (as a fallback if a property name is not found in current object). This makes it particularly easy to provide services or general inputs (from the user) without having to 'drag those properties manually' through the tree to ensure they exist in each scope where they are needed.
The following example requires `input` object to be defined in `$global`:
```js

@@ -309,5 +330,41 @@ var Logic = {

It is worth noting that this functionality only applies to functions. If an object has a $ prefix, then any functions inside that object will be crunched by `clues` as usual.
It is worth noting that this functionality only applies to functions. If an object has a $ prefix, then any functions inside that object will be crunched by `clues` as usual.
If you would like to have a service method that is partially fed by the rest of the `facts`, you can name your function `$prep`. If you want to make a `$prep` for your service, you must eventually return a `$service` method. For example:
```js
var Cabinet = {
something: () => 5,
complicated: () => 6,
$adder: function $prep(something, complicated) {
let work = something + complicated;
return function $service(number) {
return work * number;
}
}
};
```
You can also define a function as a `$prep` function by including an argument name `$prep`, which in a class definition would be something like this:
```js
class Cabinet {
something() {
return 5;
}
complicated() {
return 6
}
$adder ($prep, something, complicated) {
let work = something + complicated;
return function $service(number) {
return work * number;
}
}
}
```
### $property - lazily create children by missing reference

@@ -379,2 +436,9 @@ If a particular property can not be found in a given object, clues will try to locate a `$property` function. If that function exists, it is executed with the missing property name as the first argument and the missing value is set to be the function outcome.

Another way to create this shortcut is to have a function where only argument name is `$property`
```js
var fib = $property => ($property <= 1) ? +$property : [''+($property-1),''+($property-2), (a,b) => a+b];
```
### $external property for undefined paths

@@ -418,8 +482,20 @@ If an undefined property can not locate a `$property` function it will look for an `$external` function. The purpose of the `$external` function is similar except that the argument passed to the function will be the full remaining reference (in dot notation), not just the next reference in the chain.

return function $external(ref) {
ref = ref.replace(/\.h,'/');
....
}
}
return request.getAsync({
url : 'http://api.vendor.com/api/'+ref.replace(/\./g,'/'),
json : {user_id : userid}
});
....
```
A function with a sole argument of `$external` will behave the same:
```js
...
externalApi : userid => $external => request.getAsync({
url: 'http://api.vendor.com/api/'+$external.replace(/\./g,'/').
json: {user_id: userid}
});
```
### function that returns a function that returns a...

@@ -431,12 +507,6 @@ If the resolved value of any function is itself a function, that returned function will also be resolved (within the same scope). This allows for very powerful 'gateways' that constrains the tree traversal only to segments that are relevant for a particular solutions.

tree_b : {....},
next_step: function(step) {
if (step === 'a')
return ['tree_a',Object]
else
return ['tree_b',Object];
}
}
next_step: step => (step === 'a') ? ['tree_a',Object] : ['tree_b',Object]
}
clues(Logic,'continue',{step:'a'})
clues(Logic,'next_step',{step:'a'})
```

@@ -460,3 +530,4 @@ ### private parts

```
Unauthorized users will get an error if they try to query any of the admin functions, while admins have unlimited access.
Unauthorized users will get an error if they try to query any of the admin functions, while admins have unlimited access.
#### using first element to define private scope

@@ -517,12 +588,14 @@ But what if we want to hide certain parts of the tree from direct traversal, but still be able to use those hidden parts for logic? Array defined functions can be used to form gateways from one tree into a subtree of another. If the first element of an array-defined function is an object (and not an array) or a function, that object provides a separate scope the function will be evaluated in. The function can therefore act as a selector from this private scope.

#### making anonymous functions private
An even easier way to declare functions as private is simply [naming](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name) them `private`. Any functions named `private` will not be accessible directly, only indirectly through a different function, as an input argument. Specifically the `caller` has to be a defined value and not equal to `__user__`). Here is a quick example:
An even easier way to declare functions as private is simply [naming](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name) them `$private` (or `private`). Any functions named `$private` will not be accessible directly, only indirectly through a different function, as an input argument. Specifically the `caller` has to be a defined value and not equal to `__user__`). Any function or class function that has an argument `$private` will also be defined as private. Here is a quick example:
```js
var facts = {
a : function private() { return 2; },
b : function(a) { return a+2; }
a : function $private() { return 2; },
b : function($private, a) { return a + 1; },
sum : function(a,b) { return a + b; }
};
clues(facts,'b').then(console.log) // prints 4
clues(facts,'sum').then(console.log) // prints 5
clues(facts,'a').catch(console.log) // prints error
clues(facts,'b').catch(console.log) // prints error
```

@@ -621,1 +694,3 @@

[downloads-url]: https://npmjs.org/package/clues
[coverage-image]: https://3tjjj5abqi.execute-api.us-east-1.amazonaws.com/prod/clues/badge
[coverage-url]: https://3tjjj5abqi.execute-api.us-east-1.amazonaws.com/prod/clues/url

@@ -1,12 +0,15 @@

var clues = require('../clues'),
assert = require('assert'),
Promise = require('bluebird');
const clues = require('../clues');
const BBPromise = require('bluebird');
const t = require('tap');
describe('Array functions',function() {
var logic = {
M1 : [function() { return Promise.delay(100,10); }],
M2 : function() { return Promise.delay(20,300); },
t.test('array functions', async t => {
const logic = {
M1 : [function() { return BBPromise.delay(100,10); }],
M2 : function() { return BBPromise.delay(20,300); },
M3 : ['M1','M2',function(a,b) { return a+b; }],
M4 : function(M3) { return M3;},
container: {
M5 : Promise.resolve([{value:1},{value:2}]),
},
test_M5 : container => [container, 'M5', M5 => M5.reduce((sum,value) => sum + value.value, 0)],
recursive : [['M1',Number],[['M2',Number],['M3',Number],Array],Array],

@@ -31,40 +34,32 @@ regular_array : [1,2,3,4],

var facts = Object.create(logic);
const facts = Object.create(logic);
it('should resolve to the top',function() {
return clues(facts,'M3')
.then(function(d) {
assert.equal(d,310);
});
});
t.same(await clues(facts,'M3'),310, 'should resolve to the top');
t.same(await clues(facts,'M4'),310, 'should work for individual functions');
t.same(await clues(facts,'nested'),15,'should work for nested structures');
t.same(await clues(facts,'regular_array'),logic.regular_array,'should not affect regular array');
t.same(await clues(facts,'partial'),42,'partial positional arguments ok');
t.same(await clues(facts,'recursive'),[10,[300,310]],'should work recursively');
t.same(await clues(facts,'test_M5'),3,'resolved promises in arrays are solved okay');
t.test('should only execute arrays once', async function(t) {
let counter = 0;
const otherContext = {
M1: BBPromise.delay(100, 10),
M2: BBPromise.delay(200, 20)
};
it('should work for individual functions',function() {
return clues(facts,['M4',function(a) {
assert.equal(a,310);
}]);
});
const facts = {
M3: [otherContext, 'M1', 'M2', function(a,b) {
counter++;
return a + b;
}]
};
it('should work for nested structures',function() {
return clues(facts,['nested',function(d) {
assert.equal(d,15);
}]);
const d = await BBPromise.all([clues(facts,'M3'), clues(facts,'M3')]);
t.same(d[0],30,'results ok');
t.same(d[1],30,'results ok');
t.same(counter,1,'fn only executed once');
});
it('should not affect regular arrays',function() {
return clues(facts,function(regular_array) {
assert.equal(regular_array,logic.regular_array);
});
});
it('should work with partial positional arguments',function() {
return clues(facts, 'partial').then(function(r){
assert.equal(r, 42);
});
});
it('should work recursively',function() {
return clues(facts,'recursive').then(function(d) {
assert.deepEqual(d,[10,[300,310]]);
});
});
});
});

@@ -1,19 +0,16 @@

var clues = require('../clues'),
assert = require('assert'),
crypto = require('crypto');
const clues = require('../clues');
const crypto = require('crypto');
const t = require('tap');
function shouldError() { throw 'Should not run';}
describe('Array fn private scope',function() {
t.test('Array fn private scope', {autoend:true}, t => {
describe('With only array logic',function() {
t.test('With only array logic', async t => {
function privateObj() {
return {
answer : function(forty,two) {
return forty+two;
},
answer : (forty,two) => forty + two,
forty : 40,
two : 2,
err : function() {
err : () => {
throw 'This is an error';

@@ -24,3 +21,3 @@ }

var pub = {
const pub = {
M1 : 100,

@@ -30,44 +27,23 @@ M2 : 200

it('works without array arguments',function() {
var obj = privateObj();
return clues(null,[obj,function(answer) {
assert.equal(answer,42);
}]);
});
const obj = privateObj();
it('works with array arguments',function() {
var obj = privateObj();
return clues({},[obj,'answer',function(d) {
assert.equal(d,42);
}]);
});
t.same(await clues(null,[obj,'answer',Number]),42,'works without array arguments');
t.same(await clues(null,[privateObj,'answer',Number]),42,'works with private scope');
it('works with private scope defined from function',function() {
return clues({},[privateObj,function(answer) {
assert.equal(answer,42);
}]);
});
let d1 = await clues({},[privateObj,'err',String])
.then(shouldError,e => e);
it('handles errors correctly',function() {
return clues({},[privateObj,'err',String])
.then(function() { console.log(arguments);throw 'Should Error';},function(e) {
assert.equal(e.ref,'err');
assert.equal(e.fullref,'err');
assert.equal(e.message,'This is an error');
});
});
t.same(d1.message,'This is an error','handles errors correctly');
it('works recursively',function() {
return clues({},[Object.create(pub),[['M1',Number],['M2',Number],[privateObj(),'forty',Number],Array],function(d) {
assert.deepEqual(d,[100,200,40]);
}]);
});
let d2 = await clues(null,[Object.create(pub),[['M1',Number],['M2',Number],[privateObj(),'forty',Number],Array],Object]);
t.same(d2,[100,200,40],'works recursively');
});
describe('public/private structure',function() {
t.test('public/private structure', async t => {
var PrivateLogic = {
const PrivateLogic = {
secret : 'Hidden secret',
hash : function(secret,userid) {
hash : (secret,userid) => {
return crypto.createHash('sha1')

@@ -79,11 +55,8 @@ .update(secret)

public : function(hash,_userid) {
return {
hash : hash,
userid : _userid,
};
public : (hash,_userid) => {
return {hash, userid : _userid};
}
};
var Logic = {
const Logic = {
info : function(_userid) {

@@ -98,20 +71,21 @@ return [

it('provides custom access to private segments',function() {
var obj = Object.create(Logic,{userid: {value:'admin'}});
t.test('custom access to private segments',t => {
const obj = Object.create(Logic,{userid: {value:'admin'}});
return clues(obj,function(info) {
assert.equal(info.userid,'admin');
assert.equal(info.hash,'b21b8fd516dbb99584d651f040d970dba2245b2a');
assert.equal(obj.secret,undefined);
assert.equal(info.secret,undefined);
t.same(info.userid,'admin','username matches');
t.same(info.hash,'b21b8fd516dbb99584d651f040d970dba2245b2a','hash matches');
t.same(obj.secret,undefined,'no obj.secret');
t.same(info.secret,undefined,'no info.secret');
});
});
it('handles errors correctly',function() {
var obj = Object.create(Logic);
t.test('handles errors correctly',t => {
const obj = Object.create(Logic);
return clues(obj,'info')
.then(shouldError,function(e) {
assert.equal(e.ref,'userid');
assert.equal(e.message,'userid not defined');
assert.equal(e.caller,'hash');
assert.equal(e.fullref,'info^public^hash^userid');
t.same(e.error,true,'errors');
t.same(e.ref,'userid','userid as ref');
t.same(e.message,'userid not defined','right error message');
t.same(e.caller,'hash','caller is hash');
t.same(e.fullref,'info(public(hash(userid','fullref');
});

@@ -118,0 +92,0 @@ });

@@ -1,86 +0,41 @@

var clues = require('../clues'),
assert = require('assert');
const clues = require('../clues');
const t = require('tap');
function obj() {
return {
a : {
b : {
caller : function($caller) { return $caller; },
fullref : function($fullref) { return $fullref; }
}
},
call : ['a.b.caller',String],
fullref : ['a.b.fullref',String]
};
}
const facts = () => ({
a : {
b : {
caller : function($caller) { return $caller; },
fullref : function($fullref) { return $fullref; }
}
},
call : ['a.b.caller',String],
fullref : ['a.b.fullref',String]
});
describe('$caller',function() {
describe('with fn called directly',function() {
it('is null without provided caller',function() {
return clues(obj,'a.b.caller')
.then(function(caller) {
assert.equal(caller,undefined);
});
});
it('shows provided caller',function() {
return clues(obj,'a.b.caller',{},'__user__')
.then(function(caller) {
assert.equal(caller,'__user__');
});
});
t.test('$caller', {autoend:true}, t => {
t.test('with fn called directly', async t => {
t.same(await clues(facts,'a.b.caller'),undefined,'is null without caller');
t.same(await clues(facts,'a.b.caller',{},'__user__'),'__user__','shows with caller');
});
});
describe('with fn called indirectly',function() {
it('show last fn without provided caller',function() {
return clues(obj,'call')
.then(function(caller) {
assert.equal(caller,'call');
});
});
t.test('with fn called indirectly', async t => {
t.same(await clues(facts,'call'),'call','shows last fn without provided caller');
t.same(await clues(facts,'call',{},'__user__'),'call','shows last fn even with caller');
});
it('shows last fn even with provded caller',function() {
return clues(obj,'call',{},'__user__')
.then(function(caller) {
assert.equal(caller,'call');
});
});
});
t.test('with $caller override in factsect', async t => {
const o = facts();
o.a.b.$caller = 'CUSTOM_CALLER';
t.same(await clues(o,'call',{},'__user__'),'CUSTOM_CALLER','returns override');
});
describe('with $caller override in object',function() {
it('returns override',function() {
var o = obj();
o.a.b.$caller = 'CUSTOM_CALLER';
return clues(o,'call',{},'__user__')
.then(function(caller) {
assert.equal(caller,'CUSTOM_CALLER');
});
});
});
t.test('$fullref', async t => {
t.same(await clues(facts,'a.b.fullref'),'a.b.fullref','direct call shows fullref');
t.same(await clues(facts,'fullref'),'fullref(a.b.fullref','indirect call shows fullref');
const o = facts();
o.a.b.$fullref = 'CUSTOM_FULLREF';
t.same(await clues(o,'fullref',{}),'CUSTOM_FULLREF','$fullref override returns override');
});
describe('$fullref',function() {
it('direct call shows fullref',function() {
return clues(obj,'a.b.fullref')
.then(function(fullref) {
assert.equal(fullref,'a.b^fullref');
});
});
it('indirect call shows fullref',function() {
return clues(obj,'fullref')
.then(function(fullref) {
assert.equal(fullref,'fullref.a.b^fullref');
});
});
it('with $fullref override in object returns override',function() {
var o = obj();
o.a.b.$fullref = 'CUSTOM_FULLREF';
return clues(o,'fullref',{})
.then(function(fullref) {
assert.equal(fullref,'CUSTOM_FULLREF');
});
});
});

@@ -1,8 +0,8 @@

var clues = require('../clues'),
assert = require('assert'),
Promise = require('bluebird');
const clues = require('../clues');
const Promise = require('bluebird');
const t = require('tap');
describe('complex tree',function() {
t.test('Arrow functions',async t => {
var logic = {
const Logic = {
M1 : function() { return Promise.delay(100,10); },

@@ -18,18 +18,11 @@ M2 : function() { return Promise.delay(20,300); },

var facts = Object.create(logic);
const facts = Object.create(Logic);
const MTOP = await clues(facts,'MTOP');
it('should resolve to the top',function() {
return clues(facts,'MTOP')
.then(function(d) {
assert.equal(d,380);
});
});
it('should update the fact table',function() {
assert.equal(facts.M1.value(),10);
assert.equal(facts.M2.value(),300);
assert.equal(facts.M3.value(),310);
assert.equal(facts.M4.value(),70);
assert.equal(facts.MTOP.value(),380);
});
t.same(MTOP,380,'resolves tree to the top');
t.same(facts.M1.value(),10,'fact M1 resolved');
t.same(facts.M2.value(),300,'fact M2 resolved');
t.same(facts.M3.value(),310,'fact M3 resolved');
t.same(facts.M4.value(),70,'fact M4 resolved');
t.same(facts.MTOP.value(),380,'fact MTOP resolved');
});

@@ -1,75 +0,129 @@

var clues = require('../clues'),
assert = require('assert');
const clues = require('../clues');
const t = require('tap');
function shouldErr() { throw 'Should throw an error'; }
describe('error',function() {
var c = {a: function(b) { return b;}};
t.test('error', {autoend: true},t => {
const c = {a: function(b) { return b;}};
describe('when argument can´t be found',function() {
it('should throw an error',function() {
return clues({},'SOMETHING')
.then(shouldErr,function(e) {
assert.equal(e.ref,'SOMETHING');
assert.equal(e.message, 'SOMETHING not defined');
});
t.test('when argument is not found', async t => {
t.test('direct call',async t => {
const e = await clues({},'SOMETHING').then(shouldErr,Object);
//t.equal(e.error,true,'shows errors');
t.same(e.message,'SOMETHING not defined','message: not defined');
t.same(e.ref,'SOMETHING','ref: missing variable');
});
it('should show caller if a named logic function',function() {
return clues(c,'a')
.then(shouldErr,function(e) {
assert.equal(e.ref,'b');
assert.equal(e.message,'b not defined');
assert.equal(e.caller,'a');
});
t.test('caller is logic function', async t => {
const e = await clues(c,'a').catch(Object);
t.same(e.error,true,'errors');
t.same(e.ref,'b','ref is correct');
t.same(e.message,'b not defined','message is correct');
t.same(e.caller,'a','caller is correct');
});
});
describe('thrown',function() {
var logic = {
ERR : function() { throw 'Could not process'; },
DEP : function(ERR) { return 'Where is the error'; }
const Logic = {
ERR : function() { throw 'Could not process'; },
DEP : function(ERR) { return 'Where is the error'; },
OBJ : function() { throw {
message: 'somemessage',
value: 'somedetails'
}; }
};
t.test('throw', {autoend:true}, t => {
t.test('directly', async t => {
const facts = Object.create(Logic);
const e = await clues(facts,'ERR').catch(Object);
const reason = facts.ERR.reason();
t.equal(e.message,'Could not process','message ok');
t.same(e.ref,'ERR','ref ok');
t.same(reason.message,'Could not process','facts register message');
});
t.test('indirectly - dependent fn', async t => {
const facts = Object.create(Logic);
const e = await clues(facts,'DEP').catch(Object);
t.equal(e.message,'Could not process','message ok');
t.same(e.ref,'ERR','ref ok');
t.same(e.caller,'DEP','Should reference the first caller');
});
t.test('obj - directly', async t => {
const facts = Object.create(Logic);
const e = await clues(facts,'OBJ').catch(Object);
t.equal(e.message,'somemessage','message ok');
t.equal(e.value,'somedetails','details ok');
});
});
t.test('$logError', {autoend: true}, t => {
const Global = {
$logError : function(e,f) {
this.error = e;
this.fullref = f;
}
};
var facts = Object.create(logic);
describe('directly',function() {
it('should show up as first argument (err)',function() {
return clues(facts,'ERR')
.then(shouldErr,function(e) {
assert.equal(e.ref,'ERR');
assert.equal(e.message,'Could not process');
});
});
t.test('error with a stack', async t => {
const $global = Object.create(Global);
const facts = {
stack_error: function() { throw new Error('error');}
};
it ('should update the facts',function() {
var e = facts.ERR.reason();
assert.equal(e.ref,'ERR');
assert.equal(e.message,'Could not process');
});
await clues(facts,'stack_error',$global).then(shouldErr,Object);
t.same($global.error.message,'error');
t.ok($global.error.stack,'contains a stack');
t.same($global.error.fullref,'stack_error','fullref ok');
});
describe('indirectly',function() {
it('should throw same error for dependent logic',function() {
return clues(facts,'DEP')
.then(shouldErr,function(e) {
assert.equal(e.ref,'ERR');
assert.equal(e.message,'Could not process');
});
});
t.test('promise rejected with a stack', async t => {
const $global = Object.create(Global);
const facts = {
stack_error_promise: () => clues.reject(new Error('error'))
};
it('should contain reference to the first caller',function() {
facts = Object.create(logic);
return clues(facts,'DEP')
.catch(function() {
return clues(facts,'ERR');
})
.then(shouldErr,function(e) {
assert.equal(e.caller,'DEP');
});
});
await clues(facts,'stack_error_promise',$global).then(shouldErr,Object);
t.same($global.error.message,'error','error passed to $logError');
t.ok($global.error.stack,'contains a stack');
t.same($global.error.fullref,'stack_error_promise','fullref ok');
});
t.test('error without a stack (rejection)', async t => {
const $global = Object.create(Global);
const facts = {
rejection: function() { throw 'error';}
};
await clues(facts,'rejection',$global).then(shouldErr,Object);
t.same($global.error,undefined,'should not $logError');
});
t.test('Promise rejection without a stack', async t => {
const $global = Object.create(Global);
const facts = {
rejection_promise: function() { return clues.reject('error');}
};
await clues(facts,'rejection_promise',$global).then(shouldErr,Object);
t.same($global.error,undefined,'should not $logError');
});
t.test('stack error in an optional dependency', async t => {
const $global = Object.create(Global);
const facts = {
stack_error: () => { throw new Error('error');},
rejection: () => { throw 'error';},
optional: (_stack_error,_rejection) => 'OK'
};
await clues(facts,'optional',$global);
t.same($global.error.message,'error','error passed to $logError');
t.ok($global.error.stack,'contains a stack');
t.same($global.error.fullref,'optional(stack_error','fullref ok');
});
});
});
});

@@ -1,107 +0,146 @@

var clues = require('../clues'),
assert = require('assert');
const clues = require('../clues');
const Promise = require('bluebird');
const t = require('tap');
describe('$external',function() {
var logic = {
a : 5,
simple : Object.create({
count : 0,
$external : function(ref) {
this.count+=1;
if (ref == 'STOP') throw {message:'STOP_ERROR',error:true};
return 'simple:'+ref;
t.test('$external', {autoend:true, jobs: 10}, t => {
t.test('in simple logic with $external', {autoend: true}, t => {
const facts = {
simple: {
count : 0,
$external : function(ref) {
this.count+=1;
if (ref == 'STOP') throw {message:'STOP_ERROR',error:true};
return 'simple:'+ref;
}
}
}),
shorthand : function $external(ref) {
return 'answer:'+ref;
},
shorthandThis : function $external() {
return this;
},
concurrent : function $external() {
return clues.Promise.delay(Math.random()*1000)
.then(function() {
return new Date();
});
}
};
};
var facts = Object.create(logic);
t.test('asking for non-existent property', async t => {
const d = await clues(facts,'simple.test.1');
t.same(d,'simple:test.1','returns the $external function value');
t.ok(facts.simple['test.1'],'resolved promise is placed on logic');
t.same(facts.simple['test.1'],'simple:test.1','promise contains value');
t.same(facts.simple.count,1,'function is called once');
});
describe('in simple logic',function() {
it('runs $external function for non-existent property',function() {
return clues(facts,'simple.test')
.then(function(d) {
assert.equal(d,'simple:test');
assert(facts.simple.test.isFulfilled());
assert.equal(facts.simple.test.value(),'simple:test');
assert.equal(facts.simple.count,1);
});
t.test('asking for a different property',async t => {
t.same(await clues(facts,'simple.test.2'),'simple:test.2','returns new value');
t.same(facts.simple.count,2,'$external has now been called twice');
});
it('concurrent requests should return first response',function() {
var requests = Array.apply(null, {length: 10})
.map(function() {
return clues(facts,'concurrent.test_concurrent');
});
return clues.Promise.all(requests)
.then(function(d) {
return d.map(function(e) {
assert.equal(e,d[0],'Return time should be the same');
});
});
t.test('results so far are cached', async t => {
const d = await Promise.all([ clues(facts,'simple.test.1'), clues(facts,'simple.test.2')]);
t.same(d[0],'simple:test.1','first is cached');
t.same(d[1],'simple:test.2','second is cached');
t.equal(facts.simple.count,2,'an $external is not run again');
});
it('passes the full path as argument',function() {
return clues(facts,'simple.test.a.b.c')
.then(function(d) {
assert.equal(d,'simple:test.a.b.c');
assert.equal(facts.simple.count,2);
});
t.test('error raised in $external', async t => {
const e = await clues(facts,'simple.STOP',{},'__test__').catch(Object);
t.same(e.message,'STOP_ERROR','error message ok');
//t.same(e.ref,'abc','ref is ok');
t.same(e.fullref,'simple.STOP','fullref is ok');
t.same(e.caller,'STOP','caller is ok');
t.equal(facts.simple.STOP.isRejected(),true,'rejected promise in logic');
t.equal(facts.simple.STOP.reason().message,'STOP_ERROR','rejected promise has error');
t.equal(facts.simple.count,3,'$external has now been called 3 times');
});
});
it('caches previous results',function() {
return clues(facts,['simple.test','simple.test.a.b.c',function(a,b) {
assert.equal(a,'simple:test');
assert.equal(b,'simple:test.a.b.c');
assert.equal(facts.simple.count,2);
}]);
t.test('concurrent request', async t => {
const facts = {
concurrent : {
count: 0,
$external : function() {
return clues.Promise.delay(Math.random()*1000)
.then(() => {
this.count += 1;
return new Date();
});
}
}
};
const d = await Promise.map([...Array(10)],() => clues(facts,'concurrent.test.value'));
t.ok( d.every(e => e == d[0]),'should return the same value');
t.same(facts.concurrent.count,1,'and should not rerun the function');
});
t.test('$external inside a function', {autoend: true}, t => {
const facts = {
a : 'ok:',
funct : a => ({
count : 0,
$external : function(ref) {
this.count+=1;
return a+ref;
}
})
};
t.test('asking for non-existent property', async t => {
t.same( await clues(facts,'funct.test.1'), 'ok:test.1', 'returns value');
t.same(facts.funct.count,1,'$external is called once');
t.same( await clues(facts,'funct.test.1'), 'ok:test.1', 'previous results are cached');
t.same(facts.funct.count,1,'$an fn not run again for same property');
});
});
it('handles errors correctly',function() {
return clues(facts,'simple.STOP')
.then(function() {
throw 'Should Error';
},function(e) {
assert.equal(e.error,true);
assert.equal(e.message,'STOP_ERROR');
assert.equal(facts.simple.count,3);
assert.equal(e.fullref,'simple.STOP');
});
t.test('when function name is $external', {autoend: true}, t => {
const facts = {
a: 'ok:',
shorthand : function $external(ref) {
return this.a+ref;
}
};
t.test('asking for a property', async t => {
t.same( await clues(facts,'shorthand.test.1'),'ok:test.1','returns right value');
// TODO: value() shouldn't really be needed here... it should just be complete by the end
t.same( facts.shorthand['test.1'],'ok:test.1','stores promise with value');
});
t.test('asking for another property', async t => {
t.same( await clues(facts,'shorthand.test.2'),'ok:test.2','returns right value');
t.same( facts.shorthand['test.2'],'ok:test.2','stores promise with value');
});
});
describe('when function name is $external',function() {
it('acts as a shorthand for empty object with $external',function() {
return clues(facts,'shorthand.first')
.then(function(d) {
assert.equal(d,'answer:first');
assert.equal(facts.shorthand.value().first.value(),'answer:first');
return clues(facts,'shorthand.second');
})
.then(function(d) {
assert.equal(d,'answer:second');
assert.equal(facts.shorthand.value().first.value(),'answer:first');
assert.equal(facts.shorthand.value().second.value(),'answer:second');
});
t.test('when argument name is $external', {autoend: true}, t => {
const facts = {
a: 'ok:',
as_argument: function ($external) {
return this.a + $external;
},
as_argument_es6 : a => $external => a + $external
};
t.test('regular function', async t => {
t.test('asking for a property', async t => {
t.same( await clues(facts,'as_argument.test.1'),'ok:test.1','returns right value');
t.same( facts.as_argument['test.1'],'ok:test.1','stores promise with value');
});
t.test('asking for another property', async t => {
t.same( await clues(facts,'as_argument.test.2'),'ok:test.2','returns right value');
t.same( facts.as_argument['test.2'],'ok:test.2','stores promise with value');
});
});
it('assumes this of the parent',function() {
return clues(facts,'shorthandThis.test')
.then(function(d) {
assert.equal(d,facts);
});
t.test('ES6 fat arrow', async t => {
t.test('asking for a property', async t => {
t.same( await clues(facts,'as_argument_es6.test.1'),'ok:test.1','returns right value');
t.same( facts.as_argument_es6['test.1'],'ok:test.1','stores promise with value');
});
t.test('asking for another property', async t => {
t.same( await clues(facts,'as_argument_es6.test.2'),'ok:test.2','returns right value');
t.same( facts.as_argument_es6['test.2'],'ok:test.2','stores promise with value');
});
});
});
});

@@ -1,7 +0,7 @@

var clues = require('../clues'),
assert = require('assert'),
Promise = require('bluebird');
const clues = require('../clues');
const Promise = require('bluebird');
const t = require('tap');
describe('facts',function() {
var logic = {
t.test('facts',{autoend: true}, t => {
const Logic = {
response : function() { return Promise.delay(500,42);},

@@ -11,27 +11,22 @@ other : function() { return 5; }

var facts = Object.create(logic);
const facts = Object.create(Logic);
it('should return the solved logic when determined',function() {
var start = new Date();
return clues(facts,'response')
.then(function(d) {
var wait = (new Date()) - start;
assert.equal(wait >= 500,true,'wait was '+wait);
assert.equal(d,42);
});
t.test('called first time', async t => {
const start = new Date();
t.same(await clues(facts,'response'),42,'resolves function when called');
const wait = new Date() - start;
t.ok( wait >= 500,true, `wait was ${wait}`);
});
it('should return value immediately when solved for again',function() {
var start = new Date();
return clues(facts,'response')
.then(function(d) {
var wait = (new Date()) - start;
assert.equal(wait <= 20,true,'wait was '+wait);
assert.equal(d,42);
});
t.test('called second time', async t => {
const start = new Date();
t.same(await clues(facts,'response'),42,'resolves function when called');
const wait = new Date() - start;
t.ok( wait <= 100,true, 'should return immediately');
});
it('should not be solved for unrequested logic',function() {
assert(typeof facts.other === 'function');
t.test('unrequested logic function', {autoend: true}, t => {
t.same(typeof facts.other,'function','should not be resolved');
});
});

@@ -1,6 +0,8 @@

var clues = require('../clues'),
assert = require('assert');
const clues = require('../clues');
const t = require('tap');
describe('Global variable',function() {
var logic = {
const shouldErr = () => { throw 'Should error'; };
t.test('Global variable', {autoend: true}, t => {
const Logic = {
a: 123,

@@ -39,5 +41,5 @@ b: {

var facts = Object.create(logic);
const facts = Object.create(Logic);
var global = {
const global = {
$input : {

@@ -51,32 +53,19 @@ test: 10,

it('should be applied where referenced',function() {
return clues(facts,'b.c',global)
.then(function(d) {
assert.equal(d,133);
assert.equal(facts.b.count,1);
});
t.test('when referenced as a dependency of a function', async t => {
t.same(await clues(facts,'b.c',global), 133, 'uses the global variable');
t.same(facts.b.count,1,'fn called once');
});
it('can not be applied directly',function() {
return clues(facts,'$input',global)
.then(function() {
throw 'Should Error';
},function(e) {
assert.equal(e.message,'$input not defined');
});
t.test('when referenced directly', async t => {
const e = await clues(facts,'$input',global).then(shouldErr,Object);
t.same(e.message,'$input not defined','should error');
});
it('can not be applied as part of dot notation',function() {
return clues(facts,'d.c',global)
.then(function(d) {
assert.equal(d,29);
});
t.test('inside a nested structure', async t => {
t.same(await clues(facts,'d.c',global),29,'uses the global variable');
});
it('can be used as object $global',function() {
return clues(facts,'e.f.g',global)
.then(function(d) {
assert.equal(d,4);
});
t.test('$global object as dependency', async t => {
t.same(await clues(facts,'e.f.g',global), 4, 'uses the $global object');
});
});

@@ -1,5 +0,5 @@

var clues = require('../clues'),
assert = require('assert');
const clues = require('../clues');
const t = require('tap');
function logic($global) {
function facts($global) {
$global.$constant = 380;

@@ -9,22 +9,11 @@ return { a : { b : function($constant) { return $constant; } } };

describe('When logic is a function',function() {
it('it is evaluated before solving',function() {
return clues(logic,'a.b')
.then(function(d) {
assert.equal(d,380);
});
});
function wrapped() {
return function($global) {
return facts;
};
}
it('even nested functions are evaluated before solving',function() {
function wrapped() {
return function($global) {
return logic;
};
}
return clues(wrapped,'a.b')
.then(function(d) {
assert.equal(d,380);
});
});
t.test('when logic is a function', async t => {
t.same( await clues(facts,'a.b'),380,'direct call is evaluated before solving');
t.same( await clues(wrapped,'a.b'),380,'nested functions evaluated');
});

@@ -1,20 +0,14 @@

var clues = require('../clues'),
assert = require('assert'),
Promise = require('bluebird');
const clues = require('../clues');
const Promise = require('bluebird');
const t = require('tap');
var logic = Promise.delay(100).then(function() {
const Logic = Promise.delay(100).then(function() {
return {
$constant : 380,
a : function($constant) { return {b: $constant}; }
a : function($constant) { return {b: $constant}; }
};
});
describe('When logic is a promise',function() {
it('is used when resolved',function() {
return clues(logic,'a.b')
.then(function(d) {
assert.equal(d,380);
});
});
t.test('When logic is a promise', async t => {
t.same( await clues(Logic,'a.b'),380,'logic is resolved before solving');
});

@@ -1,54 +0,43 @@

var clues = require('../clues'),
assert = require('assert');
const clues = require('../clues');
const Promise = require('bluebird');
const t = require('tap');
function shouldError(e) { throw 'Should error '+e;}
function notDefined(e) { assert.deepEqual(e.message,'test not defined'); }
[null,undefined,4,'a',0,Object,Array,Object].forEach(function(nothing) {
describe('with value '+String(nothing),function() {
describe(' as logic',function() {
t.test('null logic', {autoend: true}, t => {
const values = [null,undefined,4,'a',0,Object,Array,Object];
it('property not defined',function() {
return clues(nothing,'test')
.then(shouldError,notDefined);
});
t.test('solving for a variable of null logic', async t => {
await Promise.map(values, async nothing => {
const value = await clues(nothing,'test').then(shouldError,Object);
t.same(value.message,'test not defined',`${nothing}.test is undefined`);
});
t.end();
});
it('path not defined',function() {
return clues(nothing,'test.test')
.then(shouldError,notDefined);
});
t.test('solving for a nested variable of null logic', async t => {
await Promise.map(values, async nothing => {
const value = await clues(nothing,'test.test').then(shouldError,Object);
t.same(value.message,'test not defined',`${nothing}.test.test is undefined`);
});
t.end();
});
it('context of empty function is empty object',function() {
return clues(nothing,function() {
if (nothing == 'a')
return assert.deepEqual(this,{0:'a'});
else
assert.deepEqual(this,{});
});
});
it('property fallback to global (if exists)',function() {
return clues(nothing,function(test) {
assert.equal(test,'global');
},{test:'global'});
});
t.test('solving for context of null logic', async t => {
await Promise.map(values, async nothing => {
const context = await clues(nothing,function() { return this;});
t.same(context, nothing == 'a' ? {0:'a'} : {},`${nothing} has a context of ${JSON.stringify(context)}`);
});
t.end();
});
describe('as a property',function() {
it('should resolve correctly',function() {
var obj = {};
obj.test = nothing;
return clues(obj,'test')
.then(function(d) {
// functions are always resolved to objects
return assert.deepEqual(d, typeof nothing === 'function' ? nothing() : nothing);
},function(e) {
// we only error if the property is `undefined`
assert.equal(nothing,undefined);
assert.equal(e.message,'test not defined');
});
});
t.test('null logic as a property', async t => {
await Promise.map(values.filter(d => d !== undefined), async nothing => {
const d = await clues({test: nothing},'test');
const expected = typeof nothing === 'function' ? nothing() : nothing;
t.same(d, expected,`test.${nothing} == ${expected}`);
});
t.end();
});
t.end();
});

@@ -1,8 +0,10 @@

var clues = require('../clues'),
assert = require('assert'),
Promise = require('bluebird');
const clues = require('../clues');
const Promise = require('bluebird');
const t = require('tap');
describe('optional argument',function() {
const shouldErr = () => { throw 'Should error'; };
var logic = {
t.test('optional argument', {autoend: true}, t => {
const Logic = {
data : function() { return Promise.delay(1000,5); },

@@ -14,40 +16,22 @@ passthrough : function(_optional) { return _optional; },

describe('not supplied',function() {
return it('should return undefined',function() {
clues(Object.create(logic),'passthrough')
.then(function(d) {
assert.deepEqual(d,undefined);
});
});
const facts = () => Object.create(Logic);
t.test('not supplied', async t => {
t.same(await clues(facts,'passthrough'),undefined,'returns undefined');
});
describe('with internal default',function() {
it('should use the internal default',function() {
return clues(Object.create(logic),'internalize')
.then(function(d) {
assert.equal(d,7);
});
});
t.test('internal default', async t => {
t.same(await clues(facts,'internalize'),7,'returns the default');
});
describe('with a set fact',function() {
it('should return the right value',function() {
return clues(Object.create(logic),'passthrough',{optional:10})
.then(function(d) {
assert.equal(d,10);
});
});
t.test('with a set global', async t => {
t.same(await clues(facts,'passthrough',{optional:10}),10,'returns global');
});
describe('with a working function',function() {
it('should return the function results',function() {
return clues(Object.create(logic),'optional_data')
.then(function(d) {
assert.equal(d,5);
});
});
t.test('with a working function', async t => {
t.same(await clues(facts,'optional_data'),5,'returns value');
});
describe('as an error',function() {
var logic2 = {
t.test('as an error', async t => {
const logic2 = {
error : function() { throw '#Error'; },

@@ -59,30 +43,19 @@ optional : function(_error) { return _error; },

it('should return undefined, if optional',function() {
return clues(Object.create(logic2),'optional')
.then(function(d) {
assert.equal(d,undefined);
});
});
const facts2 = () => Object.create(logic2);
it('should raise error if non-optonal',function() {
return clues(Object.create(logic2),'regular')
.then(function() {
throw 'Should error';
},function(e) {
assert.equal(e.message,'#Error');
});
});
let d = await clues(facts2,'optional');
t.same(d,undefined,'_ => undefined');
it('should return the error as an object when prefix is two underscores',function() {
return clues(Object.create(logic2),'showerror')
.then(function(e) {
assert.equal(e.error,true);
assert.equal(e.message,'#Error');
assert.equal(e.fullref,'showerror^error');
assert.equal(e.ref,'error');
});
});
d = await clues(facts2,'regular').then(shouldErr,Object);
t.same(d.message,'#Error','raises error if not optional');
d = await clues(facts2,'showerror');
t.same(d.error,true);
t.same(d.message,'#Error');
t.same(d.fullref,'showerror(error');
t.same(d.ref,'error');
});
});

@@ -1,11 +0,11 @@

var clues = require('../clues'),
assert = require('assert'),
Promise = require('bluebird');
const clues = require('../clues');
const Promise = require('bluebird');
const t = require('tap');
function shouldErr() { throw 'Should throw an error'; }
describe('private functions',function() {
var logic = {
M1 : function() { return Promise.delay(100,10); },
M2 : function private() { return Promise.delay(20,300); },
t.test('private functions', {autoend: true}, t => {
const Logic = {
M1 : function($private) { return Promise.delay(100,10); },
M2 : function $private() { return Promise.delay(20,300); },
M3 : ['M1','M2',function private(M1,M2) { return M1+M2; }],

@@ -19,46 +19,41 @@ M4 : function() { return Promise.delay(150,70); },

var facts = Object.create(logic);
t.test('before resolution', {autoend: true}, t => {
t.test('private regular function', async t => {
const facts = Object.create(Logic);
const e = await clues(facts,'M2').catch(Object);
t.same(e.message,'M2 not defined','errors as not defined');
t.same(facts.M2,Logic.M2,'fn is not run');
});
describe('before resolution ',function() {
it('regular fn not disclosed',function() {
return clues(facts,'M2')
.then(shouldErr,function(e) {
assert.equal(e.message,'M2 not defined');
assert.equal(facts.M2,logic.M2);
});
t.test('private array function', async t => {
const facts = Object.create(Logic);
const e = await clues(facts,'M3').catch(Object);
t.same(e.message,'M3 not defined','errors as not defined')
t.same(facts.M3, Logic.M3,'fn is not run');
});
it('array defined fn not disclosed',function() {
return clues(facts,'M3')
.then(shouldErr,function(e) {
assert.equal(e.message,'M3 not defined');
assert.equal(facts.M3,logic.M3);
});
});
t.test('$private argument', async t => {
const facts = Object.create(Logic);
const e = await clues(facts,'M1').catch(Object);
t.same(e.message,'M1 not defined','errors as not defined')
t.same(facts.M1, Logic.M1,'fn is not run');
})
});
describe('after resolution',function() {
it('should be accessible indirectly',function() {
return clues(facts,'MTOP')
.then(function(d) {
assert.equal(d,380);
});
});
t.test('after resolution', async t => {
const facts = Object.create(Logic);
const MTOP = await clues(facts,'MTOP');
const M1 = await clues(facts,'M1').catch(Object);
const M2 = await clues(facts,'M2').catch(Object);
const M3 = await clues(facts,'M3').catch(Object);
it('regular fn promise not disclosed',function() {
return clues(facts,'M2')
.then(shouldErr,function(e) {
assert.equal(e.message,'M2 not defined');
assert.equal(facts.M2.value(),300);
});
});
t.same(MTOP,380,'is available indirectly');
it('array defined fn promise not disclosed',function() {
return clues(facts,'M3')
.then(shouldErr,function(e) {
assert.equal(e.message,'M3 not defined');
assert.equal(facts.M3.value(),310);
});
});
t.same(M1.message,'M1 not defined','private fn not available directly');
t.same(facts.M1.value(),10,'private fn promise has the resolved value');
t.same(M2.message,'M2 not defined','private fn not available directly');
t.same(facts.M2.value(),300,'private fn promise has the resolved value');
t.same(M3.message,'M3 not defined','private array - not defined');
t.same(facts.M3.value(),310,'private fn promise has the resolved value');
});
});

@@ -1,157 +0,167 @@

var clues = require('../clues'),
assert = require('assert');
const clues = require('../clues');
const Promise = require('bluebird');
const t = require('tap');
describe('$property',function() {
var logic = {
a : 5,
simple : Object.create({
count : 0,
$property : function(ref) {
this.count+=1;
if (isNaN(ref)) throw 'NOT_A_NUMBER';
return +ref+2;
}
}),
funct : function(a) {
return {
t.test('$property', {autoend:true, jobs: 10}, t => {
t.test('in simple logic with $property', {autoend: true}, t => {
const facts = {
simple: {
count : 0,
$property : function(ref) {
this.count+=1;
return +ref+a;
if (isNaN(ref)) throw 'NOT_A_NUMBER';
return +ref+2;
}
};
},
nested : {
$property : function(ref) {
return { a : { b: { c: function() { return +ref+10; } } } };
}
},
shorthand : function $property(ref) {
return +ref * this.a;
},
concurrent : {
$property : function() {
return clues.Promise.delay(Math.random()*1000)
.then(function() {
return new Date();
});
}}
};
};
var facts = Object.create(logic);
t.test('asking for non-existent property', async t => {
const d = await clues(facts,'simple.1234');
t.same(d,1236,'returns the $property function value');
t.ok(facts.simple['1234'],'resolved promise is placed on logic');
t.same(facts.simple['1234'],1236,'promise contains value');
t.same(facts.simple.count,1,'function is called once');
});
describe('in simple logic',function() {
it('runs $property function for non-existent property',function() {
return clues(facts,'simple.1234')
.then(function(d) {
assert.equal(d,1236);
assert(facts.simple['1234'].isFulfilled());
assert.equal(facts.simple['1234'].value(),1236);
assert.equal(facts.simple.count,1);
});
t.test('asking for a different property',async t => {
t.same(await clues(facts,'simple.1235'),1237,'returns new value');
t.same(facts.simple.count,2,'$property has now been called twice');
});
it('concurrent requests should return first response',function() {
var requests = Array.apply(null, {length: 10})
.map(function() {
return clues(facts,'concurrent.test');
});
return clues.Promise.all(requests)
.then(function(d) {
return d.map(function(e) {
assert.equal(e,d[0],'Return time should be the same');
});
});
t.test('results so far are cached', async t => {
const d = await Promise.all([ clues(facts,'simple.1234'), clues(facts,'simple.1235')]);
t.same(d[0],1236,'first is cached');
t.same(d[1],1237,'second is cached');
t.equal(facts.simple.count,2,'an $property is not run again');
});
it('runs $property again for a different property',function() {
return clues(facts,'simple.1235')
.then(function(d) {
assert.equal(d,1237);
assert.equal(facts.simple.count,2);
});
t.test('error raised in $property', async t => {
const e = await clues(facts,'simple.abc',{},'__test__').catch(Object);
t.same(e.message,'NOT_A_NUMBER','error message ok');
t.same(e.ref,'abc','ref is ok');
t.same(e.fullref,'simple.abc','fullref is ok');
t.same(e.caller,'__test__','caller is ok');
t.equal(facts.simple.abc.isRejected(),true,'rejected promise in logic');
t.equal(facts.simple.abc.reason().message,'NOT_A_NUMBER','rejected promise has error');
t.equal(facts.simple.count,3,'$property has now been called 3 times');
});
});
it('previous results are cached',function() {
return clues(facts,['simple.1234','simple.1235',function(a,b) {
assert.equal(a,1236);
assert.equal(b,1237);
assert.equal(facts.simple.count,2);
}]);
t.test('concurrent request', async t => {
const facts = {
concurrent : {
count: 0,
$property : function() {
return clues.Promise.delay(Math.random()*1000)
.then(() => {
this.count += 1;
return new Date();
});
}
}
};
const d = await Promise.map([...Array(10)],() => clues(facts,'concurrent.test'));
t.ok( d.every(e => e == d[0]),'should return the same value');
t.same(facts.concurrent.count,1,'and should not rerun the function');
});
t.test('$property inside a function', {autoend: true}, t => {
const facts = {
a : 5,
funct : a => ({
count : 0,
$property : function(ref) {
this.count+=1;
return +ref+a;
}
})
};
t.test('asking for non-existent property', async t => {
t.same( await clues(facts,'funct.1234'), 1239, 'returns value');
t.same(facts.funct.count,1,'$property is called once');
t.same( await clues(facts,'funct.1234'), 1239, 'previous results are cached');
t.same(facts.funct.count,1,'$an fn not run again for same property');
});
});
it('handles errors correctly',function() {
return clues(facts,'simple.abc',{},'__test__')
.then(function() {
throw 'Should Error';
},function(e) {
assert.equal(e.message,'NOT_A_NUMBER');
assert.equal(e.ref,'abc');
assert.equal(e.fullref,'simple^abc');
assert.equal(e.caller,'__test__');
assert.equal(facts.simple.abc.isRejected(),true);
assert.equal(facts.simple.abc.reason().message,'NOT_A_NUMBER');
assert.equal(facts.simple.count,3);
});
t.test('$property inside a nested structure', {autoend:true}, t => {
const facts = {
nested : {
$property : function(ref) {
return { a : { b: { c: function() { return +ref+10; } } } };
}
}
};
t.test('asking for non existing property nested', async t => {
t.same( await clues(facts,'nested.1234.a.b.c'),1244,'returns correct value');
t.ok(facts.nested['1234'].a.b.c,'registers fulfilled promise');
t.same(facts.nested['1234'].a.b.c,1244,'promise registers value');
});
t.test('error raised in $property', async t => {
const e = await clues(facts,'nested.1234.a.b.d',{},'__test__').catch(Object);
t.same(e.ref,'d','ref ok');
t.same(e.fullref,'nested.1234.a.b.d','fullref ok');
t.same(e.caller,'__test__','caller ok');
});
});
describe('inside a function',function() {
it('runs $property function for non-existent property',function() {
return clues(facts,'funct.1234')
.then(function(d) {
assert.equal(d,1239);
assert.equal(facts.funct.value().count,1);
});
t.test('when function name is $property', {autoend: true}, t => {
const facts = {
a: 5,
shorthand : function $property(ref) {
return +ref * this.a;
}
};
t.test('asking for a property', async t => {
t.same( await clues(facts,'shorthand.2'),10,'returns right value');
t.same( facts.shorthand['2'],10,'stores promise with value');
});
it('caches previous results',function() {
return clues(facts,'funct.1234')
.then(function(d) {
assert.equal(d,1239);
assert.equal(facts.funct.value().count,1);
});
t.test('asking for another property', async t => {
t.same( await clues(facts,'shorthand.4'),20,'returns right value');
t.same( facts.shorthand['4'],20,'stores promise with value');
});
});
describe('inside a nested structure',function() {
it('runs $property',function() {
return clues(facts,'nested.1234.a.b.c')
.then(function(d) {
assert.equal(d,1244);
assert(facts.nested['1234'].value().a.b.c.isFulfilled());
assert(facts.nested['1234'].value(),1244);
t.test('when argument name is $property', {autoend: true}, t => {
const facts = {
a: 5,
as_argument: function ($property) {
return $property * this.a;
},
as_argument_es6 : a => $property => $property * a
};
t.test('regular function', async t => {
t.test('asking for a property', async t => {
t.same( await clues(facts,'as_argument.2'),10,'returns right value');
t.same( facts.as_argument['2'],10,'stores promise with value');
});
t.test('asking for another property', async t => {
t.same( await clues(facts,'as_argument.4'),20,'returns right value');
t.same( facts.as_argument['4'],20,'stores promise with value');
});
});
it('handles error correctly',function() {
return clues(facts,'nested.1234.a.b.d',{},'__test__')
.then(function() {
throw 'Should error';
},function(e) {
console.log('got error',e)
assert.equal(e.ref,'d');
assert.equal(e.fullref,'nested.1234.a.b^d');
assert.equal(e.caller,'__test__');
});
t.test('ES6 fat arrow', async t => {
t.test('asking for a property', async t => {
t.same( await clues(facts,'as_argument_es6.2'),10,'returns right value');
t.same( facts.as_argument_es6['2'],10,'stores promise with value');
});
t.test('asking for another property', async t => {
t.same( await clues(facts,'as_argument_es6.4'),20,'returns right value');
t.same( facts.as_argument_es6['4'],20,'stores promise with value');
});
});
});
describe('when function name is $property',function() {
it('acts as a shorthand for empty object with $property',function() {
return clues(facts,'shorthand.2')
.then(function(d) {
assert.equal(d,10);
assert.equal(facts.shorthand.value()['2'].value(),10);
return clues(facts,'shorthand.4');
})
.then(function(d) {
assert.equal(d,20);
assert.equal(facts.shorthand.value()['4'].value(),20);
assert.equal(facts.shorthand.value()['2'].value(),10);
});
});
});
});

@@ -1,7 +0,9 @@

var clues = require('../clues'),
assert = require('assert'),
Promise = require('bluebird');
const clues = require('../clues');
const Promise = require('bluebird');
const t = require('tap');
describe('In recursive logic',function() {
var logic = {
const shouldErr = () => { throw 'Should Error'; };
t.test('In recursive logic',{autoend: true}, t => {
const Logic = {
simple : {

@@ -23,11 +25,11 @@ value : 2,

e : {
f :12,
g : function(f) {
return 2*f;
},
h : function() {
return function(f) {
return f+2;
};
}
f :12,
g : function(f) {
return 2*f;
},
h : function() {
return function(f) {
return f+2;
};
}
}

@@ -42,96 +44,60 @@ }

var facts = Object.create(logic);
const facts = Object.create(Logic);
it('simple nesting works',function() {
return clues(facts,'simple.value')
.then(function(d) {
assert.equal(d,2);
});
t.test('simple nesting',async t => {
t.same( await clues(facts,'simple.value'),2,'works');
});
t.test('medium nesting works', async t => {
t.same( await clues(facts,'medium.bucket.value'),10,'works');
});
it('medium nesting works',function() {
return clues(facts,'medium.bucket.value')
.then(function(value) {
assert.equal(value,10);
});
t.test('ᐅ works as an alias for dot', async t => {
t.same( await clues(facts,'mediumᐅbucketᐅvalue'), 10, 'works');
});
t.test('complex nesting', async t => {
t.same( await clues(facts,'hard.a.b.c.d.id'),102,'works');
});
it('ᐅ works as an alias for dot',function() {
return clues(facts,'mediumᐅbucketᐅvalue')
.then(function(value) {
assert.equal(value,10);
});
t.test('ᐅ as an alias for dot',async t => {
t.same( await clues(facts,'hardᐅaᐅbᐅcᐅdᐅid'),102,'works');
});
describe('complex nesting',function() {
it('works', function() {
return clues(facts,'hard.a.b.c.d.id').then(function(value) {
assert.equal(value,102);
});
});
t.test('on returned functions',async t => {
t.same( await clues(facts,'hard.a.b.c.d.e.h'),14,'works');
});
it('ᐅ works as an alias for dot',function() {
return clues(facts,'hardᐅaᐅbᐅcᐅdᐅid')
.then(function(value) {
assert.equal(value,102);
});
});
t.test('works when fact repeats twice', async t => {
const d = await clues(facts,['hard.a.b.c.d.e','hard.a.b.c.d.e.f','hard.a.b.c.d.e.g',Array]);
t.same(d[1],12,'works');
t.same(d[2],24,'works');
});
it('works on returned functions',function() {
return clues(facts,'hard.a.b.c.d.e.h')
.then(function(h) {
assert.equal(h,14);
});
});
t.test('supports optional', async t => {
t.same( await clues(facts,['_hard.a.b.c.d.id',Number]),102,'works');
});
it('works when clue repeats twice',function() {
return clues(facts,['hard.a.b.c.d.e','hard.a.b.c.d.e.f','hard.a.b.c.d.e.g',function(a,b,c) {
assert.equal(b,12);
assert.equal(c,24);
}]);
});
it('supports optional',function() {
return clues(facts,['_hard.a.b.c.d.id',function(value) {
assert.equal(value,102);
}]);
});
it('can be resolved manually',function() {
return clues(facts,function(hard) {
return clues(hard.a.b,function(c) {
return clues(c.d,function(id) {
assert.equal(id,102);
});
});
t.test('path traversed manually', async t => {
const d = await clues(facts,hard => {
return clues(hard.a.b,c => {
return clues(c.d,'id');
});
});
t.same(d,102,'works');
});
it('bad path returns an error',function() {
return clues(facts,'hard.a.b.c.d.i.oo.oo')
.then(function() {
throw 'We should not arrive here';
},function(e) {
assert.equal(e.ref, 'i');
assert.equal(e.fullref, 'hard.a.b.c.d^i');
});
});
t.test('bad path returns an error', async t => {
const e = await clues(facts,'hard.a.b.c.d.i.oo.oo').then(shouldErr,Object);
t.same(e.message,'i not defined','errors');
t.same(e.ref,'i','ref ok');
t.same(e.fullref,'hard.a.b.c.d.i','fullref ok');
});
it('optional bad path returns undefined',function() {
return clues(facts,['_hard.a.b.e','simple.value',function(a,b) {
assert.equal(a,undefined);
assert.equal(b,2);
}]);
});
it('optional bad path returns undefined',function() {
return clues(facts,'hard.a.b.e')
.then(function() {
throw 'This function should return an error';
},function(e) {
assert.equal(e.message,'e not defined');
});
});
t.test('optional bad path returns undefined',async t => {
const d = await clues(facts,['_hard.a.b.e','simple.value',Array]);
t.same(d[0],undefined,'bad path is undefined');
t.same(d[1],2,'good path returns value');
});
});

@@ -1,59 +0,128 @@

var clues = require('../clues'),
assert = require('assert');
const clues = require('../clues');
const t = require('tap');
describe('$ as a first letter',function() {
t.test('$ as a first letter', {autoend:true}, t => {
var logic = {
const Logic = {
a : 10,
b : 11,
$logic_service : function(a) {
return a;
counter: {with_prep: 0, with_prep_arg: 0},
$logic_service : a => a,
$with_prep : function $prep(a,b,counter) {
counter.with_prep++;
let c = a + b;
return function $service(d) {
return c + d;
};
},
top : function(a) {
return {
$nested_service : function(b) {
return a+b;
}
$with_prep_arg : ($prep,a,b,counter) => {
counter.with_prep_arg++;
let c = a + b;
return function $service(d) {
return c + d;
};
}
},
top : a => ({ $nested_service : b => a + b })
};
var global = {
$global_service : function(b) {
return b;
}
};
const global = { $global_service : b => b };
describe('in logic',function() {
it ('should return a function',function() {
return clues(Object.create(logic),'$logic_service',Object.create(global))
.then(function($logic_service) {
assert.equal(typeof $logic_service,'function');
assert.equal($logic_service(20),20);
});
});
t.test('in logic', async t => {
const d = await clues(Object.create(Logic),'$logic_service',Object.create(global));
t.same(typeof d, 'function','returns a function');
t.same(d(20),20,'function works');
});
t.test('in nested logic', async t => {
const d = await clues(Object.create(Logic),'top.$nested_service',Object.create(global));
t.same(typeof d, 'function','returns a function');
t.same(d(20),30,'function works');
});
describe('in nested logic',function() {
it ('should return a function',function() {
return clues(Object.create(logic),'top.$nested_service',Object.create(global))
.then(function($nested_service) {
assert.equal(typeof $nested_service,'function');
assert.equal($nested_service(20),30);
});
});
t.test('in global', async t => {
await clues(Object.create(Logic),$global_service => {
t.same(typeof $global_service, 'function','returns a function');
t.same($global_service(20),20,'function works');
},Object.create(global));
});
describe('in global',function() {
it('should return a function',function() {
return clues(Object.create(logic),function($logic_service) {
assert.equal(typeof $logic_service,'function');
assert.equal($logic_service(20),20);
},Object.create(global));
t.test('with function called $prep', async t => {
t.test('prepares a service function', async t => {
const d = await clues(Object.create(Logic), '$with_prep', Object.create(global));
t.same(typeof d, 'function','returns a function');
t.same(d(5), 26);
});
t.test('prep solved only once', async t => {
let logic = Object.create(Logic);
logic.counter = {with_prep: 0, with_prep_arg: 0};
const d1 = await clues(logic, '$with_prep', Object.create(global));
const d2 = await clues(logic, '$with_prep', Object.create(global));
t.same(typeof d1, 'function','returns a function');
t.same(d1(5), 26);
t.same(logic.counter.with_prep, 1);
});
t.end();
});
});
t.test('with $prep as argument', async t => {
t.test('prepares a service function', async t => {
const d = await clues(Object.create(Logic), '$with_prep_arg', Object.create(global));
t.same(typeof d, 'function','returns a function');
t.same(d(5), 26);
});
t.test('prep solved only once', async t => {
let logic = Object.create(Logic);
logic.counter = {with_prep: 0, with_prep_arg: 0};
const d1 = await clues(logic, '$with_prep_arg', Object.create(global));
const d2 = await clues(logic, '$with_prep_arg', Object.create(global));
t.same(typeof d1, 'function','returns a function');
t.same(d1(5), 26);
t.same(logic.counter.with_prep_arg, 1);
});
t.end();
});
t.test('with $prep as an argument to a class function', async t => {
class Test {
constructor(a, counter) {
this.a = a;
this.counter = counter;
}
$multiply($prep,a,counter) {
counter.with_prep_arg++;
return function $service(d) {
return d * a;
};
}
}
const Logic = {
counter: {with_prep_arg: 0},
a: 2,
test: Test
};
t.test('prepares a service function', async t => {
const d = await clues(Object.create(Logic), 'test.$multiply', Object.create(global));
t.same(typeof d, 'function','returns a function');
t.same(d(5), 10);
});
t.test('prep solved only once', async t => {
let logic = Object.create(Logic);
logic.counter = {with_prep_arg: 0};
const d1 = await clues(logic, 'test.$multiply', Object.create(global));
const d2 = await clues(logic, 'test.$multiply', Object.create(global));
t.same(typeof d1, 'function','returns a function');
t.same(d1(5), 10);
t.same(logic.counter.with_prep_arg, 1);
});
t.end();
})
});

@@ -9,2 +9,3 @@ A client/server wrapper around `clues.js`, providing client access to clues solving to a browser or node client with local fact/logic space.

* `debug` Provide debug information with error messages
* `stringify` Use provided stringify function instead of default

@@ -31,2 +32,36 @@ The initialized server is a function that can be placed into `express` paths. If no arguments are given to the function, the API access is unrestricted, with requested variables (comma-delimited) in the `req.param.fn`. If a specific array of values is given to the function, it will solve those variables only. The user must provide all the inputs either as a JSON blob in the body and/or as querystring variables.

### 'inject(obj, prop, $global)'
This function takes the fact object as first argument and an object with properties that should be injected as the second argument. This function makes it easy to mock any endpoints in a logic tree by simply injecting the intended values in the right places. The key for each injected property should be the full path (dot notation) and the value can be a function or an absolute value. If the injection already overrides a pre-existing value/function the original will be available under `original_'+propertyname.
Example:
```js
const Db = {
user : function(userid) {
return database.query('users where usedid = ?',[userid])
}
};
const Logic = {
db : function(userid) {
return Object.create(Db,{
userid: {value: userid}
});
},
userid : ['input.userid',String]
};
let injected = function($global) {
return inject(Object.create(Logic), {
'userid': function(original_userid) {
return 'test_'+original_userid;
},
'db.user' : function(userid) {
return { desc: 'this is an injected response', userid: userid}
}
},$global);
};
clues(injected,'db.user',{input:{userid:'johndoe'}})
.then(console.log,console.log);
```

@@ -13,9 +13,25 @@ (function(self) {

}
// expose requests for external cancellation
var requests = self.reptileRequests = self.reptileRequests || {};
var Promise = clues.Promise;
function fetch() {
function defer() {
var resolve, reject;
var promise = new Promise(function() {
resolve = arguments[0];
reject = arguments[1];
});
return {
resolve: resolve,
reject: reject,
promise: promise
};
}
function fetch(options) {
var self = this,
buffer = '',
queue = self.$queue;
queue = self.$queue,
extraHandler = options.extraHandler;

@@ -42,2 +58,5 @@ delete self.$queue;

};
var uuid = Object.keys(queue).join(',')+String(Number(new Date()));
requests[uuid] = r;
r.addEventListener('loadend', function() { delete requests[uuid]; });

@@ -58,3 +77,2 @@ function processBuffer(d) {

var key = m[1],value;
if (!self[key]) return;

@@ -66,2 +84,10 @@ try {

}
if (!self[key]) {
if (extraHandler && value && !value.error) {
extraHandler(key, value);
}
return;
}
if (value && value.error)

@@ -81,3 +107,3 @@ queue[key].reject(value);

return function (ref) {
var defer = Promise.defer(),
var deferred = defer(),
self = this;

@@ -91,4 +117,4 @@

self.$queue[ref] = defer;
return defer.promise;
self.$queue[ref] = deferred;
return deferred.promise;
};

@@ -141,2 +167,2 @@ };

})(this);
})(this);

@@ -7,3 +7,3 @@ var clues = require('../clues'),

function stringify(obj,pretty,debug) {
function defaultStringify(obj, pretty, debug) {
var cache = [];

@@ -13,3 +13,3 @@

if (!value || value instanceof Date) return value;
if (typeof value === 'function' && value.name === 'private') return undefined;
if (typeof value === 'function' && (value.name === 'private' || value.name === '$private')) return undefined;
if (typeof value === 'function' || value.length && typeof value[value.length-1] === 'function')

@@ -42,4 +42,6 @@ return debug ? '[Function]' : undefined;

options = options || {};
options.$global = options.$global || {};
var stringify = typeof options.stringify === 'function' ? options.stringify : defaultStringify;
function stringifyError(e) {
function stringifyError(e,debug) {
var err = {error: true};

@@ -55,3 +57,3 @@ Object.getOwnPropertyNames(e)

if (!options.debug) {
if (!debug) {
err = {

@@ -64,2 +66,9 @@ error : true,

}
if (options.single)
return {
error : true,
message : err.message,
status : err.status,
xflash : err.xflash
};
return err;

@@ -73,2 +82,3 @@ }

var _res = (!options.quiet) ? res : {set: noop, write: noop, flush: noop},
_end = _res.end,
pretty = (options.pretty || req.query.pretty) && 2,

@@ -81,3 +91,6 @@ first = '{ \t\n\n';

if (typeof(res.flush) == 'function') _res.flush();
_res.end = function() {
_end.apply(_res,arguments);
_res.end = _res.write = _res.flush = noop;
};

@@ -94,10 +107,9 @@ Object.keys(req.query || {})

var duration = {};
var $global = Object.create(options.$global || {},{
var $global = Object.create(options.$global,{
res : {value: res},
req : {value: req},
input: {value: req.body},
$duration : {value: function(ref,time) {
duration[ref] = time;
$duration_stats: {value: {}},
$duration : {value: options.$global.$duration || function(ref,time) {
$global.$duration_stats[ref] = time;
}}

@@ -113,13 +125,30 @@ });

$global.root = facts;
function emit_property(ref,d,debug) {
var txt = {};
txt[ref] = d;
txt = first+stringify(txt,pretty,debug,req);
first = '';
_res.write(txt.slice(1,txt.length-1)+',\t\n');
if (typeof(res.flush) == 'function') _res.flush();
}
$global.$emit_property = emit_property;
// The api request is either determined by options.select, req.param.fn or by remaining url
var data = (options.select || decodeURI((req.params && req.params.fn) || req.url.slice(1).replace(/\//g,'.').replace(/\?.*/,'')).split(','))
var data = (options.select || decodeURIComponent((req.params && req.params.fn) || req.url.slice(1).replace(/\//g,'.').replace(/\?.*/,'')).split(','))
.map(function(ref) {
ref = ref.replace(/\//g,'.');
var debug;
if (ref === '' && options.debug) ref = facts;
return clues(facts,ref,$global,'__user__')
.catch(stringifyError)
.finally(function() {
debug = options.debug !== undefined ? options.debug : $global.reptileDebug;
})
.catch(function(e) {
return stringifyError(e,debug);
})
.then(function(d) {
if (options.single) {
_res.send(d.error ? (d.status||400) : 200, stringify(d,pretty,options.debug)+'\n');
_res.status(d.error ? (d.status||400) : 200)
.end(stringify(d,pretty,debug,req));
_res.write = noop;

@@ -132,8 +161,3 @@ _res.end = noop;

var txt = {};
txt[ref] = d;
txt = first+stringify(txt,pretty,options.debug);
first = '';
_res.write(txt.slice(1,txt.length-1)+',\t\n');
if (typeof(res.flush) == 'function') _res.flush();
emit_property(ref,d,debug);
});

@@ -150,8 +174,13 @@ });

.then(function() {
if (options.debug) {
var txt = {
$debug : duration
};
txt = stringify(txt,pretty,options.debug);
_res.write(txt.slice(1,txt.length-1)+',\t\n');
if (options.debug !== undefined ? options.debug : $global.reptileDebug) {
var stats = $global.$duration_stats;
stats = Object.keys(stats)
.map(function(key) {
var val = stats[key];
return {key: key, count:val.count, time: val.time, wait: val.wait};
})
.sort(function(a,b) {
return b.time - a.time;
});
emit_property('$debug',stats);
}

@@ -158,0 +187,0 @@ _res.write('"__end__" : true\t\n}');

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc