Examples

Note about the Java server examples: The server classes are simply the interface implementations. The code is exposed using the provided BarristerServlet class via web.xml. Examples are run using the Maven Jetty plugin. For more details see the example source.

Hello World

This example demonstrates a simple Calulator interface with two functions.

It also shows how to access basic metadata about the IDL contract such as:

  • barrister_version - Version of barrister that translated the IDL to JSON
  • date_created - Time JSON file was translated. UTC milliseconds since epoch.
  • checksum - A hash of the structure of the IDL that ignores comments, whitespace, and function parameter names. The intent is that if the checksum changes, something semantically meaningful about the IDL has changed.
//
// The Calculator service is easy to use.
//
// Examples
// --------
//
//     x = calc.add(10, 30)
//     # x == 40
//
//     y = calc.subtract(44, 10)
//     # y == 34

interface Calculator {
    // Adds two numbers together and returns the result   
    add(a float, b float) float
    
    // Subtracts b from a and returns the result
    subtract(a float, b float) float
}
package example;

public class Server implements Calculator {

    public Double add(Double a, Double b) {
        return a+b;
    }

    public Double subtract(Double a, Double b) {
        return a-b;
    }

}
package example;

import com.bitmechanic.barrister.HttpTransport;

public class Client {

    public static void main(String argv[]) throws Exception {
        HttpTransport trans = new HttpTransport("http://127.0.0.1:8080/example/");
        CalculatorClient calc = new CalculatorClient(trans);

        System.out.println(String.format("1+5.1=%.1f", calc.add(1.0, 5.1)));
        System.out.println(String.format("8-1.1=%.1f", calc.subtract(8.0, 1.1)));

        System.out.println("\nIDL metadata:");

        // BarristerMeta is a Idl2Java generated class in the same package
        // as the other generated files for this IDL
        System.out.println("barrister_version=" + BarristerMeta.BARRISTER_VERSION);
        System.out.println("checksum=" + BarristerMeta.CHECKSUM);
    }

}
var barrister = require('barrister');
var express   = require('express');
var fs        = require('fs');

function Calculator() { }
Calculator.prototype.add = function(a, b, callback) {
    // first param is for errors
    callback(null, a+b);
};
Calculator.prototype.subtract = function(a, b, callback) {
    callback(null, a-b);
};

var idl    = JSON.parse(fs.readFileSync("../calc.json").toString());
var server = new barrister.Server(idl);
server.addHandler("Calculator", new Calculator());

var app = express.createServer();
app.use(express.bodyParser());
app.post('/calc', function(req, res) {
    server.handle({}, req.body, function(respJson) {
        res.contentType('application/json');
        res.send(respJson);
    });
});
app.listen(7667);
var barrister = require('barrister');

function checkErr(err) {
    if (err) {
        console.log("ERR: " + JSON.stringify(err));
        process.exit(1);
    }
}

var client = barrister.httpClient("http://localhost:7667/calc");

client.loadContract(function(err) {
    checkErr(err);

    var calc = client.proxy("Calculator");

    calc.add(1, 5.1, function(err, result) {
        var i;
        checkErr(err);
        console.log("1+5.1=" + result);

        calc.subtract(8, 1.1, function(err, result) {
            checkErr(err);
            console.log("8-1.1=" + result);

            console.log("\nIDL metadata:");
            meta = client.getMeta();
            keys = [ "barrister_version", "checksum" ];
            for (i = 0; i < keys.length; i++) {
                console.log(keys[i] + "=" + meta[keys[i]]);
            }
        });
    });
});
<?php

$path = $_ENV["BARRISTER_PHP"];
include_once("$path/barrister.php");

class Calculator {

  function add($a, $b) {
    return $a + $b;
  }

  function subtract($a, $b) {
    return $a - $b;
  }

}

$server = new BarristerServer("../calc.json");
$server->addHandler("Calculator", new Calculator());
$server->handleHTTP();
?>
<?php

$path = $_ENV["BARRISTER_PHP"];
include_once("$path/barrister.php");

$barrister = new Barrister();
$client    = $barrister->httpClient("http://localhost:8080/cgi-bin/server.php");
$calc      = $client->proxy("Calculator");

echo sprintf("1+5.1=%.1f\n", $calc->add(1, 5.1));
echo sprintf("8-1.1=%.1f\n", $calc->subtract(8, 1.1));

echo "\nIDL metadata:\n";
$meta = $client->getMeta();
$keys = array("barrister_version", "checksum");
foreach ($keys as $i=>$key) {
  echo "$key=$meta[$key]\n";
}

?>
from flask import Flask, request, make_response
import barrister

# Our implementation of the 'Calculator' interface in the IDL
class Calculator(object):

    # Parameters match the params in the functions in the IDL
    def add(self, a, b):
        return a+b

    def subtract(self, a, b):
        return a-b

contract = barrister.contract_from_file("../calc.json")
server   = barrister.Server(contract)
server.add_handler("Calculator", Calculator())

app = Flask(__name__)

@app.route("/calc", methods=["POST"])
def calc():
    resp_data = server.call_json(request.data)
    resp = make_response(resp_data)
    resp.headers['Content-Type'] = 'application/json'
    return resp

app.run(host="127.0.0.1", port=7667)
import barrister

trans  = barrister.HttpTransport("http://localhost:7667/calc")

# automatically connects to endpoint and loads IDL JSON contract
client = barrister.Client(trans)

print "1+5.1=%.1f" % client.Calculator.add(1, 5.1)
print "8-1.1=%.1f" % client.Calculator.subtract(8, 1.1)

print
print "IDL metadata:"
meta = client.get_meta()
for key in [ "barrister_version", "checksum" ]:
    print "%s=%s" % (key, meta[key])

# not printing this one because it changes per run, which breaks our
# very literal 'examples' test harness, but let's verify it exists at least..
assert meta.has_key("date_generated")
require 'sinatra'
require 'barrister'

class Calculator

  def add(a, b)
    return a+b
  end

  def subtract(a, b)
    return a-b
  end

end

contract = Barrister::contract_from_file("../calc.json")
server   = Barrister::Server.new(contract)
server.add_handler("Calculator", Calculator.new)

post '/calc' do
  request.body.rewind
  resp = server.handle_json(request.body.read)
  
  status 200
  headers "Content-Type" => "application/json"
  resp
end
require 'barrister'

trans = Barrister::HttpTransport.new("http://localhost:7667/calc")

# automatically connects to endpoint and loads IDL JSON contract
client = Barrister::Client.new(trans)

puts "1+5.1=%.1f" % client.Calculator.add(1, 5.1)
puts "8-1.1=%.1f" % client.Calculator.subtract(8, 1.1)

puts
puts "IDL metadata:"
meta = client.get_meta
[ "barrister_version", "checksum" ].each do |key|
    puts "#{key}=#{meta[key]}"
end
1+5.1=6.1
8-1.1=6.9

IDL metadata:
barrister_version=0.1.6
checksum=51a911b5eb0b61fbb9300221d8c37134

Batches and Errors

This example demonstrates two Barrister concepts:

  • Custom errors
    • How to generate errors from server code
    • How to trap errors in client code
  • Batches
    • How to create a batch request
    • How to add function calls to a batch
    • How to send the batch
    • How to iterate through batch results
interface Echo {
  // if s == &quot;err&quot; then server should return 
  // an error with code=99
  //
  // otherwise it should return s
  echo(s string) string
}
package example;

import com.bitmechanic.barrister.RpcException;

public class Server implements Echo {

    public String echo(String s) throws RpcException {
        if (s.equals("err")) {
            throw new RpcException(99, "Error!");
        }
        else {
            return s;
        }
    }

}
package example;

import com.bitmechanic.barrister.HttpTransport;
import com.bitmechanic.barrister.RpcException;
import com.bitmechanic.barrister.RpcResponse;
import com.bitmechanic.barrister.Batch;
import java.util.List;

public class Client {

    public static void main(String argv[]) throws Exception {
        HttpTransport trans = new HttpTransport("http://127.0.0.1:8080/example/");
        EchoClient client = new EchoClient(trans);
        
        System.out.println("hello");
        try {
            client.echo("err");
        }
        catch (RpcException e) {
            System.out.println("err.code=" + e.getCode());
        }

        Batch batch = new Batch(trans);
        EchoClient batchEcho = new EchoClient(batch);
        batchEcho.echo("batch 0");
        batchEcho.echo("batch 1");
        batchEcho.echo("err");
        batchEcho.echo("batch 2");
        batchEcho.echo("batch 3");

        List<RpcResponse> result = batch.send();
        for (RpcResponse resp : result) {
            if (resp.getError() != null) {
                System.out.println("err.code=" + resp.getError().getCode());
            }
            else {
                System.out.println(resp.getResult());
            }
        }
    }

}
var barrister = require('barrister');
var express   = require('express');
var fs        = require('fs');

function Echo() { }
Echo.prototype.echo = function(s, callback) {
    if (s === "err") {
        callback({ code: 99, message: "Error!" }, null);
    }
    else {
        callback(null, s);
    }
};

var idl    = JSON.parse(fs.readFileSync("../batch.json").toString());
var server = new barrister.Server(idl);
server.addHandler("Echo", new Echo());

var app = express.createServer();
app.use(express.bodyParser());
app.post('/batch', function(req, res) {
    server.handle({}, req.body, function(respJson) {
        res.contentType('application/json');
        res.send(respJson);
    });
});
app.listen(7667);
var barrister = require('barrister');

function runSync(funcs) {
    if (funcs.length > 0) {
        var nextFunction = funcs.shift();

        nextFunction(function(err, result) {
            var i;
            if (err) {
                console.log("err.code=" + err.code);
            }
            else {
                if (result instanceof Array) {
                    // handle batch result
                    for (i = 0; i < result.length; i++) {
                        if (result[i].error) {
                            console.log("err.code=" + result[i].error.code);
                        }
                        else {
                            console.log(result[i].result);
                        }
                    }
                }
                else {
                    // handle single result
                    console.log(result);
                }
            }
            runSync(funcs);
        });

    }
}

////////

var client = barrister.httpClient("http://localhost:7667/batch");

client.loadContract(function(err) {
    if (err) { 
        console.log("error loading contract");
        process.exit(1);
    }

    var echo  = client.proxy("Echo");

    var batch = client.startBatch();
    var batchEcho = batch.proxy("Echo");

    var funcs = [
        function(next) { echo.echo("hello", next); },
        function(next) { echo.echo("err", next); },
        function(next) {
            batchEcho.echo("batch 0");
            batchEcho.echo("batch 1");
            batchEcho.echo("err");
            batchEcho.echo("batch 2");
            batchEcho.echo("batch 3");
            batch.send(next);
        }
    ];
    runSync(funcs);
});
<?php

$path = $_ENV["BARRISTER_PHP"];
include_once("$path/barrister.php");

class EchoServer {

  // echo is a reserved word in PHP. The current workaround is to
  // append an underscore to any reserved method names.  Barrister
  // will try to resolve "[name]_" if the specified
  // function name is not found.
  function echo_($s) {
    if ($s === "err") {
      throw new BarristerRpcException(99, "Error!");
    }
    else {
      return $s;
    }
  }

}

$server = new BarristerServer("../batch.json");
$server->addHandler("Echo", new EchoServer());
$server->handleHTTP();
?>
<?php

$path = $_ENV["BARRISTER_PHP"];
include_once("$path/barrister.php");

$barrister = new Barrister();
$client    = $barrister->httpClient("http://localhost:8080/cgi-bin/server.php");
$echo      = $client->proxy("Echo");

echo $echo->echo("hello") . "\n";
try {
    $echo->echo("err");
}
catch (BarristerRpcException $e) {
    echo "err.code=" . $e->getCode() . "\n";
}

$batch = $client->startBatch();

$batchEcho = $batch->proxy("Echo");
$batchEcho->echo("batch 0");
$batchEcho->echo("batch 1");
$batchEcho->echo("err");
$batchEcho->echo("batch 2");
$batchEcho->echo("batch 3");

$results = $batch->send();
foreach ($results as $i=>$res) {
    if ($res->error) {
        echo "err.code=" . $res->error->code . "\n";
    }
    else {
        echo $res->result . "\n";
    }
}

?>
from flask import Flask, request, make_response
import barrister

class Echo(object):

    def echo(self, s):
        if s == "err":
            raise barrister.RpcException(99, "Error!")
        else:
            return s

contract = barrister.contract_from_file("../batch.json")
server   = barrister.Server(contract)
server.add_handler("Echo", Echo())

app = Flask(__name__)

@app.route("/batch", methods=["POST"])
def batch():
    resp_data = server.call_json(request.data)
    resp = make_response(resp_data)
    resp.headers['Content-Type'] = 'application/json'
    return resp

app.run(host="127.0.0.1", port=7667)
import barrister

trans  = barrister.HttpTransport("http://localhost:7667/batch")
client = barrister.Client(trans)

print client.Echo.echo("hello")
try:
    client.Echo.echo("err")
except barrister.RpcException as e:
    print "err.code=%d" % e.code

batch = client.start_batch()
batch.Echo.echo("batch 0")
batch.Echo.echo("batch 1")
batch.Echo.echo("err")
batch.Echo.echo("batch 2")
batch.Echo.echo("batch 3")

results = batch.send()
for res in results:
    if res.result:
        print res.result
    else:
        # res.error is a barrister.RpcException
        # you can throw it here if desired
        print "err.code=%d" % res.error.code
require 'sinatra'
require 'barrister'

class Echo

  def echo(s)
    if s == "err"
      raise Barrister::RpcException.new(99, "Error!")
    else
      return s
    end
  end

end

contract = Barrister::contract_from_file("../batch.json")
server   = Barrister::Server.new(contract)
server.add_handler("Echo", Echo.new)

post '/batch' do
  request.body.rewind
  resp = server.handle_json(request.body.read)
  
  status 200
  headers "Content-Type" => "application/json"
  resp
end
require 'barrister'

trans  = Barrister::HttpTransport.new("http://localhost:7667/batch")
client = Barrister::Client.new(trans)

puts client.Echo.echo("hello")
begin
  client.Echo.echo("err")
rescue Barrister::RpcException => e
  puts "err.code=#{e.code}"
end

batch = client.start_batch()
batch.Echo.echo("batch 0")
batch.Echo.echo("batch 1")
batch.Echo.echo("err")
batch.Echo.echo("batch 2")
batch.Echo.echo("batch 3")

result = batch.send
result.each do |r|
  # either r.error or r.result will be set
  if r.error
    # r.error is a Barrister::RpcException, so you can raise it if desired
    puts "err.code=#{r.error.code}"
  else
    # result from a successful call
    puts r.result
  end
end
hello
err.code=99
batch 0
batch 1
err.code=99
batch 2
batch 3

Type validation

This example demonstrates the automatic type validation that Barrister performs including:

  • Simple type mismatches (e.g. pass an int when a string is expected)
  • Ensuring that objects contain all non-optional struct fields (including fields on parent structs)
  • Ensuring that objects do not contain properties missing from a struct
  • Ensuring enum values are valid
enum PageCategory {
    local
    world
    sports
    business
}

struct Entity {
    id           string
    createdTime  int
    updatedTime  int
    version      int
}

struct Page extends Entity {
    authorId        string
    publishTime     int           [optional]
    title           string
    body            string
    category        PageCategory
    tags            []string      [optional]
}

interface ContentService {

  // Adds a new page to the system.  Automatically updates createdTime and updatedTime.
  // sets version to 1.
  //
  // returns the generated page id
  addPage(authorId string, title string, body string, category PageCategory) string

  // Raises error code 30 if page.version is out of date
  // Raises error code 40 if no page exists with the given page.id
  //
  // otherwise it updates the page, increments the version, and returns the
  // revised version number
  updatePage(page Page) int
  
  // Deletes the page with the given id and version number
  //
  // returns false if no page exists with the id
  //
  // if page exists, raises error code 30 if version is out of date
  // otherwise deletes page and returns true
  deletePage(id string, version int) bool

  // Returns null if page is not found
  getPage(id string) Page [optional]

}
<?php

$path = $_ENV["BARRISTER_PHP"];
include_once("$path/barrister.php");

function _debug($s) {
    file_put_contents('php://stderr', "$s\n");
}

function now_millis() {
    return time() * 1000;
}

function create_page($authorId, $title, $body, $category, $publishTime=null) {
    $now_ms = now_millis();
    return (object) array(
             "id"          => uniqid("", true),
             "version"     => 1,
             "createdTime" => $now_ms,
             "updatedTime" => $now_ms,
             "authorId"    => $authorId,
             "title"       => $title,
             "body"        => $body,
             "category"    => $category,
             "publishTime" => $publishTime );
}

class ContentService {

    function __construct() {
        $this->_load();
    }

    function addPage($authorId, $title, $body, $category) {
        $page = create_page($authorId, $title, $body, $category);
        $id = $page->id;
        $this->pagesById->$id = $page;
        $this->_save();
        return $id;
    }

    function updatePage($page) {
        $id = $page->id;
        $existing = $this->getPage($id);
        if (!$existing) {
            throw new BarristerRpcException(40, "No page exists with id: $id");
        }
        elseif ($existing->version !== $page->version) {
            throw new BarristerRpcException(30, "Version out of date: $page->version != $existing->version");
        }
        else {
            $version = $existing->version + 1;
            $page->version     = $version;
            $page->createdTime = $existing->createdTime;
            $page->updatedTime = now_millis();
            $this->pagesById->$id = $page;
            $this->_save();
            return $version;
        }
    }

    function deletePage($id, $version) {
        $existing = $this->getPage($id);
        if ($existing) {
            if ($existing->version === $version) {
                unset($this->pagesById->$id);
                $this->_save();
                return true;
            }
            else {
                throw new BarristerRpcException(30, "Version out of date");
            }
        }
        else {
            return false;
        }
    }

    function getPage($id) {
        return $this->pagesById->$id;
    }

    function _save() {
        file_put_contents("content.json", json_encode($this->pagesById));
    }

    function _load() {
        if (!file_exists("content.json")) {
            $this->pagesById = (object) array();
            return;
        }

        $data = file_get_contents("content.json");
        if ($data === false) {
            $this->pagesById = (object) array();
        }
        else {
            $this->pagesById = json_decode($data, false);
        }
    }

}

$server = new BarristerServer("../validation.json");
$server->addHandler("ContentService", new ContentService());
$server->handleHTTP();
?>
<?php

function assertFailure($script, $line, $message) {
    print "Assertion failed: $script on line: $line\n";
}

assert_options(ASSERT_ACTIVE,   true);
assert_options(ASSERT_BAIL,     true);
assert_options(ASSERT_WARNING,  false);
assert_options(ASSERT_CALLBACK, 'assertFailure');

function updatePageExpectErr($page) {
    global $service;
    try {
        $service->updatePage($page);
        print "updatePage allowed invalid page\n";
        exit(1);
    }
    catch (BarristerRpcException $e) {
        assert($e->getCode() === -32602);
    }
}

$path = $_ENV["BARRISTER_PHP"];
include_once("$path/barrister.php");

$barrister = new Barrister();
$client    = $barrister->httpClient("http://localhost:8080/cgi-bin/server.php");
//$client    = $barrister->httpClient("http://localhost:7667/content");
$service   = $client->proxy("ContentService");

$invalid_add_page = array(
    // use an int for authorId
    array(1, "title", "body", "sports"),
    // pass a null title
    array("author-1", null, "body", "sports"),
    // pass a float for body
    array("author-1", "title", 32.3, "sports"),
    // pass a bool for category
    array("author-1", "title", "body", true),
    // pass an invalid enum value
    array("author-1", "title", "body", "op-ed")
);

foreach ($invalid_add_page as $i=>$page_data) {
    try {
        $service->addPage($page_data[0], $page_data[1], $page_data[2], $page_data[3]);
        print "addPage allowed invalid data\n";
        exit(1);
    }
    catch (BarristerRpcException $e) {
        // -32602 is the standard JSON-RPC error code for
        // "invalid params", which Barrister uses if types are invalid
        assert($e->getCode() === -32602);
    }
}
print "Test 1 - Passed\n";


//
// Test 2 - Create a page, then test getPage/updatePage cases
//

$pageId = $service->addPage("author-1", "title", "body", "sports");
$page   = $service->getPage($pageId);
assert($page !== null);

$page->title = "new title";
$page->publishTime = time() * 1000;
$version = $service->updatePage($page);
assert($version === 2);

$page2 = $service->getPage($pageId);
assert($page2->title === $page->title);
assert($page2->publishTime === $page->publishTime);

print "Test 2 - Passed\n";

//
// Test 3 - Test updatePage type validation
//

$page = $page2;

// Remove required fields one at a time and verify that updatePage rejects request
$required_fields = array("id", "createdTime", "updatedTime", "version", "body", "title");
foreach ($required_fields as $i=>$field) {
    $page_copy = clone $page;
    unset($page_copy->$field);
    updatePageExpectErr($page_copy);
}

// Try sending a struct with an extra field
$page_copy = clone $page;
$page_copy->unknown = "hi";
updatePageExpectErr($page_copy);

// Try sending an array with an invalid element type
$page_copy = clone $page;
$page_copy->tags = array("good", "ok", 1);
updatePageExpectErr($page_copy);

// Try a valid array
$page_copy = clone $page;
$page_copy->tags = array("good", "ok");
$version = $service->updatePage($page_copy);
assert($version === 3);

print "Test 3 - Passed\n";


//
// Test 4 - getPage / deletePage
//

// delete non-existing page
assert(false === $service->deletePage("bogus-id", $version));

// delete real page
assert(true === $service->deletePage($page->id, $version));

// get page we just deleted
assert(null === $service->getPage($page->id));

print "Test 4 - Passed\n";

?>
from flask import Flask, request, make_response
import barrister
import uuid
import time

def now_millis():
    return int(time.time() * 1000)

def create_page(authorId, title, body, category, publishTime=None):
    now_ms = now_millis()
    return { "id"          : uuid.uuid4().hex,
             "version"     : 1,
             "createdTime" : now_ms,
             "updatedTime" : now_ms,
             "authorId"    : authorId,
             "title"       : title,
             "body"        : body,
             "category"    : category,
             "publishTime" : publishTime }

class ContentService(object):

    def __init__(self):
        self.pagesById = { }

    def addPage(self, authorId, title, body, category):
        page = create_page(authorId, title, body, category)
        self.pagesById[page["id"]] = page
        return page["id"]

    def updatePage(self, page):
        existing = self.getPage(page["id"])
        if not existing:
            raise barrister.RpcException(40, "No page exists with id: %s" % page["id"])
        elif existing["version"] != page["version"]:
            raise barrister.RpcException(30, "Version is out of date")
        else:
            version = existing["version"] + 1
            page["version"]     = version
            page["createdTime"] = existing["createdTime"]
            page["updatedTime"] = now_millis()
            self.pagesById[page["id"]] = page
            return version

    def deletePage(self, id, version):
        existing = self.getPage(id)
        if existing:
            if existing["version"] == version:
                del self.pagesById[id]
                return True
            else:
                raise barrister.RpcException(30, "Version is out of date")
        else:
            return False

    def getPage(self, id):
        if self.pagesById.has_key(id):
            return self.pagesById[id]
        else:
            return None

contract = barrister.contract_from_file("../validation.json")
server   = barrister.Server(contract)
server.add_handler("ContentService", ContentService())

app = Flask(__name__)

@app.route("/content", methods=["POST"])
def content():
    resp_data = server.call_json(request.data)
    resp = make_response(resp_data)
    resp.headers['Content-Type'] = 'application/json'
    return resp

app.run(host="127.0.0.1", port=7667)
import barrister
import sys
import copy
import time

trans  = barrister.HttpTransport("http://localhost:7667/content")
client = barrister.Client(trans)

#
# Test 1 - Try adding a page with incorrect types.  Note that server.py 
#          has no type validation code.  Type enforcement is done
#          automatically by Barrister based on the IDL

invalid_add_page = [
    # use an int for authorId
    [ 1, "title", "body", "sports" ],
    # pass a null title
    [ "author-1", None, "body", "sports" ],
    # pass a float for body
    [ "author-1", "title", 32.3, "sports" ],
    # pass a bool for category
    [ "author-1", "title", "body", True ],
    # pass an invalid enum value
    [ "author-1", "title", "body", "op-ed" ]
]

for page_data in invalid_add_page:
    try:
        client.ContentService.addPage(*page_data)
        print "addPage allowed invalid data: %s" % page_data
        sys.exit(1)
    except barrister.RpcException as e:
        # -32602 is the standard JSON-RPC error code for
        # "invalid params", which Barrister uses if types are invalid
        assert e.code == -32602

print "Test 1 - Passed"


#
# Test 2 - Create a page, then test getPage/updatePage cases
#

pageId = client.ContentService.addPage("author-1", "title", "body", "sports")
page   = client.ContentService.getPage(pageId)
assert page != None

page["title"] = "new title"
page["publishTime"] = int(time.time() * 1000)
version = client.ContentService.updatePage(page)
assert version == 2

page2 = client.ContentService.getPage(pageId)
assert page2["title"] == page["title"]
assert page2["publishTime"] == page["publishTime"]

print "Test 2 - Passed"

#
# Test 3 - Test updatePage type validation
#
def updatePageExpectErr(page):
    try:
        client.ContentService.updatePage(page)
        print "updatePage allowed invalid page: %s" % str(page)
        sys.exit(1)
    except barrister.RpcException as e:
        assert e.code == -32602

page = page2

# Remove required fields one at a time and verify that updatePage rejects request
required_fields = [ "id", "createdTime", "updatedTime", "version", "body", "title" ]
for field in required_fields:
    page_copy = copy.copy(page)
    del page_copy[field]
    updatePageExpectErr(page_copy)

# Try sending a struct with an extra field
page_copy = copy.copy(page)
page_copy["unknown-field"] = "hi"
updatePageExpectErr(page_copy)

# Try sending an array with an invalid element type
page_copy = copy.copy(page)
page_copy["tags"] = [ "good", "ok", 1 ]
updatePageExpectErr(page_copy)

# Try a valid array
page_copy = copy.copy(page)
page_copy["tags"] = [ "good", "ok" ]
version = client.ContentService.updatePage(page_copy)
assert version == 3

print "Test 3 - Passed"


#
# Test 4 - getPage / deletePage
#

# delete non-existing page
assert False == client.ContentService.deletePage("bogus-id", version)

# delete real page
assert True  == client.ContentService.deletePage(page["id"], version)

# get page we just deleted
assert None == client.ContentService.getPage(page["id"])

print "Test 4 - Passed"
require 'sinatra'
require 'barrister'

def now_millis
  return (Time.now.to_f * 1000).floor
end

def create_page(authorId, title, body, category, publishTime=nil)
    now_ms = now_millis()
    return { "id"          => Barrister::rand_str(24),
             "version"     => 1,
             "createdTime" => now_ms,
             "updatedTime" => now_ms,
             "authorId"    => authorId,
             "title"       => title,
             "body"        => body,
             "category"    => category,
             "publishTime" => publishTime }
end

class ContentService

  def initialize
    @pagesById = { }
  end

  def addPage(authorId, title, body, category)
    page = create_page(authorId, title, body, category)
    @pagesById[page["id"]] = page
    return page["id"]
  end

  def updatePage(page)
    existing = getPage(page["id"])
    if !existing
      raise Barrister::RpcException.new(40, "No page exists with id: " + page["id"])
    elsif existing["version"] != page["version"]
      raise Barrister::RpcException.new(30, "Version out of date")
    else
      version = existing["version"] + 1
      page["version"]     = version
      page["createdTime"] = existing["createdTime"]
      page["updatedTime"] = now_millis
      @pagesById[page["id"]] = page
      return version
    end
  end

  def deletePage(id, version)
    existing = getPage(id)
    if existing
      if existing["version"] == version
        @pagesById.delete(id)
        return true
      else
        raise Barrister::RpcException.new(30, "Version out of date")
      end
    else
      return false
    end
  end

  def getPage(id)
    return @pagesById[id]
  end

end

contract = Barrister::contract_from_file("../validation.json")
server   = Barrister::Server.new(contract)
server.add_handler("ContentService", ContentService.new)

post '/content' do
  request.body.rewind
  resp = server.handle_json(request.body.read)
  
  status 200
  headers "Content-Type" => "application/json"
  resp
end
require 'barrister'

def assert(b)
  if !b
    raise RuntimeError, "Failed assertion"
  end
end

def now_millis
  return (Time.now.to_f * 1000).floor
end

trans = Barrister::HttpTransport.new("http://localhost:7667/content")
client = Barrister::Client.new(trans)

#
# Test 1 - Try adding a page with incorrect types.  Note that server.py 
#          has no type validation code.  Type enforcement is done
#          automatically by Barrister based on the IDL

invalid_add_page = [
    # use an int for authorId
    [ 1, "title", "body", "sports" ],
    # pass a null title
    [ "author-1", nil, "body", "sports" ],
    # pass a float for body
    [ "author-1", "title", 32.3, "sports" ],
    # pass a bool for category
    [ "author-1", "title", "body", true ],
    # pass an invalid enum value
    [ "author-1", "title", "body", "op-ed" ]
]

invalid_add_page.each do |page_data|
  begin
    client.ContentService.addPage(*page_data)
    abort("addPage allowed invalid data: #{page_data}")
  rescue Barrister::RpcException => e
    # -32602 is the standard JSON-RPC error code for
    # "invalid params", which Barrister uses if types are invalid
    assert e.code == -32602
  end
end

puts "Test 1 - Passed"


#
# Test 2 - Create a page, then test getPage/updatePage cases
#

pageId = client.ContentService.addPage("author-1", "title", "body", "sports")
page   = client.ContentService.getPage(pageId)
assert page != nil

page["title"] = "new title"
page["publishTime"] = now_millis
version = client.ContentService.updatePage(page)
assert version == 2

page2 = client.ContentService.getPage(pageId)
assert page2["title"] == page["title"]
assert page2["publishTime"] == page["publishTime"]

puts "Test 2 - Passed"

#
# Test 3 - Test updatePage type validation
#
def updatePageExpectErr(client, page)
  begin
    client.ContentService.updatePage(page)
    abort("updatePage allowed invalid page: #{page}")
  rescue Barrister::RpcException => e
    assert e.code == -32602
  end
end

page = page2

# Remove required fields one at a time and verify that updatePage rejects request
required_fields = [ "id", "createdTime", "updatedTime", "version", "body", "title" ]
required_fields.each do |field|
  page_copy = page.clone
  page_copy.delete(field)
  updatePageExpectErr(client, page_copy)
end

# Try sending a struct with an extra field
page_copy = page.clone
page_copy["unknown-field"] = "hi"
updatePageExpectErr(client, page_copy)

# Try sending an array with an invalid element type
page_copy = page.clone
page_copy["tags"] = [ "good", "ok", 1 ]
updatePageExpectErr(client, page_copy)

# Try a valid array
page_copy = page.clone
page_copy["tags"] = [ "good", "ok" ]
version = client.ContentService.updatePage(page_copy)
assert version == 3

puts "Test 3 - Passed"


#
# Test 4 - getPage / deletePage
#

# delete non-existing page
assert false == client.ContentService.deletePage("bogus-id", version)

# delete real page
assert true  == client.ContentService.deletePage(page["id"], version)

# get page we just deleted
assert nil == client.ContentService.getPage(page["id"])

puts "Test 4 - Passed"
Test 1 - Passed
Test 2 - Passed
Test 3 - Passed
Test 4 - Passed

Experimental: Alternate transport / serializer

This example shows how you can build multi-tier topologies with Barrister. In this fictional example requests originate from the client via HTTP as JSON messages.

Imagine a backend implemented as a set of independent services that use Redis as a broker. For performance, backend messages are encoded using message pack.

We want to ensure that only authenticated HTTP clients can put messages on the Redis bus, and we want the backend services to be able to access the username of the user originating the request. To summarize the flow:

  • Client makes Barrister requests using HTTP, secured with Basic Auth
    • In this example, the client is Python, but in the real world this might be a rich web app written in Backbone.js that makes service calls over HTTP
  • Web Server accepts request and acts as an authenticating router:
    • Basic Auth header is decoded and verified (in memory in this example, but in the real world, perhaps against a database, or another downstream service)
    • Barrister request is deserialized as JSON, and re-encoded with message pack. A name/value pair map of headers is included in the msg pack message. The headers include a reply_to key used to route the reply from the backend server, and a username key that indicates the identity of the HTTP user.
    • Msg pack message is queued on a Redis list using lpush
    • We block for a reply on reply_to key in Redis using brpop with a 30 second timeout
  • Backend worker process actually implements the IDL
    • Has a while(true) loop that poll Redis for requests using brpop
    • For each request:
      • Message is deserialized via message pack
      • Request body is processed using Barrister
      • Response is serialized via message pack
      • Response is sent to Redis via lpush using the reply_to header as the key

This architecture is interesting because it allows us to:

  • Write a rich Javascript client that can directly consume backend services
  • Write a mostly domain-neutral HTTP router that is responsible for basic security
  • Write all our business logic on the backend as decoupled processes

The Redis transport could easily be swapped out for other transports such as ZeroMQ, AMQP, etc. The redis code is about 10 lines total in this example.

struct Contact {
    contactId   string
    username    string
    firstName   string
    lastName    string
}

interface ContactService {
    put(c Contact) string
    get(contactId string) Contact [optional]
    remove(contactId string) bool
}
from flask import Flask, request, Response, make_response
from functools import wraps
from multiprocessing import Process
import barrister
import redis
import msgpack
import json
import uuid
import threading
import signal
import select

# Helper functions to serialize/deserialize the msgpack messages
def dump_msg(headers, body):
    return msgpack.dumps([ headers, body ])
    
def load_msg(raw):
    return msgpack.loads(raw)

# Auth code from: http://flask.pocoo.org/snippets/8/
def check_auth(username, password):
    """This function is called to check if a username /
    password combination is valid.
    """
    return username == 'johndoe' and password == 'johnpass'

def authenticate():
    """Sends a 401 response that enables basic auth"""
    return Response(
    'Could not verify your access level for that URL.\n'
    'You have to login with proper credentials', 401,
    {'WWW-Authenticate': 'Basic realm="Login Required"'})

def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            return authenticate()
        return f(*args, **kwargs)
    return decorated

###############################################################
# Router -- Accepts HTTP requests and sends to Redis
#
# Doesn't actually process any messages.  Has no domain
# specific code, aside from user authentication.
###############################################################

app = Flask(__name__)

# generic redis bridging function
def bridge_to_redis_backend(queue, http_req):
    # unpack request JSON and reserialize w/msgpack
    req = json.loads(http_req.data)
    
    # create a headers map that contains a 'reply_to' key that is unique
    # the worker will send the response to that key, and we'll dequeue it
    # from there.
    headers = { "reply_to" : "reply-" + uuid.uuid4().hex }
    
    # if we have HTTP auth, add the username to the headers
    # so that downstream processes know the user context that originated
    # the request, and can apply additional security rules as desired
    if http_req.authorization:
        headers["username"] = http_req.authorization.username
    
    msg_to_redis = dump_msg(headers, req)
    
    # send to redis
    redis_client = redis.StrictRedis("localhost", port=6379)
    redis_client.lpush(queue, msg_to_redis)
    
    # block for 30 seconds for a reply on our reply_to queue
    raw_resp = redis_client.brpop(headers["reply_to"], timeout=30)
    
    if raw_resp:
        (headers, resp) = load_msg(raw_resp[1])
    else:
        errmsg = "30 second timeout on queue: %s" % queue
        resp = { "jsonrpc" : "2.0", "error" : { "code" : -32700, "message" : errmsg } }
    
    return resp

@app.route("/contact", methods=["POST"])
@requires_auth
def contact():
    # Send to redis -- let some backend worker process it
    resp = bridge_to_redis_backend("contact", request)
    
    # serialize as JSON and send to client
    http_resp = make_response(json.dumps(resp))
    http_resp.headers['Content-Type'] = 'application/json'
    return http_resp

def start_router():
    try:
        app.run(host="127.0.0.1", port=7667)
    except select.error:
        pass

###############################################################
# Worker -- This is the Barrister Server process.  It polls
#        Redis for requests, and processes them.  In practice
#        this would be a separate daemon.  With Redis as a
#        central broker, you could run as many copies of this
#        process as you wish to balance load.
#################################################################

class ContactService(object):

    def __init__(self, req_context):
        """
        req_context is a thread local variable that we use to
        share out of band context.  In this example we use it
        to give this class access to the headers on the request,
        which are used to enforce security rules
        """
        self.contacts    = { }
        self.req_context = req_context

    def put(self, contact):
        existing = self._get(contact["contactId"])
        if existing:
            self._check_contact_owner(existing)
        
        self._check_contact_owner(contact)
        self.contacts[contact["contactId"]] = contact
        return contact["contactId"]
        
    def get(self, contactId):
        c = self._get(contactId)
        if c:
            self._check_contact_owner(c)
            return c
        else:
            return None
        
    def remove(self, contactId):
        c = self._get(contactId)
        if c:
            self._check_contact_owner(c)
            del self.contacts[contactId]
            return True
        else:
            return False
            
    def _get_username(self):
        """
        Grabs the username from the thread local context
        """
        headers = self.req_context.headers
        try:
            return headers["username"]
        except:
            return None
            
    def _get(self, contactId):
        try:
            return self.contacts[contactId]
        except:
            return None
    
    def _check_contact_owner(self, contact):
        username = self._get_username()
        if not username or username != contact["username"]:
            raise barrister.RpcException(4000, "Permission Denied for user")
            
def start_worker():
    # create a thread local that we can use to store request headers
    req_context = threading.local()
    
    contract = barrister.contract_from_file("../redis-msgpack.json")
    server   = barrister.Server(contract)
    server.add_handler("ContactService", ContactService(req_context))
    
    redis_client = redis.StrictRedis("localhost", port=6379)
    while True:
        raw_msg = redis_client.brpop([ "contact" ], timeout=1)
        if raw_msg:
            (headers, req) = load_msg(raw_msg[1])
            if headers.has_key("reply_to"):
                tls  = threading.local()
                # set the headers on the thread local req_context
                req_context.headers = headers
                resp = server.call(req)
                redis_client.lpush(headers["reply_to"], dump_msg(headers, resp))
                req_context.headers = None

if __name__ == "__main__":
    # In a real system the router and worker would probably be
    # separate processes.  For this demo we're combining them
    # for simplicity
    worker_proc = threading.Thread(target=start_worker)
    worker_proc.daemon = True
    worker_proc.start()

    start_router()
import barrister
import urllib2
import sys

password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_mgr.add_password(None, 'http://localhost:7667/','johndoe','johnpass')
auth_handler = urllib2.HTTPBasicAuthHandler(password_mgr)

trans  = barrister.HttpTransport("http://localhost:7667/contact", 
                                 handlers=[auth_handler])
client = barrister.Client(trans)

contact = {
    "contactId" : "1234",
    "username"  : "johndoe",
    "firstName" : "Mary",
    "lastName"  : "Smith"
}

contactId = client.ContactService.put(contact)
print "put contact: %s" % contactId

contact2 = client.ContactService.get(contactId)
assert contact2 == contact

deleted = client.ContactService.remove(contactId)
assert deleted == True

# Try to be naughty and create a contact for another user
contact = {
    "contactId" : "12345",
    "username"  : "sally",
    "firstName" : "Ed",
    "lastName"  : "Henderson"
}

try:
    # should fail with error.code=4000
    client.ContactService.put(contact)
    
    print "Error! Server allowed us to act as another user"
    sys.exit(1)
except barrister.RpcException as e:
    assert e.code == 4000
    
print "OK"
put contact: 1234
OK