Доброго времени суток!
Если Вам хотелось разделить своё приложение на сервер и клиент, если Вы хотите добавить 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-реализацию
Помимо этого аргументы и возвращаемые данные методов должны уметь [де]сериализовываться используя 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<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.
Автор: deviator