How-to: Взаимодействие через HTTP-протокол
Пример 1
Условие
Задан некоторый набор городов, привязанных к странам.
CLASS Country 'Страна';
id 'Код' = DATA STRING[20] (Country) IN id;
name 'Имя' = DATA ISTRING[100] (Country) IN id;
country (STRING[20] id) = GROUP AGGR Country c BY id(c);
CLASS City 'Город';
name 'Имя' = DATA ISTRING[100] (City) IN id;
country 'Страна' = DATA Country (City);
nameCountry 'Страна' (City c) = name(country(c));
FORM cities 'Города'
OBJECTS c = City
PROPERTIES(c) name, nameCountry, NEW, DELETE
;
NAVIGATOR {
NEW cities;
}
Нужно отправить на определенный url HTTP-запрос на добавление города в формате JSON.
Решение
postCity 'Отправить' (City c) {
EXPORT JSON FROM countryId = id(country(c)), name = name(c);
LOCAL result = FILE();
EXTERNAL HTTP 'http://localhost:7651/exec?action=Location.createCity' PARAMS exportFile() TO result;
LOCAL code = STRING[10]();
LOCAL message = STRING[100]();
IMPORT JSON FROM result() TO() code, message;
IF NOT code() = '0' THEN {
MESSAGE 'Ошибка: ' + message();
}
}
EXTEND FORM cities
PROPERTIES(c) postCity
;
Оператор EXPORT создаст JSON в формате FILE и сохранит его в свойство exportFile. Пример сформированного файла:
{"countryId":"123","name":"San Francisco"}
Дальше вызывается оператор EXTERNAL, который делает запрос на предопределенный url, передавая туда в качестве Body содержимое сформированного файла. В данном случае, так как свойство в блоке FROM имеет тип JSON, то в качестве типа контента будет использоваться application/json. В url'е закодированы <пространство имен>.<имя свойства>. В данном случае пространство именем модуля вызываемого свойства createCity является Location. Все параметры передаются по порядку с идентификатором p. Ответ, который будет получен от сервера, будет записан в свойство result. Предположим, что ответ получен в формате JSON и имеет один из следующих видов:
{"code":"0","message":"OK"}
{"code":"1","message":"Некорректный код страны"}
Ответ разбирается при помощи оператора IMPORT, который раскладывает соответствующие параметры в свойства code и message соответственно. В случае ошибки пользователю выдается сообщение с текстом ошибки.
Пример 2
Условие
Аналогично Примеру 1.
Нужно принять соответствующий HTTP-запрос и создать новый город в базе данных с параметрами запроса.
Решение
createCity (FILE f) {
LOCAL cy = STRING[20] ();
LOCAL ne = STRING[100] ();
IMPORT JSON FROM f AS FILE TO() cy = countryId, ne = name;
IF NOT country(cy()) THEN
RETURN JSON FROM code = '1', message = 'Некорректный код страны';
NEW c = City {
name(c) <- ne();
country(c) <- country(cy());
APPLY;
}
RETURN JSON FROM code = '0', message = 'OK';
}
Так как свойство имеет название createCity и расположено в модуле с пространством имен Location, то url, на котором будет принят запрос, имеет следующий вид:
http://localhost:7651/exec?action=Location.createCity
Body HTTP-запроса будет передан параметром с типом FILE. В локальные свойства cy и ne, считываются значения из параметров countryId и name соответственно.
Если не находится страна с соответствующим кодом, то оператор RETURN прерывает выполнение и возвращает JSON с содержанием, описанным в предыдущем примере. Этот JSON формирует оператор JSON, а значение RETURN по умолчанию передаётся в ответе (см. результаты).
В случае, если все действия были завершены успешно, то возвращается соответствующий ответ с сообщением ОК.
Пример 3
Условие
Задана логика заказов книг.
CLASS Book 'Книга';
id 'Код' = DATA STRING[10] (Book) IN id;
name 'Наименование' = DATA ISTRING[100] (Book) IN id;
book (STRING[10] id) = GROUP AGGR Book b BY id(b);
CLASS Order 'Заказ';
date 'Дата' = DATA DATE (Order);
number 'Номер' = DATA STRING[10] (Order);
CLASS OrderDetail 'Строка заказа';
order 'Заказ' = DATA Order (OrderDetail) NONULL DELETE;
book 'Книга' = DATA Book (OrderDetail) NONULL;
nameBook 'Книга' (OrderDetail d) = name(book(d));
quantity 'Количество' = DATA INTEGER (OrderDetail);
price 'Цена' = DATA NUMERIC[14,2] (OrderDetail);
FORM order 'Заказ'
OBJECTS o = Order PANEL
PROPERTIES(o) date, number
OBJECTS d = OrderDetail
PROPERTIES(d) nameBook, quantity, price, NEW, DELETE
FILTERS order(d) = o
EDIT Order OBJECT o
;
FORM orders 'Заказы'
OBJECTS i = Order
PROPERTIES(i) READONLY date, number
PROPERTIES(i) NEWSESSION NEW, EDIT, DELETE
;
NAVIGATOR {
NEW orders;
}
Нужно отправить на определенный url HTTP-запрос на создание заказа в формате JSON.
Решение
FORM exportOrder
OBJECTS order = Order PANEL
PROPERTIES dt = date(order), nm = number(order)
OBJECTS detail = OrderDetail
PROPERTIES id = id(book(detail)), qn = quantity(detail), pr = price(detail)
FILTERS order(detail) = order
;
exportOrder 'Отправить' (Order o) {
EXPORT exportOrder OBJECTS order = o JSON;
LOCAL result = FILE();
EXTERNAL HTTP 'http://localhost:7651/exec?action=Location.importOrder' PARAMS exportFile() TO result;
}
EXTEND FORM orders
PROPERTIES(i) exportOrder;
;
Для создания JSON с вложенными тэгами нужно создать форму с соответствующими объектами, связанными через конструкцию FILTERS. На основе зависимостей между ними создается JSON с соответствующей структурой. В данном случае, JSON будет выглядеть следующим образом:
{
"dt":"20.08.18",
"nm":"1",
"detail":[
{
"pr":5.99,
"id":"b1",
"qn":3
},
{
"pr":6.99,
"id":"b2",
"qn":2
}
]
}
Для order не создается свой тэг, так как значение объекта передается параметром для оператора EXPORT.
В данном примере, ответ полученный на HTTP-запрос игнорируется.
Пример 4
Условие
Аналогично Примеру 3.
Нужно принять соответствующий HTTP-запрос и создать новый заказ в базе данных с параметрами запроса.
Решение
date = DATA LOCAL DATE();
number = DATA LOCAL STRING[10]();
id = DATA LOCAL STRING[10] (INTEGER);
quantity = DATA LOCAL INTEGER (INTEGER);
price = DATA LOCAL NUMERIC[14,2] (INTEGER);
FORM importOrder
PROPERTIES dt = date(), nm = number()
OBJECTS detail = INTEGER
PROPERTIES id = id(detail), qn = quantity(detail), pr = price(detail)
;
importOrder (FILE f) {
IMPORT importOrder JSON FROM f;
NEW o = Order {
date(o) <- date();
number(o) <- number();
FOR id(INTEGER detail) DO NEW d = OrderDetail {
order(d) <- o;
book(d) <- book(id(detail));
quantity(d) <- quantity(detail);
price(d) <- price(detail);
}
APPLY;
}
}
Для импорта соответствующего файла в формате JSON создается форма аналогичной структуры, только в качестве классов объектов используется тип INTEGER. При импорте значения тэгов будут помещены в свойства с соответствующими именами. Свойства date и number не имеют параметров, так как в JSON значения для них идут на самом верхнем уровне.
Пример 5
Условие
Аналогично Примеру 4.
Нужно отправить на определенный url HTTP-запрос на создание заказа в формате JSON, аналогичный предыдущему примеру, только обернуть все в тэг order.
Решение
GROUP order;
FORM exportOrderNew
OBJECTS o = Order
PROPERTIES IN order dt = date(o), nm = number(o)
OBJECTS detail = OrderDetail IN order
PROPERTIES id = id(book(detail)), qn = quantity(detail), pr = price(detail)
FILTERS order(detail) = o
;
exportOrderNew 'Отправить (новый)' (Order o) {
EXPORT exportOrderNew OBJECTS o = o JSON;
LOCAL result = FILE();
EXTERNAL HTTP 'http://localhost:7651/exec?action=Location.importOrderNew' PARAMS exportFile() TO result;
}
EXTEND FORM orders
PROPERTIES(i) exportOrderNew;
;
В отличие от предыдущего примера создаем группу order, при помощи оператора GROUP. При объявлении формы в эту группу записываем все свойства для заказа и объект detail. Результирующий JSON будет выглядеть следующим образом:
{
"order":{
"dt":"20.08.18",
"nm":"1",
"detail":[
{
"pr":5.99,
"id":"b1",
"qn":3
},
{
"pr":6.99,
"id":"b2",
"qn":2
}
]
}
}
Пример 6
Условие
Аналогично Примеру 5.
Нужно принять соответствующий HTTP-запрос и создать новый заказ в базе данных с параметрами запроса.
Решение
FORM importOrderNew
PROPERTIES IN order dt = date(), nm = number()
OBJECTS detail = INTEGER IN order
PROPERTIES id = id(detail), qn = quantity(detail), pr = price(detail)
;
importOrderNew (FILE f) {
IMPORT importOrderNew JSON FROM f;
NEW o = Order {
date(o) <- date();
number(o) <- number();
FOR id(INTEGER detail) DO NEW d = OrderDetail {
order(d) <- o;
book(d) <- book(id(detail));
quantity(d) <- quantity(detail);
price(d) <- price(detail);
}
APPLY;
}
}
Точно так же как и при экспорте, добавляем все свойств и объект detail в группу order для корректного приема новой версии JSON.
Пример 7
Условие
Аналогично Примеру 3.
Нужно по HTTP GET запросу, в котором задана дата, вернуть список номеров заказов от этой даты.
Решение
FORM exportOrders
OBJECTS date = DATE PANEL
OBJECTS order = Order
PROPERTIES nm = number(order)
FILTERS date(order) = date
;
getOrdersByDate (DATE d) {
RETURN JSON(exportOrders OBJECTS date = d);
}
Url, на который следует слать HTTP запрос, будет выглядеть следующим образом: http://localhost:7651/exec?action=Location.getOrdersByDate&p=12.11.2018.
В ответ будет возвращен, например, следующий JSON :
{
"order": [
{
"nm": "42"
},
{
"nm": "65"
}
]
}
Если заказов от этой даты нет, оператор JSON возвращает NULL, и ответ приходит с пустым телом. Если получателю нужен JSON с пустым списком ({"order":[]}), форму выгружают в файл оператором EXPORT и возвращают этот файл:
getOrdersByDateFile (DATE d) {
LOCAL f = JSONFILE ();
EXPORT exportOrders OBJECTS date = d JSON TO f;
RETURN f();
}
Так же возвращают и результат в формате, отличном от JSON: например, EXPORT CSV FROM ... TO f и затем RETURN f().
Пример 8
Условие
Аналогично Примеру 3.
Для каждого заказа существует список приложенных к нему файлов.
CLASS Attachment 'Приложение';
order = DATA Order (Attachment) NONULL DELETE;
name 'Имя' = DATA STRING (Attachment);
file = DATA FILE (Attachment);
Нужно реализовать HTTP GET запрос, который будет возвращать для переданного внутреннего идентификатора заказа его параметры и список файлов. Кроме того, нужно отдельным запросом реализовать получение содержимого конкретного файла.
Решение
FORM orderAttachments
OBJECTS o = Order
PROPERTIES(o) number, date
OBJECTS attachments = Attachment
PROPERTIES id = VALUE(attachments), name(attachments)
FILTERS order(attachments) = o
;
getOrderAttachments (LONG orderId) {
FOR LONG(Order o AS Order) = orderId DO
RETURN JSON(orderAttachments OBJECTS o = o);
}
Для формирования HTTP запроса нужно использовать следующий url : http://localhost:7651/exec?action=getOrderAttachments&p=32178. В нем параметр p содержит внутренний идентификатор заказа.
В ответ будет возвращен, например, следующий JSON :
{
"date": "20.10.2021",
"number": "12",
"attachments": [
{
"name": "File 1",
"id": 32180
},
{
"name": "File 2",
"id": 32183
}
]
}
В атрибутах id будут содержаться внутренние идентификаторы файлов. Затем содержимое этих файлов может быть прочитано запросом по следующему url : http://localhost:7651/exec?action=getOrderAttachment&p=32180. Действие getOrderAttachment объявляется следующим образом :
getOrderAttachment (LONG id) {
FOR LONG(Attachment a AS Attachment) = id DO
RETURN file(a);
}
Запрос вернет файл с Content-Type соответствующим расширению свойства file.
Пример 9
Условие
Аналогично Примеру 1.
При разработке нужно выполнить фрагмент кода на lsFusion на работающем сервере — например, посчитать города каждой страны, — не добавляя его в модуль и не перезапуская сервер.
Решение
Код передаётся в теле POST-запроса по адресу /eval или /eval/action HTTP API с типом содержимого, отличным от application/x-www-form-urlencoded:
curl -X POST -H "Content-Type: text/plain; charset=utf-8" --data-binary @script.lsf http://localhost:7651/eval
Сервер оборачивает полученный текст в собственный модуль, зависящий от всех загруженных модулей, поэтому заголовка MODULE и REQUIRE в тексте нет, а к элементам проекта обращаются по имени. Форма текста зависит от адреса:
-
/eval— последовательность инструкций модуля, одна из которых объявляет действие с именемrun. Остальные инструкции могут объявлять свойства и действия, которые оно использует, в том числе действиеINTERNALсо встроенным фрагментом кода на Java, у которого нет формы инструкции действия и которое вызывается изrun[]:cityCount (Country c) = GROUP SUM 1 IF country(City ct) = c;
logCount INTERNAL <{ System.out.println("counting cities"); }>;
run() {
logCount();
EXPORT JSON FROM name = name(Country c), cities = cityCount(c);
} -
/eval/action— только тело этого действия: инструкции действия, включая объявления локальных свойств. Заголовок модуля, объявление действия и форма оператора, объявляющая отдельное действие, — напримерINTERNALс именем класса или со встроенным фрагментом кода на Java, — здесь недопустимы (missing EOF at 'MODULE',no viable alternative at input '{'или'<{ ... }>'): такое объявление записывается в скрипт для/eval, как выше:LOCAL count = INTEGER ();
count() <- GROUP SUM 1 IF c IS City;
RETURN count();
По умолчанию в ответе возвращается значение RETURN (см. результаты). RETURN возвращает значение из любого места действия, в том числе из блока NEWSESSION и после APPLY. APPLY, отменённый ограничением, не завершает запрос ошибкой: изменения не сохраняются, а действие продолжается, поэтому без проверки ниже пример вернул бы количество с несохранённым городом. Действие проверяет canceled[] сразу после APPLY и при отмене возвращает текст applyMessage[]:
run() {
NEWSESSION {
NEW ct = City {
name(ct) <- 'Springfield';
}
APPLY;
IF canceled() THEN RETURN applyMessage();
RETURN JSON FROM cities = GROUP SUM 1 IF c IS City;
}
}
Пример 10
Условие
Аналогично Примеру 1.
Сервис, принимающий города, доступен не всегда: при слишком частых обращениях он отвечает 429 Too Many Requests, во время перезапуска — 503 Service Unavailable, а иногда соединение с ним не удаётся открыть вовсе. Запрос нужно повторить несколько раз с растущей паузой, прежде чем показать ошибку.
Решение
postCity 'Отправить' (City c) {
EXPORT JSON FROM countryId = id(country(c)), name = name(c);
LOCAL result = FILE();
LOCAL responseHeaders = TEXT (TEXT);
LOCAL attempt = INTEGER ();
LOCAL pause = LONG ();
LOCAL wait = LONG ();
LOCAL sent = BOOLEAN ();
timeoutHttp() <- 10000; // нет ответа за 10 с — ошибка, повторяется как неудавшееся соединение
attempt() <- 1;
pause() <- 1000;
WHILE NOT sent() AND attempt() <= 5 DO {
statusHttp() <- NULL; // при неудавшемся соединении или истечении времени ожидания статус не записывается
responseHeaders(TEXT h) <- NULL WHERE responseHeaders(h); // заголовки предыдущей попытки
TRY {
EXTERNAL HTTP 'http://localhost:7651/exec?action=Location.createCity' HEADERSTO responseHeaders PARAMS exportFile() TO result;
sent() <- TRUE;
} CATCH {
// 429 и 5xx, как и неудавшееся соединение (статус NULL), стоит повторить; любой другой статус окончательный
IF (statusHttp() < 500 AND statusHttp() != 429) OR attempt() = 5 THEN
throwException(messageCaughtException());
// пауза: Retry-After (число секунд, при любом написании имени заголовка), если сервис его прислал, иначе удваивающаяся пауза
wait() <- OVERRIDE LONG(GROUP MAX responseHeaders(TEXT h) IF lower(h) = 'retry-after') * 1000, pause();
sleep(wait());
pause() <- pause() * 2;
attempt() <- attempt() + 1;
}
}
LOCAL code = STRING[10]();
LOCAL message = STRING[100]();
IMPORT JSON FROM result() TO() code, message;
IF NOT code() = '0' THEN {
MESSAGE 'Ошибка: ' + message();
}
}
EXTEND FORM cities
PROPERTIES(c) postCity
;
Цикл делает до пяти попыток. Ответ со статусом не из 2xx и неудавшееся соединение одинаково завершают оператор EXTERNAL ошибкой, которую перехватывает оператор TRY. Каждая попытка начинается с очистки statusHttp[] и сохранённых заголовков, чтобы и статус, и заголовки относились только к этой попытке: статус записывается только при полученном ответе, поэтому после неудавшегося соединения или истечения времени ожидания он остаётся NULL, а заголовок, отсутствующий в ответе, иначе сохранил бы значение, полученное одной из предыдущих попыток. При ответе со статусом не из 2xx тело ответа, сохранённые заголовки и статус записываются до возбуждения ошибки, так что восстанавливающее действие их видит. Статусы 429 и 5xx, как и отсутствующий статус, означают, что запрос стоит повторить. Отсутствующий статус означает лишь то, что эта попытка не записала статус. Сервис уже мог обработать запрос, поэтому так повторяют только запрос, повтор которого безвреден или который сервис распознаёт как повторный. При любом другом статусе или неудаче пятой попытки throwException[TEXT] завершает действие, вызывая новую ошибку с сообщением перехваченной ошибки. Пауза перед следующей попыткой — значение заголовка Retry-After, если сервис прислал его как число секунд. Если заголовка нет или его значение не число (в заголовке может стоять и HTTP-дата), преобразование в LONG даёт NULL, и используется пауза этой попытки: 1, 2, 4 и 8 секунд. Платформа записывает имена заголовков в исходном регистре, поэтому для поиска Retry-After они приводятся к нижнему регистру. sleep[LONG] (System) приостанавливает само действие на это время. timeoutHttp[] задаёт время ожидания соединения и ответа для каждой попытки — 10 секунд, поэтому вызов, не получивший ответа за это время, завершается ошибкой и повторяется как неудавшееся соединение.