Sitelet https://github.com/bmatusiak/rectify/tree/main/example
Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

README.md

Rectify examples

Two of them. The first answers "what is this library", the second answers "how do I build something with it".

node hello/index.js      # a service, a consumer, and nothing else
node app/index.js        # a small app in the shape these are built in
node app/test.js         # and how an app in that shape is tested

No dependencies, node built-ins included. The plugin files import nothing at all -- everything they use arrives through imports -- so the only thing a plugin here depends on is another plugin. Only the boot imports the plugins, and the tests import the harness.

hello

Two plugins and a boot. plugin_b provides hello, plugin_a consumes it, and index.js hands both to Rectify. Thirty seconds, and it is the same thing the top of the main README walks through.

app

app/
  index.js         the boot: gather the plugins, start, drive
  test.js          the same boot, with the test plugins added
  config.js        settings, keyed by the service name each plugin provides
  plugins/
    log/           provides log, consumes only the base class
    server/        provides server -- route() is where other plugins attach
    time/          consumes server, extends it, provides time
    status/        consumes server and time, asks time, provides nothing

A tiny app, kept deliberately unimpressive, so that what it demonstrates is the wiring rather than the features. There is no HTTP in it: server.request is an in-memory dispatch, which means the file that would become a real server is one plugin, and nothing around it would change.

How plugins use each other

A service is not only functions to call -- it is where other plugins attach. server registers a route method, and the plugins that consume it add their own routes as they load:

// plugins/time/plugin.js
export default function setup(imports, register) {
    var { Plugin, server, log } = imports;

    var plugin = new Plugin("time");

    server.route("GET", "/time", function () {     // extending another plugin
        return { now: now() };
    });

    register(null, {                               // and offering its own
        time: plugin.api({ now: now, uptime: uptime }),
        onDestroy: plugin.unload
    });
}

setup.consumes = ["Plugin", "server", "log"];
setup.provides = ["time"];
// plugins/status/plugin.js -- asks the time plugin rather than reading a clock
export default function setup(imports, register) {
    var { server, time, log } = imports;

    server.route("GET", "/status", function () {
        return { now: time.now(), uptimeMs: time.uptime(), ok: true };
    });

    register();
}

setup.consumes = ["app", "server", "time", "log"];
setup.provides = [];

Neither file mentions the other's path, and nothing declares that log loads before server before time before status -- that order falls out of what each one declares. index.js lists them backwards on purpose. Adding a plugin means adding it to that list, never finding the right place in it.

Three of the four are built on PluginBase, which is why they consume "Plugin" and hand plugin.api({...}) to register. status is not, on purpose: it registers no service, so it has nothing to state a surface for and nothing to undo. The base class is optional, and easier to believe as optional when something in front of you is not using it.

Two things shape an app built this way:

  • A plugin extends another during its own setup, so a provider must not start doing its job until the load is finished, or it serves 404s for whichever plugin had not registered yet. The server waits for the "ready" its PluginBase instance announces -- which is why index.js never opens it.
  • A plugin that provides nothing is normal. status exists only to add a route to someone else. It still declares provides: [] and still calls register().

Testing

A test is a plugin. There is a plugin.test.js beside each plugin, and each one is an ordinary Rectify plugin that consumes the thing it tests:

// plugins/time/plugin.test.js
import harness from "../../../../harness.js";

var { describe, it, assert } = harness;

export default function setup(imports, register) {
    var { time, server } = imports;

    describe("time", function () {

        it("provides a clock", function () {
            assert.ok(typeof time.now === "function");
        });

        it("added its route to the server", async function () {
            var answered = await server.request("GET", "/api/time");
            assert.equal(answered.status, 200);
        });

    });

    register();
}

setup.consumes = ["time", "server"];
setup.provides = [];

Declaring consumes: ["time"] is what hands it the real service and guarantees it loads after it. Nothing had to be wired up to make a plugin testable -- the wiring that builds the app is the wiring that runs the tests.

Loading and running stay two separate moments. describe and it only register a suite while the app loads; the bodies run afterwards, when the boot asks the harness for them. That matters because most of what is worth checking is not true yet while a plugin loads -- the server refuses requests until the app is up. Its own suite leans on exactly that: the request it fires during setup is asserted, later, to have been refused.

test.js is index.js with the test plugins added to the list:

var plugins = [
    status, time, server, log, rectify.PluginBase,
    logTest, serverTest, timeTest, statusTest
];
plugins.config = config;

var app = rectify.build(plugins, { appName: "example-app" });
await app.start();

var results = await harness.run();

It also holds the checks no single plugin can make about itself: that the graph came out whole, that dropping a plugin fails build() before any plugin body runs, and that swapping one plugin changes the answer and nothing else. Substitution needs no library -- a double is a plugin providing the same name, so it is one entry in the list:

var plugins = [status, frozenTime, server, log, rectify.PluginBase];   // instead of `time`

which is also the only way to say exactly what status answers, since the real clock moves.