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.
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 JSONdate_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
This example demonstrates two Barrister concepts:
interface Echo {
// if s == "err" 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
This example demonstrates the automatic type validation that Barrister performs including:
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
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:
reply_to
key used to route
the reply from the backend server, and a username
key that indicates the identity of the HTTP user.lpush
reply_to
key in Redis using brpop
with a 30 second timeoutwhile(true)
loop that poll Redis for requests using brpop
lpush
using the reply_to
header as the keyThis architecture is interesting because it allows us to:
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