Pull to refresh

REST-сервер и тонкий клиент с использованием vibe-d

Website developmentProgrammingD
Tutorial
Доброго времени суток, Хабр! Если Вам хотелось разделить своё приложение на сервер и клиент, если Вы хотите добавить API к своему vibe-сайту или если Вам просто нечего делать.

Эти ситуации мало чем отличаются, поэтому сначала мы рассмотрим простой случай:

  • Есть какая-то модель:

    module model;
    
    import std.math;
    
    struct Point { float x, y; }
    float sqr(float v) { return v * v; }
    
    float dist()(auto ref const(Point) a, auto ref const(Point) b)
    {
        return sqrt(sqr(a.x - b.x) + sqr(a.y - b.y));
    }
    
    class Model
    {
        float triangleAreaByLengths(float a, float b, float c)
        {
            auto p = (a + b + c) / 2;
            return sqrt(p * (p - a) * (p - b) * (p - c));
        }
    
        float triangleAreaByPoints(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);
        }
    }
    

  • Есть код, который её использует:

    
    import std.stdio;
    import model;
    
    void main()
    {
        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));
    }
    

Итак, что нам нужно сделать, чтобы из одного обычного приложения сделать 2 — rest-сервер и тонкого клиента:

  • Выделить интерфейс модели;
  • Создать код сервера;
  • Вместо настоящей модели создать rest-реализацию.

Скучные, но важные моменты
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-значения создать экземпляр нельзя, а создать как-то надо для регистрации вложенного интерфейса.

Итак выделим интерфейс:

interface IModel
{
    @method(HTTPMethod.GET)
    float triangleAreaByLengths(float a, float b, float c);

    @method(HTTPMethod.GET)
    float triangleAreaByPoints(Point a, Point b, Point c);
}

class Model : IModel
{
...
}

Декораторы @method(HTTPMethod.GET) необходимы для построения роутинга. Также есть способ обойтись без них — использовать соглашение именования методов (префиксы):

  • get,queryGET метод;
  • set, putPUT;
  • add, create, postPOST;
  • remove, erase, deleteDELETE;
  • update, patchPATCH.

Код сервера будет по классике vibe записан в статическом конструкторе модуля:


shared static this()
{
    auto router = new URLRouter;
    router.registerRestInterface(new Model); // создаём конкретную реализацию модели

    auto set = new HTTPServerSettings;
    set.port = 8080;
    set.bindAddresses = ["127.0.0.1"];

    listenHTTP(set, router);
}

И наконец изменения в коде, использующем модель:

...
    auto m = new RestInterfaceClient!IModel("http://127.0.0.1:8080/"); // тут мы уже используем интерфейс модели
...

Фреймворк сам реализует обращения к серверу и [де]сериализацию типов данных.

В итоге мы разделили приложение на сервер и клиент минимально изменив существующий код! Кстати говоря, выброшенные исключения пробрасываются vibe'ом в клиентское приложение, к сожалению, без сохранения типа исключения.

Рассмотрим более сложный случай — в модели имеются методы, возвращающие массивы несериализуемых объетов (классов). Тут без изменения существующего кода, к сожалению, не обойтись. Реализуем такую ситуацию в нашем примере.

Будем возвращать разные агрегаторы точек:

interface IPointCalculator
{
    struct CollectionIndices { string _name; } // необходимая структура для реализации коллекции

    @method(HTTPMethod.GET)
    Point calc(string _name, Point[] points...);
}


interface IModel
{
...
    @method(HTTPMethod.GET)
    Collection!IPointCalculator calculator();
}


class PointCalculator : IPointCalculator
{
    Point calc(string _name, Point[] points...)
    {
        import std.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);
        }
        else if (_name == "left")
            return points.fold!((a,b)=>a.x<b.x?a:b);
        else
            throw new Exception("Unknown calculator '" ~ _name ~ "'");
    }
}

class Model : IModel
{
    PointCalculator m_pcalc;
    this() { m_pcalc = new PointCalculator; }
...
    Collection!IPointCalculator calculator() { return Collection!IPointCalculator(m_pcalc); }
}

По сути IPointCalculator это не элемент коллекции, а сама коллекция и структура CollectionIndices как раз указывает на наличие индексов, используемых для получения элементов этой коллекции. Нижнее подчёркивание перед _name обуславливает формат запроса к методу calc как к calculator/:name/calc, где :name потом передаётся первым параметром в метод, а CollectionIndices позволяет такой запрос построить при реализации интерфейса с помощью new RestInterfaceClient!IModel.

Используется это так:

...
    writeln(m.calculator["center"].calc(a, b, c));
...

Если возвращаемый тип сменить с Collection!IPointCalculator на IPointCalculator то мало что поменяется:

...
    writeln(m.calculator.calc("center", a, b, c));
...

При этом формат запроса останется прежним. Не совсем понятна роль Collection в этой комбинации.

На закуску реализуем web версию нашего клиента. Для этого нужно:

  • Создать html-страницу с js-кодом, использующим наш rest API;
  • Немного добавить кода в серверную часть.

Шаблонизатор diet, используемый в vibe, очень похож на jade:

html
  head
    title Пример REST
    style.
      .label { display: inline-block; width: 20px; }
      input { width: 100px; }
    script(src = "model.js")
    script.
      function getPoints() {
        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}];
      }

      function calcTriangleArea() {
        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

Выглядит, конечно, так себе, но для примера норм:


Изменения в коде сервера:

...
    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");
...

Как мы можем заметить, vibe за нас генерирует js-код для обращения к нашему API.

В заключение можно отметить, что на данном этапе есть некоторые шероховатости, например неправильная генерация js кода для всех возвращаемых интерфейсов (забыли добавить this. для этих полей в js объекте) и для коллекций в частности (неправильная генерация url — :name ни на что не заменяется). Но эти шероховатости легко поправимы, думаю их исправят в ближайшем будущем.

На этом всё! Код примера можно скачать на github.
Tags:ddlangтонкий клиентweb-клиентrestrest-api
Hubs: Website development Programming D
Rating +22
Views 8.4k Add to bookmarks 32
Comments
Comments 10

Popular right now

Back end developer (Python/PHP, POST/GET API)
from 1,000 to 1,500 $InsightWhaleRemote job
Системный аналитик
from 60,000 ₽AgentAppСанкт-ПетербургRemote job
Frontend-разработчик
to 200,000 ₽WEBINARRemote job
Системный аналитик
from 150,000 to 250,000 ₽ArtezioСанкт-Петербург
Системный аналитик
to 250,000 ₽Sportmaster LabСанкт-ПетербургRemote job

Top of the last 24 hours