REST-сервер и тонкий клиент с использованием vibe-d
Эти ситуации мало чем отличаются, поэтому сначала мы рассмотрим простой случай:
- Есть какая-то модель:
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-реализацию.
Помимо этого аргументы и возвращаемые данные методов должны уметь [де]сериализовываться используя 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, Point c);
}
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
По сути
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.