Hudson Taylor (HT) is a library for building software systems with a Service Oriented Architecture. It comprises of server library for providing services with well documented and defined APIs, and a client library for calling services. All service APIs have Schemas that both document expectations as well as validate and pre-process incoming data.
HT can be used within a single process to logically partition services from the beginning. This means that your project has clean internal interfaces and when it comes time to scale out, you can break-out services and replicate them horizontally.
var ht = require("hudson-taylor");
var myServiceSetup = require("./echo").setup; // Import your service setup.
var s = ht.Service();
s.add("myEchoService", myServiceSetup);
s.listenHTTP({port : 7001}); // Start the service listening via HTTP.
exports.setup = function(s, readyCallback) {
/* service setup functions take a service object and a callback, You must
* call the callback as soon as your service is ready to receive jobs.
*/
readyCallback(); // No async setup, callback immediately.
//Implement an echo API schema with documentation:
var echoSchema = {
_doc : "Echo API, returns string input that you send to it.",
input : s.String({
maxLength : 200,
_doc : "Input string to be returned" }),
}
s.on("echo", schema, function(data, callback) {
callback(null, data.input); // Data returned must be encodable as JSON
});
//Other API handlers here..
}
This connects to our echo service via HTTP and logs a returned string.
var s = ht.Services();
s.connect("myEchoService", ht.HTTPClient("http://auth.localhost", 7001));
s.remote("myEchoService", "echo", {input : "Hello World!", function(err, res) {
if(!err) return console.error(err);
console.log(res);
});
One of the nice things about HT is that you can connect to a service via several different methods without changing your service code in the slightest.
This can make deployment very flexible, here are some of the communication methods:
''' server method / client method '''
- listenHTTP / HTTPClient: uses JSON-RPC over HTTP to communicate.
- listenHTTP / HTTPRoundRobinClient: uses JSON-RPC over HTTP to communicate with multiple instances of a service.
- listenTCP / TCPClient: uses JSON-RPC over TCP to communicate.
- N/A / LocalServiceClient: Host a service natively in process and use JSON and local function calls to communicate. This is very useful for development, testing environments or small deployments.
Custom server and client channels can be implemented easily.
The easiest way to get started writing services is to export a setup function for each service, and then pass those to ht.Services().add(). Your service setup function will register one or more handlers using service.on() (see below). The signature for a service setup function is:
function(<service>, readyCallback, /* any extra arguments passed to ht.Services.add() */)
The extra arguments to add() allow you to pass in other services, database connections, logging etc that you can use within your signal handlers, for instance here is a contrived authentication API that calls out to a 'session' service to check an auth token before continuing with regular authentication:
authSetup = require("./auth").setup;
var db, logger, services = null;
/* Do some db setup, configure client services, then: */
services.add("auth", authSetup, db, logger, services);
And then in your setup:
exports.setup = function(s, ready, db, logger, otherService) {
s.on("login", <schema>, function(data, callback) {
// Check if we've got a valid oAuth token
services.remote("session", "checkToken", {token : data.token}, function(err, sessionCheck) {
if(err) return callback(err);
if(!sessionCheck.authenticated) {
//user needs to authenticate again
db.users.find({id : data.userID}, function(err, user) {
//do some checks
callback(authenticated);
});
});
});
})
service.on("signal", <schema>, handler(data, callback), /* optional */ validationErrorHandler(err, callback))
var schema = s.Obj({
_doc : "Echo API, returns string input that you send to it.",
input : s.String({
maxLength : 200,
_doc : "Input string to be returned" }),
})
Each schema type is an object with a 'validate' method that will attempt to validate the object and return the new data, or alternatively raise an error. These can be used to do type conversion if required. They also have a 'document' method which is called when generating API documentation.
There are several built-in schema types and you can easily implement your own. This can be useful for doing automatic coversion of application specific types, for instance converting an 'id' string into a mongoDB ObjectID.
props : { }