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:

  • 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 метода static MyType fromJson(Json data) и Json toJson() const, где описывается процесс перевода сложных структур в Json тип, пример.

Это не касается возвращаемых интерфейсов, они так же работают через передачу аргументов по сети, но есть другой момент: метод, возвращающий экземпляр класса, реализующего возвращаемый интерфейс объект, не должен принимать аргументов. Тут объяснить можно лишь одним: для регистрации rest-интерфейса используется экземпляр, а если функция принимает аргументы, то, возможно, с аргументами, имеющими init-значения создать экземпляр нельзя, а создать как-то надо для регистрации вложенного интерфейса.

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#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 - :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 .

Also popular now: