Пришлось на некоторое время уйти в изучение документов HL7. Потом, кстати, и на Хабре обнаружил неплохой цикл статей об этой технологии от Wayfarer15. Попутно выяснил, что самым последним активно разрабатываемым стандартом в этой области является Fast Healthcare Interoperability Resources (далее FHIR). В основе FHIR лежит технология REST и обмен XML/JSON документами через REST ресурсы.
Как это применимо к Bagri? Оказалось, что вполне: примерно с месяц назад в Bagri добавилась поддержка REST, а также возможность динамического определения ресурсов REST в модулях XQuery с помощью аннотаций RESTXQ. Т.е. любой ресурс FHIR можно динамически создать и опубликовать, даже без рестарта серверов Bagri. Давайте попробуем?
Создаём прототип FHIR-сервера за 45 минут..
Для этого нам понадобятся:
- последняя версия Bagri, развёрнутая на вашем компьютере, ее можно скачать здесь;
- тестовый набор данных FHIR, доступен по данной ссылке;
- базовые знания языка XQuery, с помощью которого мы будем разрабатывать наш прототип:
Создадим новую схему в конфигурационном файле Bagri (<bagri_home>/config/config.xml), назовём её FHIR.
<schema name="FHIR" active="true">
<version>1</version>
<createdAt>2016-11-09T23:14:40.096+03:00</createdAt>
<createdBy>admin</createdBy>
<description>FHIR: schema for FHIR XML demo</description>
<properties>
<!-- диапазон портов для серверов схемы -->
<entry name="xdm.schema.ports.first">11000</entry>
<entry name="xdm.schema.ports.last">11100</entry>
<entry name="xdm.schema.members">localhost</entry>
<entry name="xdm.schema.thread.pool">16</entry>
<entry name="xdm.schema.query.cache">true</entry>
<!-- путь к данным схемы, которые хранятся в файлах XML -->
<entry name="xdm.schema.store.data.path">../data/fhir/xml</entry>
<entry name="xdm.schema.store.type">File</entry>
<entry name="xdm.schema.format.default">XML</entry>
<entry name="xdm.schema.partition.count">271</entry>
<entry name="xdm.schema.population.size">1</entry>
<entry name="xdm.schema.buffer.size">64</entry>
<entry name="xdm.schema.store.enabled">true</entry>
<entry name="xdm.schema.data.cache">NEVER</entry>
<entry name="xdm.schema.data.stats.enabled">true</entry>
<entry name="xdm.schema.trans.backup.async">0</entry>
<entry name="xdm.schema.trans.backup.sync">1</entry>
<entry name="xdm.schema.trans.backup.read">false</entry>
<entry name="xdm.schema.data.backup.read">false</entry>
<entry name="xdm.schema.data.backup.async">1</entry>
<entry name="xdm.schema.data.backup.sync">0</entry>
<entry name="xdm.schema.dict.backup.sync">0</entry>
<entry name="xdm.schema.dict.backup.async">1</entry>
<entry name="xdm.schema.dict.backup.read">true</entry>
<entry name="xdm.schema.query.backup.async">0</entry>
<entry name="xdm.schema.query.backup.sync">0</entry>
<entry name="xdm.schema.query.backup.read">true</entry>
<entry name="xdm.schema.transaction.timeout">60000</entry>
<entry name="xdm.schema.health.threshold.low">25</entry>
<entry name="xdm.schema.health.threshold.high">0</entry>
<entry name="xdm.schema.store.tx.buffer.size">2048</entry>
<entry name="xdm.schema.population.buffer.size">1000000</entry>
<entry name="xdm.schema.query.parallel">true</entry>
<entry name="xdm.schema.partition.pool">32</entry>
<entry name="xqj.schema.baseUri">file:/../data/fhir/xml/</entry>
<entry name="xqj.schema.orderingMode">2</entry>
<entry name="xqj.schema.queryLanguageTypeAndVersion">1</entry>
<entry name="xqj.schema.bindingMode">0</entry>
<entry name="xqj.schema.boundarySpacePolicy">1</entry>
<entry name="xqj.schema.scrollability">1</entry>
<entry name="xqj.schema.holdability">2</entry>
<entry name="xqj.schema.copyNamespacesModePreserve">1</entry>
<entry name="xqj.schema.queryTimeout">0</entry>
<entry name="xqj.schema.defaultFunctionNamespace">http://www.w3.org/2005/xpath-functions</entry>
<entry name="xqj.schema.defaultElementTypeNamespace">http://www.w3.org/2001/XMLSchema</entry>
<entry name="xqj.schema.copyNamespacesModeInherit">1</entry>
<entry name="xqj.schema.defaultOrderForEmptySequences">2</entry>
<entry name="xqj.schema.defaultCollationUri">http://www.w3.org/2005/xpath-functions/collation/codepoint</entry>
<entry name="xqj.schema.constructionMode">1</entry>
</properties>
<!-- коллекция документов типа Patient -->
<collections>
<collection id="1" name="Patients">
<version>1</version>
<createdAt>2016-11-09T23:14:40.096+03:00</createdAt>
<createdBy>admin</createdBy>
<docType>/{http://hl7.org/fhir}Patient</docType>
<description>All patient documents</description>
<enabled>true</enabled>
</collection>
</collections>
<fragments/>
<!-- индекс по пути /Patient/id/@value для ускорения поиска по id пациента -->
<indexes>
<index name="idx_patient_id">
<version>1</version>
<createdAt>2016-11-09T23:14:40.096+03:00</createdAt>
<createdBy>admin</createdBy>
<docType>/{http://hl7.org/fhir}Patient</docType>
<path>/{http://hl7.org/fhir}Patient/{http://hl7.org/fhir}id/@value</path>
<dataType xmlns:xs="http://www.w3.org/2001/XMLSchema">xs:string</dataType>
<caseSensitive>true</caseSensitive>
<range>false</range>
<unique>true</unique>
<description>Patient id</description>
<enabled>true</enabled>
</index>
</indexes>
<resources>
<!-- базовый ресурс, предоставляющий метаданные, будет доступен по базовому адресу http://localhost:3030/ -->
<resource name="common">
<version>1</version>
<createdAt>2016-11-09T23:14:40.096+03:00</createdAt>
<createdBy>admin</createdBy>
<path>/</path>
<module>common_module</module>
<description>FHIR Conformance resource exposed via REST</description>
<enabled>true</enabled>
</resource>
<!-- ресурс пациентов, будет доступен по адресу http://localhost:3030/Patient -->
<resource name="patient">
<version>1</version>
<createdAt>2016-11-09T23:14:40.096+03:00</createdAt>
<createdBy>admin</createdBy>
<path>/Patient</path>
<module>patient_module</module>
<description>FHIR Patient resource exposed via REST</description>
<enabled>true</enabled>
</resource>
</resources>
<triggers/>
</schema>
Тестовые данные распакуем на локальный диск в директорию <bagri_home>/data/fhir/xml. Про работу с JSON документами в Bagri я писал в предыдущей статье, так что в данном примере для экономии места я покажу только работу с данными в формате XML.
На момент написания статьи спецификация FHIR определяла 110 стандартных ресурсов, доступ к которым может предоставляться сервером. Часть из них является служебными и служит для предоставления информации о самой системе, а остальная часть — это прикладные ресурсы, которые выполняют работу с медицинскими данными. Служебный ресурс Conformance является обязательным для реализации и предоставляет сведения о доступном функционале системы. Наличие или отсутствие остальных ресурсов и их поведение определяется тем, что мы задекларируем в Conformance.
Прикладные ресурсы, согласно спецификации FHIR, могут публиковать следующие методы:
Операции на уровне ресурсов:
- read — получение текущего состояния заданного идентификатором ресурса
- vread — получение состояния конкретной версии заданного ресурса
- update — обновление заданного ресурса
- delete — удаление заданного ресурса
- history — получение истории обновлений заданного ресурса
Операции на уровне типа ресурса:
- create — создание нового ресурса
- search — поиск среди ресурсов одного типа по разным критериям
- history — получение истории обновлений по указанному типу ресурса
В показательных целях мы реализуем 2 ресурса: уже обозначенный Conformance и прикладной ресурс Patient. Conformance определит, какой функционал будет доступен клиентам ресурса Patient.
Ниже по тексту будет много смайликов. Не пугайтесь, это издержки синтаксиса XQuery :).
Реализация Conformance для нашего прототипа выглядит довольно просто: cоздадим новый модуль XQuery <bagri_home>/data/fhir/common_module.xq. В заголовке объявим используемую версию языка, пространство имен модуля и пространства имен используемых внешних схем:
xquery version "3.1";
module namespace conf = "http://hl7.org/fhir";
declare namespace rest = "http://www.expath.org/restxq";
declare
%rest:GET (: определяет метод HTTP, через который ресурс будет доступен :)
%rest:path("/metadata") (: определяет путь доступа к ресурсу, относительно базового URL:)
%rest:produces("application/fhir+xml") (: возвращает данные в формате XML :)
%rest:query-param("_format", "{$format}") (: принимает один необязательный параметр _format :)
function conf:get-conformance($format as xs:string?) as item() {
if (exists($format) and not ($format = ("application/xml", "application/fhir+xml"))) then
"The endpoint produce response in application/fhir+xml format, but [" || $format || "] specified"
else
<CapabilityStatement xmlns="http://hl7.org/fhir">
<id value="FhirServer"/>
<url value="http://localhost:3030/metadata"/>
<version value="1.1-SNAPSHOT"/>
<name value="Bagri FHIR Server Conformance Statement"/>
<status value="draft"/>
<experimental value="true"/>
<date value="{fn:current-dateTime()}"/>
<publisher value="Bagri Project"/>
<contact>
<name value="Maxim Petrov"/>
<telecom>
<system value="other"/>
<value value="@mfalifax"/>
<use value="work"/>
</telecom>
</contact>
<description value="Standard Conformance Statement for the open source Reference FHIR Server provided by Bagri"/>
<kind value="instance"/>
<instantiates value="http://hl7.org/fhir/Conformance/terminology-server"/>
<software>
<name value="Reference Server"/>
<version value="1.1-SNAPSHOT"/>
<releaseDate value="2016-11-10"/>
</software>
<implementation>
<description value="FHIR Server running at http://localhost:3030/"/>
<url value="http://localhost:3030/"/>
</implementation>
<fhirVersion value="1.7.0"/>
<acceptUnknown value="both"/>
<format value="application/fhir+xml"/>
<rest>
<mode value="server"/>
<!-- перечисление типов ресурсов, публикуемых сервером -->
<resource>
<type value="Patient"/>
<profile>
<reference value="http://fhir3.healthintersections.com.au/open/StructureDefinition/patient"/>
</profile>
<!-- перечисление методов, реализованные ресурсом -->
<interaction>
<code value="read"/>
</interaction>
<interaction>
<code value="vread"/>
</interaction>
<interaction>
<code value="search-type"/>
</interaction>
<interaction>
<code value="update"/>
</interaction>
<interaction>
<code value="create"/>
</interaction>
<interaction>
<code value="delete"/>
</interaction>
<readHistory value="true"/>
<updateCreate value="true"/>
<!-- параметры, доступные к использованию при поиске методом search -->
<searchParam>
<name value="birthdate"/>
<definition value="http://hl7.org/fhir/SearchParameter/Patient-birthdate"/>
<type value="date"/>
<documentation value="The patient's date of birth"/>
<!-- поиск по условию equals -->
<modifier value="exact"/>
</searchParam>
<searchParam>
<name value="gender"/>
<definition value="http://hl7.org/fhir/SearchParameter/Patient-gender"/>
<type value="token"/>
<documentation value="Gender of the patient"/>
<modifier value="exact"/>
</searchParam>
<searchParam>
<name value="identifier"/>
<definition value="http://hl7.org/fhir/SearchParameter/Patient-identifier"/>
<type value="token"/>
<documentation value="A patient identifier"/>
<!-- поиск по условию contains -->
<modifier value="contains"/>
</searchParam>
<searchParam>
<name value="name"/>
<definition value="http://hl7.org/fhir/SearchParameter/Patient-name"/>
<type value="string"/>
<documentation value="A server defined search that may match any of the string fields in the HumanName, including family, give, prefix, suffix and/or text"/>
<modifier value="contains"/>
</searchParam>
<searchParam>
<name value="telecom"/>
<definition value="http://hl7.org/fhir/SearchParameter/Patient-telecom"/>
<type value="token"/>
<documentation value="The value in any kind of telecom details of the patient"/>
<modifier value="contains"/>
</searchParam>
</resource>
</rest>
</CapabilityStatement>
};
Собственно, это единственный метод, из которого состоит ресурс Conformance. Его задача — определить другие точки доступа к системе и параметры, которыми можно пользоваться в этих взаимодействиях.
Для прикладного ресурса Patient создадим другой модуль XQuery:
<bagri_home>/data/fhir/patient_module.xq. Так же в заголовке объявим используемые пространства имен:
module namespace fhir = "http://hl7.org/fhir/patient";
declare namespace http = "http://www.expath.org/http";
declare namespace rest = "http://www.expath.org/restxq";
declare namespace bgdm = "http://bagridb.com/bagri-xdm";
declare namespace p = "http://hl7.org/fhir";
Реализуем метод read:
declare
%rest:GET (: определяет метод HTTP, через который ресурс будет доступен :)
%rest:path("/{id}") (: определяет путь доступа к ресурсу; id - шаблонный параметр пути :)
%rest:produces("application/fhir+xml") (: устанавливает формат возвращаемых данных :)
function fhir:get-patient-by-id($id as xs:string) as element()? {
collection("Patients")/p:Patient[p:id/@value = $id]
};
Выглядит, на мой взгляд, весьма привлекательно: реализация требуемого функционала всего в одну строку! Но, как известно, дьявол кроется в деталях. Помимо основного поведения, спецификация FHIR определяет так же многочисленные дополнительные ситуации и статусы и заголовки HTTP, которые сервис обязан возвращать в таких случаях. Попробуем переписать показанный выше метод read с учётом расширенных требований:
declare
%rest:GET
%rest:path("/{id}")
%rest:produces("application/fhir+xml")
function fhir:get-patient-by-id($id as xs:string) as element()* {
let $itr := collection("Patients")/p:Patient[p:id/@value = $id]
return
if ($itr) then
(<rest:response>
<http:response status="200">
(: запрашиваемый ресурс имеет версию? :)
{if ($itr/p:meta/p:versionId/@value) then (
(: заголовок ETag должен содержать номер версии найденного ресурса Patient :)
<http:header name="ETag" value="W/"{$itr/p:meta/p:versionId/@value}""/>,
(: заголовок Content-Location должен содержать адрес, по которому доступна последняя версия ресурса :)
<http:header name="Content-Location" value="/Patient/{$id}/_history/{$itr/p:meta/p:versionId/@value}"/>
) else (
(: иначе заголовок Content-Location должен содержать базовый адрес ресурса :)
<http:header name="Content-Location" value="/Patient/{$id}"/>
)}
(: заголовок Last-Modified должен содержать дату/время последней модификации ресурса :)
<http:header name="Last-Modified" value="{format-dateTime(xs:dateTime($itr/p:meta/p:lastUpdated/@value), "[FNn,3-3], [D] [MNn,3-3] [Y] [H01]:[m01]:[s01] [z,*-6]")}"/>
</http:response>
</rest:response>, $itr)
else
(: возвращаем статус 404 если пациент с заданным id не найден :)
<rest:response>
<http:response status="404" message="Patient with id={$id} was not found."/>
</rest:response>
};
Для указания статуса и заголовков ответа HTTP используется структура http:response, которая должна передаваться в первом элементе последовательности возвращаемых данных. Так же обратите внимание, что пришлось изменить тип возвращаемых данных с element()? на element()*, чтобы передать эту служебную информацию на REST сервер.
Конечно, такая полная реализация требований спецификации получается гораздо более многословной. Но не берусь сказать, с помощью какого языка/технологии можно выполнить требования FHIR компактнее. С другой стороны, сильно привлекают возможности XQuery по работе с XML и с последовательностями данных.
Ниже я уже не буду отвлекаться на обработку всех возможных дополнительных сценариев, в примере выше было показано, как возвращать на сервер дополнительные статусы и заголовки HTTP.
Базовая реализация метода vread выглядит очень похоже:
declare
%rest:GET
%rest:path("/{id}/_history/{vid}") (: кроме идентификатора здесь в качестве шаблона пути также используется номер версии :)
%rest:produces("application/fhir+xml")
function fhir:get-patient-by-id-version($id as xs:string, $vid as xs:string) as element()? {
collection("Patients")/p:Patient[p:id/@value = $id and p:meta/p:versionId/@value = $vid]
};
Следующий метод — search. В ресурсе Conformance мы указали, что можем выполнять поиск пациентов по 5 параметрам: name, birthday, gender, identifier и telecom. Так же мы указали как именно используется параметр поиска, через элемент modifier, который может принимать следующие значения: missing | exact | contains | not | text | in | not-in | below | above | type. Их описание и соответствующее поведение поисковой системы можно посмотреть здесь.
declare
%rest:GET
%rest:produces("application/fhir+xml")
%rest:query-param("identifier", "{$identifier}") (: параметры поиска передаём в строке :)
%rest:query-param("birthdate", "{$birthdate}") (: запроса http; все они не обязательные :)
%rest:query-param("gender", "{$gender}")
%rest:query-param("name", "{$name}")
%rest:query-param("telecom", "{$telecom}")
function fhir:search-patients($identifier as xs:string?, $birthdate as xs:date?, $gender as xs:string?, $name as xs:string?, $telecom as xs:string?) as element()* {
(: получим набор результатов (пациентов), удовлетворяющих условиям поиска :)
let $itr := collection("Patients")/p:Patient[
(not(exists($gender)) or p:gender/@value = $gender)
and (not(exists($birthdate)) or p:birthDate/@value = $birthdate)
and (not(exists($name)) or contains(data(p:text), $name))
and (not(exists($identifier)) or contains(p:identifier/p:value/@value, $identifier))
and (not(exists($telecom)) or contains(string-join(p:telecom/p:value/@value, " "), $telecom))]
(: возвращаем результаты внутри контейнера Bundle :)
return
<Bundle xmlns="http://hl7.org/fhir">
<id value="{bgdm:get-uuid()}" /> (: сгенерируем уникальный bundle ID :)
<meta>
<lastUpdated value="{current-dateTime()}" />
</meta>
<type value="searchset" />
<total value="{count($itr)}" />
<link>
<relation value="self" />
<url value="http://bagridb.com/Patient/search?name=test" />
</link>
{for $ptn in $itr
return
<entry>
<resource>{$ptn}</resource>
</entry>
}
</Bundle>
};
сreate — создание нового ресурса Patient, либо новой версии уже имеющегося ресурса.
declare
%rest:POST (: создание нового ресурса осуществляется методом POST :)
%rest:consumes("application/fhir+xml") (: ожидаем получить полное состояние ресурса в теле запроса в формате XML :)
%rest:produces("application/fhir+xml") (: новое состояние ресурса вернем клиенту в том же формате :)
function fhir:create-patient($content as xs:string) as element()? {
let $doc := parse-xml($content) (: распарсим входную строку в документ XML, заодно и провалидируем его :)
let $uri := xs:string($doc/p:Patient/p:id/@value) || ".xml" (: сформируем uri нового ресурса :)
let $uri := bgdm:store-document(xs:anyURI($uri), $content, ()) (: сохраним документ и получим в ответ его uri, хотя он не должен отличаться от сформированного нами 2мя строками выше :)
let $content := bgdm:get-document-content($uri) (: а вот состояние ресурса, в соответствие со спецификацией, может отличаться от полученного на вход, например система могла заполнить некоторые пропущенные поля их значениями по умолчанию :)
let $doc := parse-xml($content)
return $doc/p:Patient
};
update — создание новой версии имеющегося ресурса Patient, либо создание нового ресурса, если пациент с заданным идентификатором ещё не зарегистрирован в системе.
declare
%rest:PUT (: изменение существующего ресурса осуществляется методом PUT :)
%rest:path("/{id}"). (: изменяем ресурс соответствующий заданному шаблонному параметру :)
%rest:consumes("application/fhir+xml")
%rest:produces("application/fhir+xml")
function fhir:update-patient($id as xs:string, $content as xs:string) as element()? {
for $uri in fhir:get-patient-uri($id) (: используем утилитную функцию чтобы не дублировать код :)
let $uri := bgdm:store-document($uri, $content, ())
let $content := bgdm:get-document-content($uri, ())
let $doc := parse-xml($content)
return $doc/p:Patient
};
delete — удаление зарегистрированного в системе ресурса Patient.
declare
%rest:DELETE (: удаление ресурса, естественно, с помощью DELETE :)
%rest:path("/{id}")
function fhir:delete-patient($id as xs:string) as item()? {
for $uri in fhir:get-patient-uri($id)
return bgdm:remove-document($uri) (: удалить соответствующий ресурсу документ :)
};
Вспомогательный метод, используемый из функций обновления и удаления:
declare
%private
function fhir:get-patient-uri($id as xs:string) as xs:anyURI? {
(: сформируем динамический запрос XQuery :)
let $query :=
' declare namespace p = "http://hl7.org/fhir";
declare variable $id external;
for $ptn in fn:collection("Patients")/p:Patient
where $ptn/p:id/@value = $id
return $ptn'
(: выполнив его, получим в ответ uri документа, удовлетворяющего условиям запроса :)
let $uri := bgdm:query-document-uris($query, ("id", $id), ())
return xs:anyURI($uri)
};
Как видим, в реализации логики управления ресурсами используются функции XQuery, предоставляемые библиотеками Bagri. Вот их краткое описание:
bgdm:get-uuid() as xs:string - сгенерировать уникальный идентификатор uuid
bgdm:query-document-uris(xs:string, xs:anyType*, xs:anyAtomicType*) as xs:string* - вернуть uri документов, которые попадают в динамическую выборку XQuery
bgdm:store-document(xs:anyURI, xs:string, xs:anyAtomicType*) as xs:anyURI - зарегистрировать в системе новый документ, либо новую версию имеющегося документа
bgdm:get-document-content(xs:anyURI) as xs:string* - вернуть текстовое содержимое документа
bgdm:remove-document(xs:anyURI) as xs:anyURI - удалить документ
На этом реализация серверных модулей, выполняющихся логику управления ресурсами FHIR, закончена. Думаю, в 45 минут мы уложились :). В следующей части статьи я хотел бы показать, как запустить разработанные выше ресурсы и оттестировать их. Ну и, конечно, было бы очень интересно послушать, что многоуважаемая аудитория Хабра думает по этому поводу.
Комментарии (8)
aleks_public
29.11.2016 20:30Спасибо за статью. К сожалению по первой ссылке получаю «Мероприятие не найдено!»
mhalifax
30.11.2016 11:51Здравствуйте, да к сожалению представить доклад на конференцию я не успел, буду пытаться сделать его к апрелю, ссылку исправил на апрельскую конференцию, спасибо, что заметили
Wayfarer15
01.12.2016 00:55Я понимаю, что идея статьи показать/рассказать про Bagri, а не HL7 FHIR. Но вот одна из проблем с FHIR как раз не в том, чтобы нарисовать сервер за 45 минут (грубо говоря, сотворить REST enabled server за 45 минут можно кучей самых различных средств, например, на Mongo, на fhirbase и прочее-прочее), а в том, что указываем профайл для ресурса Patient — http://fhir3.healthintersections.com.au/open/StructureDefinition/patient — и потом про это забываем. В результате наша база хранить ресурсы на разных профайлах, которые потенциально могут быть несовместимы. В частности, вышеуказанный профайл имеет расширения (extensions) для ресурса Patient.
Тем не менее, как средство длябитьяэкспериментов с БД, FHIR вполне пойдёт.
: )))mhalifax
01.12.2016 12:24Здравствуйте, не очень понял, в чем проблема. Ок, у нас есть ресурсы пациентов в разных профилях, их состояние хранится в разных XML/JSON документах. Если есть требования обрабатывать разные профили единообразно — делаем так. Если нет — пишем код на xquery с учётом этой разницы. Bagri, на мой взгляд, хорошо подходит для этой задачи, потому что абсолютно schema-less, и не требует каких-либо предварительных знаний о структуре хранимых документов. По Mongo я не эксперт, но fhir строить на нем я бы не стал, там поддержки XML нет, насколько я знаю…
Wayfarer15
01.12.2016 20:14Проблема в том, что данные потенциально могут быть потеряны поскольку одна система отправляет больше данных, чем другая способна распознать. По этому поводу советую глянуть секцию 2.15 Implementation Guides в FHIR спеке.
FHIR поддерживает, как ты сам указыавешь, XML и JSON форматы, поэтому все эти NoSQL базы данных также могут быть использованы.
Опять же, всё это мало относится к статье как презентации базы Bagri.
dsukhoroslov
Максим, спасибо за статью. Похоже ты проект с гитхаба собирал, да? Я пре-релиз с REST-сервером еще выложить то не успел :). Ну ок, сегодня тогда займусь этим.
mhalifax
Здравствуйте, да я собирал проект именно из Гитхаба. К сожалению не знал что этого функционала нет в релизе на сайте. Будем ждать его там.
dsukhoroslov
Собрал и выложил пре-релиз с новым функционалом, вот здесь можно его взять: https://github.com/dsukhoroslov/bagri/releases/tag/v1.1.0-EA1.
Swagger прилично утяжелил сборку, посмотрим, насколько он окажется полезен…