Pronto.js: Creating and chaining commands
Pronto.js is a JavaScript framework for Node.js designed for writing fast, efficient, asynchronous, component-based applications. It can be used for web applications, REST API servers, command-line programs, and so on.
Pronto.js is based on the idea that code consists of three major conceptual pieces. First there is a route. A route executes a chain of commands. So writing a Pronto application is all about writing commands, and then organizing them into chains mapped to routes.
Here we look at how to write commands. In this article, we will build a small sample app composed of two commands chained together in a single route. We'll highlight how to write commands and how to share components through the context. <!--break-->
Creating a First Command: RandomName
The first command is our RandomName
command. It's job is to put a random name into the context so that other commands can later use that name. We're not going to be all that ambitious with our first command. It'll be basic.
Conceptually, all commands in Pronto.js inherit the pronto.Command
prototype. That prototype has some core event logic that allows Pronto to supervise the command's lifecycle.
When it comes to implementing a new command, there are three basic steps:
- Create a new command constructor.
- Make sure the new command inherits
pronto.Command
(Tip: there's a utility function that does this for you.) - Declare the
execute()
function.
With these in mind, here is the complete code for the RandomName
command:
var pronto = require('pronto');
// Constructor
function RandomName() {
// The pool of names.
this.names = ['Anna', 'Bob', 'Claire', 'Dennis'];
}
module.exports = RandomName;
// Inherit the Command prototype.
pronto.inheritsCommand(RandomName);
// The command body.
RandomName.prototype.execute = function (cxt, params) {
// Get a random number within this.name's range.
var i = Math.floor(Math.random() * this.names.length);
// Get a random name.
var randomName = this.names[i];
// We're done, here's the name.
this.done(randomName);
}
There's not much to the RandomName()
constructor. It just declares our paltry pool of name possibilities. Often, command constructors are empty.
RandomName.prototype.execute
is where most of the code lives. The execute()
function, when run, will be given two parameters:
cxt
: Thepronto.Context
object. This has information about the currently executing request. It also functions as a name/value storage for the current request. We'll return to it later.params
: AnObject
of name/value pairs passed into this command. We'll see more of this later.
For our simple example, we won't directly use either of these. But we will indirectly use the context.
The first few lines of the execute()
function are pretty straightforward: We just retrieve a random name from this.names
.
But the last line of execute()
is the key.
this.done(randomName);
This call does two very important things:
- It stores
randomName
in the context so that another command can use it. - It signals Pronto that it is done executing.
Why is that second one necessary? won't return
do that? The answer is no. Pronto is built to be asynchronous. It's possible that a command will execute a callback, but shouldn't actually notify Pronto that it is complete until the callback is fired. As a quick example, we could read a file like this:
SomeCommand.prototype.execute = function (cxt, params) {
// Make sure we can still access this
var cmd = this;
fs.readFile('foo.txt', function (data) {
cmd.done(data);
});
}
The command above doesn't notify Pronto that it is complete until the file's contents have been read.
That's all there is to our first command. Now let's write a second one -- one that takes advantage of Pronto's ability to pass parameters to commands.
Creating a Second Command: SayHi
The second command is designed to "say hi" on the console. It takes a name, and optionally an alternate greeting.
Here's the entire code to SayHi
:
var pronto = require('pronto');
// Constructor
function SayHi() {
}
module.exports = SayHi;
// Inherit the Pronto Command prototype.
pronto.inheritsCommand(SayHi);
// This is the command.
SayHi.prototype.execute = function (cxt, params) {
// One param is required. It's the 'to' param.
this.required(params, ['to']);
var to = params.to;
// If the param 'message' is specified, we will use
// this to generate the message. Otherwise we will
// use the default string.
var message = params.message || 'Hi %s.';
// Say 'hi' to someone.
console.log(message, to);
// Tell Pronto we're done.
this.done();
}
This time around, the boilerplate at the top should be familiar: We require pronto
, export the command, create a constructor, and inherit the pronto.Command
prototype.
Let's focus on the SayHi.prototype.execute
function.
The first thing this does is declare that it requires some params. In this case, it actually requires only one:
// One param is required. It's the 'to' param.
this.required(params, ['to']);
The base pronto.Command
object will ensure that every command in the Array
is present before continuing. In other words, it requires that Pronto pass it in values for those parameters. we require that a to
parameter be passed in. This is the name of the person we will say "hi" to.
Not all parameters have to be required. Any parameters can be passed in on the param
object.
It's best practice to clearly document what parameters a command can take, and also to assign all params a to a local variable right at the top of the execute()
function so that others can clearly see this at a glance.
Our code above accepts one addition non-required parameter: message
. If this is set, it will be used. Otherwise, the default message of Hi %s
is used.
So we now have two parameters:
to
: Who we are saying "Hi" to.message
: The string we will use to say "hi".
Time to print those to the console: console.log(message, to)
.
From there, we just need to tell Pronto we're done: this.done()
. We don't pass anything into done()
this time, so nothing is stored in the context for this object.
Putting the Commands Together
Now we can write a Pronto program to use our two new commands. We're going to write an example program that says "Hi" to a random person. Here is the code, which I have in example.js
.
var pronto = require('pronto');
var RandomName = require('./lib/randomname');
var SayHi = require('./lib/sayhi');
var register = new pronto.Registry();
var router = new pronto.Router(register);
register.route('example')
.does(RandomName, 'name')
.does(SayHi)
.using('to').from('cxt:name')
router.handleRequest('example');
This is a basic single-run console app for Pronto (as opposed to a web app, which would start up a server). If any of the boilerplate looks unfamiliar, you may want to check out Creating Apps with Pronto.
Here's the important part:
register.route('example')
.does(RandomName, 'name')
.does(SayHi)
.using('to').from('cxt:name')
This registers a route, called example
, that will do the following when run:
- Execute
RandomName
and store the name in the context asname
. (Remember that call todone(randomName)
? That'll be in the context with the keyname
.) - Execute
SayHi
passing the parameterto
the value ofcxt:name
. That last part (from
) will cause Pronto to look in the context for a keyname
, which is therandomName
we created earlier.
The output, then, will be something like this:
Hi Claire.
Now with a minor change, we can change the greeting:
register.route('example')
.does(RandomName, 'name')
.does(SayHi)
.using('to').from('cxt:name')
.using('message', 'Hello %s')
Now we are passing in a second parameter. And we want to set it's value directly, not having the value retrieved from (from()
) somewhere else. So we just do using('message', 'Hello %s')
. Now the output will be:
Hello Claire
And that is how we pass values from one command to another. (The technical name for this, by the way, is dependency injection, and this is very close to the design pattern known as Inversion of Control (IoC).)
In a well-constructed Pronto chain, each command will be responsible for one discrete operation. For a database tool, then, you might have separate commands to do each of the following:
- Open a database connection (putting the connection in the context)
- Run a query (using the connection, and returning the result set)
- Iterating over the result set (using the result set, perhaps printing the value)
- Closing the connection (using the connection)
It may at first seem tedious until you begin building bigger tools. Well-constructed components are highly re-usable, and can rapidly be re-assembled into other routes and applications.