REST server and thin client using vibe-d
- Tutorial
These situations are not much different, so first we look at a simple case:
- There is some kind of model:
module model; importstd.math; structPoint {float x, y; } floatsqr(float v){ return v * v; } floatdist()(auto ref const(Point) a, auto ref const(Point) b){ returnsqrt(sqr(a.x - b.x) + sqr(a.y - b.y)); } classModel {floattriangleAreaByLengths(float a, float b, float c){ auto p = (a + b + c) / 2; returnsqrt(p * (p - a) * (p - b) * (p - c)); } floattriangleAreaByPoints(Point a, Point b, Point c){ auto ab = dist(a, b); auto ac = dist(a, c); auto bc = dist(b, c); return triangleAreaByLengths(ab, ac, bc); } } - There is a code that uses it:
importstd.stdio; import model; voidmain(){ auto a = Point(1, 2); auto b = Point(3, 4); auto c = Point(4, 1); auto m = new Model; writeln(m.triangleAreaByPoints(a, b, c)); }
So, what do we need to do to make 2 - a rest server and a thin client from one ordinary application:
- Highlight the model interface;
- Create server code;
- Instead of a real model, create a rest implementation.
Помимо этого аргументы и возвращаемые данные методов должны уметь [де]сериализовываться используя vibe.data.json. Это умеют все встроенные типы данны и прострые структуры (без private полей). Для реализации [де]сериализации можно объявить 2 метода
static MyType fromJson(Json data) и Json toJson() const, где описывается процесс перевода сложных структур в Json тип, пример.Это не касается возвращаемых интерфейсов, они так же работают через передачу аргументов по сети, но есть другой момент: метод, возвращающий
So, select the interface:
interfaceIModel
{
@method(HTTPMethod.GET)
floattriangleAreaByLengths(float a, float b, float c);
@method(HTTPMethod.GET)
floattriangleAreaByPoints(Point a, Point b, Point c);
}
classModel : IModel
{
...
}Decorators are
@method(HTTPMethod.GET)needed to build routing. There is also a way to do without them - use the method naming convention (prefixes):get,query-GETmethod;set,put-PUT;add,create,post-POST;remove,erase,delete-DELETE;update,patch-PATCH.
The server code will be written in the classic vibe in the static constructor of the module:
shared staticthis(){
auto router = new URLRouter;
router.registerRestInterface(new Model); // создаём конкретную реализацию моделиautoset = new HTTPServerSettings;
set.port = 8080;
set.bindAddresses = ["127.0.0.1"];
listenHTTP(set, router);
}And finally, changes to the code using the model:
...
auto m = new RestInterfaceClient!IModel("http://127.0.0.1:8080/"); // тут мы уже используем интерфейс модели
...The framework itself implements calls to the server and [de] serialization of data types.
As a result, we divided the application into a server and a client minimally changing the existing code! By the way, thrown exceptions are thrown by vibe into the client application, unfortunately, without saving the type of exception.
Consider a more complex case - the model has methods that return arrays of non-serializable objects (classes). Unfortunately, one cannot do without changing the existing code. We realize this situation in our example.
We will return different point aggregators:
interface IPointCalculator
{
structCollectionIndices {string _name; } // необходимая структура для реализации коллекции
@method(HTTPMethod.GET)
Point calc(string _name, Point[] points...);
}
interface IModel
{
...
@method(HTTPMethod.GET)
Collection!IPointCalculator calculator();
}
classPointCalculator : IPointCalculator
{
Point calc(string _name, Point[] points...){
importstd.algorithm;
if (_name == "center")
{
auto n = points.length;
float cx = points.map!"a.x".sum / n;
float cy = points.map!"a.y".sum / n;
return Point(cx, cy);
}
elseif (_name == "left")
return points.fold!((a,b)=>a.x<b.x?a:b);
elsethrownew Exception("Unknown calculator '" ~ _name ~ "'");
}
}
classModel : IModel
{
PointCalculator m_pcalc;
this() { m_pcalc = new PointCalculator; }
...
Collection!IPointCalculator calculator(){ return Collection!IPointCalculator(m_pcalc); }
}In fact,
IPointCalculatorthis is not an element of the collection, but the collection itself and the structure CollectionIndicesjust indicate the presence of indexes used to obtain the elements of this collection. The underscore before _namedetermines the format of the request to the method calcas k calculator/:name/calc, where it is :namethen passed as the first parameter to the method, and CollectionIndicesallows you to build such a request when implementing the interface with new RestInterfaceClient!IModel. It is used like this:
...
writeln(m.calculator["center"].calc(a, b, c));
...If the return type is changed from
Collection!IPointCalculatorto IPointCalculatorthen little will change:...
writeln(m.calculator.calc("center", a, b, c));
...The request format will remain the same. The role
Collectionin this combination is not entirely clear . For appetizer, we implement the web version of our client. To do this, you need:
- Create an html page with js code using our rest API;
- Add a little code to the server side.
The diet template used in vibe is very similar to jade :
html
head
title Пример REST
style.
.label { display: inline-block; width: 20px; }
input { width: 100px; }
script(src = "model.js")
script.
functiongetPoints() {
var ax = parseFloat(document.getElementById('ax').value);
var ay = parseFloat(document.getElementById('ay').value);
var bx = parseFloat(document.getElementById('bx').value);
var by = parseFloat(document.getElementById('by').value);
var cx = parseFloat(document.getElementById('cx').value);
var cy = parseFloat(document.getElementById('cy').value);
return [{x:ax, y:ay}, {x:bx, y:by}, {x:cx, y:cy}];
}
functioncalcTriangleArea() {
var p = getPoints();
IModel.triangleAreaByPoints(p[0], p[1], p[2], function(r) {
document.getElementById('area').innerHTML = r;
});
}
body
h1 Расчёт площади треугольника
div
div.label A:
input#ax(placehoder="a.x",value="1")
input#ay(placehoder="a.y",value="2")
div
div.label B:
input#bx(placehoder="b.x",value="2")
input#by(placehoder="b.y",value="1")
div
div.label C:
input#cx(placehoder="c.x",value="0")
input#cy(placehoder="c.y",value="0")
div
button(onclick="calcTriangleArea()") Расчитать
p Площадь:
span#areaIt looks, of course, so-so, but for an example of norms:

Changes in server code:
...
auto restset = new RestInterfaceSettings;
restset.baseURL = URL("http://127.0.0.1:8080/");
router.get("/model.js", serveRestJSClient!IModel(restset));
router.get("/", staticTemplate!"index.dt");
...As we can see, vibe generates js code for us to access our API.
In conclusion, it can be noted that at this stage there are some roughnesses, for example, incorrect generation of js code for all returned interfaces (forgot to add
this.for these fields in the js object) and for collections in particular (incorrect generation of url - :namenothing is replaced). But these roughnesses are easily fixable, I think they will be fixed in the near future . That's all! Sample code can be downloaded on github .