REST server and thin client using vibe-d
- Tutorial
Good day, Habr! If you wanted to split your application into a server and a client, if you want to add an API to your vibe-site, or if you just have nothing to do.
These situations are not much different, so first we look at a simple case:
So, what do we need to do to make 2 - a rest server and a thin client from one ordinary application:
So, select the interface:
Decorators are
The server code will be written in the classic vibe in the static constructor of the module:
And finally, changes to the code using the model:
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:
In fact,
It is used like this:
If the return type is changed from
The request format will remain the same. The role
For appetizer, we implement the web version of our client. To do this, you need:
The diet template used in vibe is very similar to jade :
It looks, of course, so-so, but for an example of norms:

Changes in server code:
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
That's all! Sample code can be downloaded on github .
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.
Boring but important points
Cначала немного о модели. На момент написания vibe-d-0.7.30-beta.1 не поддерживал перегрузку функций (вообще), что, отчасти, логично, так как мы бы пытались вызвать метод не имея точной информации об аргументах, ибо мы передаём их по сети, vibe даже не знал бы к какому типу их приводить — нужно было бы выяснять это перебором, но тут есть тонкие моменты («5» можно привести и к int и к float, например).
Помимо этого аргументы и возвращаемые данные методов должны уметь [де]сериализовываться используя vibe.data.json. Это умеют все встроенные типы данны и прострые структуры (без private полей). Для реализации [де]сериализации можно объявить 2 метода
Это не касается возвращаемых интерфейсов, они так же работают через передачу аргументов по сети, но есть другой момент: метод, возвращающийэкземпляр класса, реализующего возвращаемый интерфейс объект, не должен принимать аргументов. Тут объяснить можно лишь одним: для регистрации rest-интерфейса используется экземпляр, а если функция принимает аргументы, то, возможно, с аргументами, имеющими init-значения создать экземпляр нельзя, а создать как-то надо для регистрации вложенного интерфейса.
Помимо этого аргументы и возвращаемые данные методов должны уметь [де]сериализовываться используя 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
-GET
method;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,
IPointCalculator
this is not an element of the collection, but the collection itself and the structure CollectionIndices
just indicate the presence of indexes used to obtain the elements of this collection. The underscore before _name
determines the format of the request to the method calc
as k calculator/:name/calc
, where it is :name
then passed as the first parameter to the method, and CollectionIndices
allows 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!IPointCalculator
to IPointCalculator
then little will change:...
writeln(m.calculator.calc("center", a, b, c));
...
The request format will remain the same. The role
Collection
in 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#area
It 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 - :name
nothing 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 .