The Filter library exposes just two functions:
ARCcore.filter.create
.request
method of the filter object returned by ARCcore.filter.create
factory.Here is a simple example that demonstrates the use of both ARCcore.factory.create
and request
functions.
In this example we implement a trivial developer-defined bodyFunction
that returns the length
of a string passed by its caller provided that the input string is not "forceImplementationBug"
in which case we return object.
What we want is to guarantee that our bodyFunction
is never called with anything other than a string.
And, we want to guarantee that bodyFunction
never returns anything other than a numerical value (or error)
to its caller.
To accomplish this with the Filter library we wrap bodyFunction
in a filter object that declares these
behavioral constraints using filter specifications.
// simple filter example
const arccore = require('arccore');
// Create a filter object via the filter factory.
var factoryResponse = arccore.filter.create({
operationID: 'demo', // pick a random 22-char identifier
operationName: 'Simple Example Filter',
operationDescription: 'Filter that accepts a string and returns either error, or numerical result.',
inputFilterSpec: { ____accept: 'jsString' },
bodyFunction: function(input) {
var result;
if (input !== "forceImplementationBug")
result = input.length;
else
result = { message: "Implementation bug!" };
return { error: null, result: result };
},
outputFilterSpec: { ____accept: 'jsNumber' }
});
// Throw exception if the filter factory fails.
if (factoryResponse.error)
throw new Error(factoryResponse.error);
// Dereference the filter object.
var exampleFilter = factoryResponse.result;
// Test good and bad input.
var responses = {
badInput: exampleFilter.request({ message: "Not what the filter expects" }),
goodInput: exampleFilter.request("The length of this string is 32."),
implementationBug: exampleFilter.request("forceImplementationBug")
};
console.log(JSON.stringify(responses, undefined, 4));
Executing this small program produces the following output.
$ node filter-example-1.js
{
"badInput": {
"error": "Filter [VEil5yIpSySKkE5QjuCe8Q::Simple Example Filter] failed while normalizing request input. Error at path '~': Value of type 'jsObject' not in allowed type set [jsString].",
"result": null
},
"goodInput": {
"error": null,
"result": 32
},
"implementationBug": {
"error": "Filter [VEil5yIpSySKkE5QjuCe8Q::Simple Example Filter] failed while normalizing response result. Error at path '~': Value of type 'jsObject' not in allowed type set [jsNumber].",
"result": null
}
}
On first glance this might look like too much work for such a trivial operation. But, if you were to implement this operation traditionally with inline input and output checks, and then implement an exhaustive set of tests to ensure functionality and protect your production code from accidental regression, you would end up writing even more code than this. Note that here you need to write zero external tests to guarantee that your constraints are always enforced; Filter takes care of that for you and is backed itself by ~540 unit tests.
Briefly, consider the alternative: as changing requirements force you to alter your product code function contracts and assumptions change. Changing API contracts incur the following costs:
These costs add up in a hurry and have a real impact on team agility, and product quality. Filter makes it easy to uplevel quickly-authored function subroutines to production-quality and avoid the creep of technical debt.
In the previous example we showed how to manufacture a filter object via ARCcore.filter.create
software factory function.
The following subsections detail the optional and required inputs and explain how these inputs are used by the filter factory to customize the run-time behavior of constructed filter objects.
In the previous example, we manufactured a simple filter object by passing the following input object to the filter
factory function ARCcore.filter.create
:
const filterFactoryInput = {
operationID: 'demo', // pick a random 22-char identifier
operationName: 'Simple Example Filter',
operationDescription: 'Filter that accepts a string and returns either error, or numerical result.',
inputFilterSpec: { ____accept: 'jsString' },
bodyFunction: function(input) {
var result;
if (input !== "forceImplementationBug")
result = input.length;
else
result = { message: "Implementation bug!" };
return { error: null, result: result };
},
outputFilterSpec: { ____accept: 'jsNumber' }
};
The following subsections explain each of the name-value pairs in detail.
The operationID
parameter is a required string input.
The value provided is required to be either "demo", or a unique 22-character Internet Routable URI Token (IRUT) identifier.
IRUT's are 22-character, 128-bit unique identifier strings that can be used in URI's and URL's that are created
with the ARCcore.identifier library export function ARCcore.identifier.irut.fromEther
.
If the special reserved value demo
is used, then ARCcore.identifier.irut.fromEther
is called by the filter
factory internally to obtain an instance-specific IRUT to assign to the constructed filter object. This facility
is useful for demos (obviously). And, in situations where it's not important to be able to identify a filter object
by its operation ID.
The operationName
parameter is an optional string.
If specified, operationName
should be a short descriptive operation moniker that is useful to human working in
the debugger. And to humans reading automatically-generated documentation for filters.
The operationDescription
parameter is an optional string.
If specified, operationDescription
should briefly explain the purpose and operation of the filter object.
Similar to operationName
, operationDescrition
is informational metadata for human developers.
The inputFilterSpec
parameter is an optional filter specification object.
If specified, inputFilterSpec
is used to customize the run-time behavior of a filter object's internal
input Filter Spec Processor (iFSP) which constrains the set of inputs that will propagated to subsequent
stages within the filter object implementation.
If inputFilterSpec
is not specified (i.e. undefined), then the iFSP is disabled allowing any input passed
to the filter object's request
method to propagate forward.
Most filter objects define inputFilterSpec
.
The bodyFunction
parameter is an optional JavaScript function defined by a developer.
If bodyFunction
is not specified (see Filter Semantic Conventions below for details) then
the filter object's request
method will pass the iFSP's output straight through to the rFSP
and oFSP (result and output Filter Spec Processor stages) without any custom transformation.
This is typical when filter objects are used for object construction, and generic signature-
based software message routing via the ARCcore.discriminator library.
If specified, bodyFunction
is dispatched iff the filter object's iFSP accepts the input passed to the filter's
request
method (or if the iFSP is disabled).
Developer-defined bodyFunction
's must be written using a prescriptive signature that is required by the
Filter library implementation. Specifically, bodyFunction
must adhere to the following conventions:
error
and result
with the following semantics:
error
must be a string value. If complex error information is be communicated, use JSON.error
indicates the result
is invalid and should not be used by the caller.That's it. Beyond these baseline requirements, the Filter library imposes no additional restrictions on what
a developer can do inside their bodyFunction
.
Having at this point written hundreds of filter objects, the following boilerplate example is the
recommended starting point for implementing your own bodyFunction
's:
// Custom bodyFunction boilerplate sample
var bodyFunction = function(request) {
// Initialize a response object in the correct format required by the filter library.
// If we encounter an error, we indicate this by setting response.error to a string value.
// Note that if response.error is a string, then response.result is _not_ validated allowing
// additional information about the error to be returned.
// If our algorithm completes without error, then we leave response.result `null` and set
// `response.result` to be validated/normalized by the output filter spec processor (oFSP).
var response = { error: null, result: undefined };
// A simple JavaScript idiom to help us with error handling.
var errors = [];
var inBreakScope = false;
while (!inBreakScope) {
inBreakScope = true;
////
// BEGIN: YOUR CUSTOM ALGORITHM
// Do something that might fail here ...
var subOpResult = subOperation();
// If we're not happy with subOpResult...
if (subOpResult !== true) {
// insert an error into the errors array
errors.unshift("Sub-operation did not return true as expected!");
// exit the while loop
break;
}
// Okay - that worked. Set the result.
response.result = subOpResult;
// END: YOUR CUSTOM ALGORITHM
////
} // end while
// If any error(s) occurred, set response.error to indicate that response.result is invalid.
if (errors.length)
response.error = errors.join(" ");
// Return the response to filter.
return response;
};
The outputFilterSpec
parameter is an optional filter specification object.
If specified, outputFilterSpec
is used to customize the run-time behavior of a filter object's internal
output Filter Spec Processor (oFSP) which constrains the set of values that a filter object's request
is allowed to return via the result
parameter of its response
object.
If outputFilterSpec
is not specified (i.e. undefined), then the oFSP is disabled allowing any response
result value to be returned by the filter object's request
methods.
As explained in the previous section, the input object passed into the Filter libraries filter factory
ARCcore.filter.create
allows developers to optionally specify values for inputFilterSpec
,
bodyFunction
, and outputFilterSpec
. Currently, all eight possibilities are allowed. However,
there are only a few cases that we actually ever use.
Here are some simple conventions that we follow to build filter objects for specific use cases:
Oftentimes we need to validate/normalize untrusted input from outside of our application before allowing that data to be processed by our application business logic.
To accomplish this, we need a filter specification and a simple filter
that does not perform any custom transformation (i.e. we don't need a bodyFunction
).
By convention, we construct a filter object for this purpose by calling ARCcore.filter.create
setting the inputFilterSpec
, and leaving bodyFunction
and outputFilterSpec
undefined as follows:
const arccore = require('arccore');
var factoryResponse = arccore.filter.create({
operationID: 'demo',
inputFilterSpec: require('./ingress-spec')
});
Following this convention allows readers of your code to quickly surmise that you're simply validating/ normalizing input before allowing it into the interior of your application.
Ingress filters are also frequently used to affect signature-based software message routing via the ARCcore.discriminator library.
Oftentimes we need to validate/normalize data that is destined for external subsystems or applications. This could be because we're being extra careful with sensitive information or brittle external subsystems. And, sometimes we just want/need an extra layer of assurance that we haven't done something horrible (e.g. sending bad writes to external storage subsystems).
To accomplish this, we need a filter specification and a simple filter that
does not perform any custom transformation (i.e. we don't need a bodyFunction
).
By convention, we construct a filter object for this purpose by calling ARCcore.filter.create
setting the outputFilterSpec
, and leaving inputFilterSpec
and bodyFunction
undefined as follows:
const arccore = require('arccore');
var factoryResponse = arccore.filter.create({
operationID: 'demo',
outputFilterSpec: require('./egress-spec')
});
Following this convention allows readers of your code to quickly surmise that you're simply validating/ normalizing output before passing it out of your application.