ARCcore.filter Library API Reference
Login
-Home
/
-Documentation
/
-ARCcore
/
-filter
/
API
+identifier
+graph
-filter
+discriminator
+types
+util
API
Architecture
Specifications
Examples
ARCcore.filter Library API Reference
ARCcore.filter library developer API documentation and reference.

Filter Factory API

The Filter library exposes just two functions:

  • The main software factory function which is exported from the ARCcore package as ARCcore.filter.create.
  • The run-time request method of the filter object returned by ARCcore.filter.create factory.

Simple Example

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:

  • Implementation refactoring of specific subsystem code (nothing we can do about that).
  • Modification of subsystem unit tests to align with the updated I/O and behavior contracts.
  • Modification of functional tests to ensure that the impact does not destabilize the rest of the product.
  • Continued hassles maintaining consistency across the product wrt calling conventions, error reporting, and documentation.

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.

Filter Factory Configuration

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.

Factory Input Object

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.

filterFactoryInput.operationID

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.

filterFactoryInput.operationName

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.

filterFactoryInput.operationDescription

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.

filterFactoryInput.inputFilterSpec

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.

filterFactoryInput.bodyFunction

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:

  • Accepts a single value as input.
  • Returns an object with sub-properties error and result with the following semantics:
    • If set, error must be a string value. If complex error information is be communicated, use JSON.
    • If set, 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;
};

filterFactoryInput.outputFilterSpec

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.

Filter Semantic Conventions

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:

Ingress Filters

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.

Egress Filters

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.

Encapsule Project, Seattle WA
Copyright © 2018 Chris Russell
Sat Dec 15 2018 04:56:06 GMT-0500 (EST)

Encapsule/holism v0.0.26
Documents Under Contruction