REST-сервер і тонкий клієнт з використанням vibe-d

Доброго часу доби, Хабр! Якщо Вам хотілося розділити свою програму на сервер і клієнт, якщо Ви хочете додати 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, c Point)
    {
    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-реалізацію.
Нудні, але важливі моментиСпочатку трохи про моделі. На момент написання vibe-d-0.7.30-beta.1 не підтримував перевантаження функцій (взагалі), що, від частини, логічно, так як ми намагалися викликати метод не маючи точної інформації про аргументи, бо ми передаємо їх по мережі, vibe навіть не знав би до якого типу їх приводити — потрібно було б з'ясовувати це перебором, але тут є тонкі моменти («5» можна привести і до int і до float, наприклад).

Крім цього аргументи та повертає дані методи повинні вміти [де]сериализовываться використовуючи vibe.data.json. Це вміють всі вбудовані типи даних і прострые структури (без private полів). Для реалізації [де]серіалізації можна оголосити 2 методу
static MyType frontJson(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, c Point);
}

class Model : IModel
{
...
}

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

  • get
    ,
    query
    GET
    метод;
  • set
    ,
    put
    PUT
    ;
  • add
    ,
    create
    ,
    post
    POST
    ;
  • remove
    ,
    erase
    ,
    delete
    DELETE
    ;
  • update
    ,
    patch
    PATCH
    .
Код сервера буде по класиці 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® {
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.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.