Часть 1. Создание HTTP-сервиса

Часть 2. Тестирование и отладка HTTP-сервиса с помощью SoapUI

Разработаем HTTP-сервис, который по уникальному идентификатору номенклатуры будет возвращать остаток этой номенклатуры на складе. HTTP-сервис должен отвечать следующим требованиям:

  1. Формат запроса и ответа - JSON;
  2. Запрос принимается методом POST;
  3. В заголовках запроса должна присутствовать информация о клиенте (клиент - другая информационная система);
  4. Сервис должен быть версионируемым (что бы клиенты не страдали от изменений в сервисе, а по возможности переходили на следующую версию).

Первым делом, создаем HTTP-сервис HTTPСервисОстатки, в корневом URL указываем значение balance.

Рисунок 1. Основные свойства http-сервиса

В шаблонах URL добавляем шаблон /nomenclature/{version} под именем Номенклатура. В шаблоне имеется параметр version, который заключен в фигурные скобки, он нам нужен, что бы выполнить требование №4 (см. рисунок 2).

Рисунок 2. Шаблон URL

Для шаблона добавляем HTTP-метод POST и создаем обработчик для этого метода, это требование №2 (см. рисунок 3).

Рисунок 3. HTTP-метод POST

В разрабатываемый HTTP-сервис закладываем две версии - первая будет доступна по url: /nomenclature/v1 и вторая по url: /nomenclature/v2. Первая версия будет работать только с одним уникальным идентификатором номенклатуры, а вторая версия с массивом уникальных идентификаторов. Структура JSON-строки запроса и ответа для каждой версии приведены в таблице ниже.

/nomenclature/v1/nomenclature/v2
Запрос
{
  "Период": "",
  "UUID": ""
}
      
{
  "Период": "",
  "UUID": []
}
      
Ответ
{
  "Версия": 0,
  "НаДату": "",
  "Остаток": 0
}
      
{
   "Версия": 0,
   "НаДату": "",
   "Остатки": [
     {
      "UUID": "",
      "Остаток": 0
     }
   ],
   "Резерв": null
}
      

Приступим к программной реализации обработчика HTTP-метода POST.

Если клиент обращается к неизвестной версии HTTP-сервиса, то необходимо об этом как-то ему сообщить, да что бы было понятно в чем дело, поэтому для выполнения требования №4, не достаточно ввести параметр version в шаблон URL, его значение нужно дополнительно анализировать в обработчике HTTP-метода. В случае неизвестного значения  возвращаем в ответ ошибку 404 с описанием причины, что такой URL серверу неизвестен или версия сервиса неизвестна.

	
// Определение запрашиваемой версии сервиса.
// Если версия не известна, то отклоняем запрос с описанием причины.
ДопустимыеВерсииСервиса	= "v1,v2";
ВерсияСервиса = Запрос.ПараметрыURL["version"];
Если СтрРазделить(ДопустимыеВерсииСервиса, ",").Найти(ВерсияСервиса) = Неопределено Тогда 
    Ответ = Новый HTTPСервисОтвет(404);
    Ответ.УстановитьТелоИзСтроки(НСтр("ru = 'Версия не существует'"));
    Возврат Ответ;
КонецЕсли;
  

Что бы выполнить требование №1, проверяем в заголовках HTTP-запроса наличие заголовка Content-Type, его значение должно равняться application/json. Это стандартный заголовок и один из самых известных, используется для определения типа передаваемых данных. Если данный заголовок будет отсутствовать, то по умолчанию считаем что пришли данные типа текст в формате JSON, иначе анализируем значение заголовка и принимаем решение, если это не текстовые данные в формате JSON, выкидываем в ответ ошибку с кодом 415.

	
// Чтение значения заголовка Content-Type.
// Если значение заголовка не соответствует допустимому,
// то отклоняем запрос с описанием причины.
ТипДопустимогоКонтента = "application/json";
ContentType = Запрос.Заголовки.Получить("Content-Type");
Если ContentType <> Неопределено Тогда
    Если СтрНайти(ContentType, ТипДопустимогоКонтента) = 0 Тогда 
        Ответ = Новый HTTPСервисОтвет(415);
        Ответ.УстановитьТелоИзСтроки(НСтр("ru = 'Ожидаемый формат - JSON'"));
        Возврат Ответ;
    КонецЕсли;
КонецЕсли;
  

Что бы выполнить требование №3, проверяем в заголовках HTTP-запроса наличие заголовка Client-Name-1C, это собственный заголовок, мы его придумали сами и используем для своих целей, допустим, фиксируем в журнале регистрации значение заголовка. Наличие данного заголовка обязательно, если он отсутствует, то выкидываем в ответ ошибку с кодом 406.

	
// Чтение обязательного заголовка Client-Name-1C.
ClientName1C = Запрос.Заголовки.Получить("Client-Name-1C");
Если ClientName1C = Неопределено Тогда
    Ответ = Новый HTTPСервисОтвет(406);
    Ответ.УстановитьТелоИзСтроки(НСтр("ru = 'Отсутствует обязательный заголовок'"));
    Возврат Ответ;
Иначе 
    // Запись в журнал регистрации о клиенте.
    // ...
КонецЕсли;
  

Теперь самое основное, реализуем логику работы HTTP-сервиса. Получаем тело HTTP-запроса как строку, в нашем случает это JSON-строка (см. требование №1). Что бы было удобно работать с данными, которые содержит в себе JSON-строка, читаем JSON-строку в коллекцию значений. Спасибо разработчикам платформы 1С за процедуры и функции по работе с JSON, которые появились в версии 8.3.

	
// Читаем тело запроса как JSON-строку.
СтроковоеТело = Запрос.ПолучитьТелоКакСтроку();
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(СтроковоеТело);
// Когда задана функция восстановления, то третий параметр игнорируется, хотя в СП написано наоборот.
ДанныеЗапроса = ПрочитатьJSON(ЧтениеJSON, , , , "ФункцияВосстановленияЗначений", РаботаJSON);
ЧтениеJSON.Закрыть();
  

Для каждой версии HTTP-сервиса (см. требование №4) пишем код в соответствии с логикой работы каждой версии. В первой версии работаем с одним уникальным идентификатором, во второй с массивом. Данные для ответа формируем в виде коллекции значений.

ДанныеОтвета = Новый Структура;
Если ВерсияСервиса = "v1" Тогда 

    // Обрабатываем запрос.
    Период = ДанныеЗапроса.Период;
    Номенклатура = Справочники.Номенклатура.ПолучитьСсылку(ДанныеЗапроса.UUID);

    // Выполняем расчет остатков.
    // ...
		
    // Формируем ответ.
    ДанныеОтвета.Вставить("Версия", 1.04);
    ДанныеОтвета.Вставить("НаДату", Период);
    ДанныеОтвета.Вставить("Остаток", 10);

ИначеЕсли ВерсияСервиса = "v2" Тогда

    // Обрабатываем запрос.
    Период = ДанныеЗапроса.Период;
    СписокНоменклатур = Новый Массив;
    Для Каждого ТекЭлемент Из ДанныеЗапроса.UUID Цикл 
        СписокНоменклатур.Добавить(Справочники.Номенклатура.ПолучитьСсылку(ТекЭлемент));
    КонецЦикла;

    // Выполняем расчет остатков.
    // ...

    ОстаткиПоНоменклатурам = Новый Массив;
    Для Каждого ТекЭлемент Из СписокНоменклатур Цикл 
        Остаток = Новый Структура("UUID,Остаток", ТекЭлемент.УникальныйИдентификатор(), 10);
        ОстаткиПоНоменклатурам.Добавить(Остаток);
    КонецЦикла;

    // Формируем ответ.
    ДанныеОтвета.Вставить("Версия", 2.15);
    ДанныеОтвета.Вставить("НаДату", Период);
    ДанныеОтвета.Вставить("Остатки", ОстаткиПоНоменклатурам);

КонецЕсли;
  

Сформированные данные для ответа записываем в JSON-строку, на основании которой создаем HTTP-ответ с кодом 200. В заголовках HTTP-ответа обязательно указываем заголовок Content-Type, что бы клиент точно знал тип данных и их формат в ответе.

	
// Сохраняем ответ в формате JSON-строки.
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, ДанныеОтвета, , "ФункцияПреобразованияЗначений", РаботаJSON);	
СтроковоеТело = ЗаписьJSON.Закрыть();

// Отвечаем.
Ответ = Новый HTTPСервисОтвет(200);
Ответ.Заголовки.Вставить("Content-Type", ТипДопустимогоКонтента + "; charset=utf-8");
Ответ.УстановитьТелоИзСтроки(СтроковоеТело);
  

В запросе и ответе участвуют уникальные идентификаторы. Проблема в том, что 1Сные процедуры и функции по работе с JSON не умеют преобразовывать уникальный идентификатор из строки в тип УникальныйИдентификатор и обратно, но их этому можно научить. Делается это с помощью параметра ИмяФункцииПреобразования для процедуры ЗаписатьJSON и с помощью параметра ИмяФункцииВосстановления для процедуры ПрочитатьJSON. Реализацию функций для перечисленных параметров рассматривать не будем, так как это выходит за рамки создания HTTP-сервиса.

Полный код обработчика для метода POST:

	
Функция ШаблонURLМетодPOST(Запрос)
    
    // Определение запрашиваемой версии сервиса.
    // Если версия неизвестна, то отклоняем запрос с описанием причины.
    ДопустимыеВерсииСервиса = "v1,v2";
    ВерсияСервиса = Запрос.ПараметрыURL["version"];
    Если СтрРазделить(ДопустимыеВерсииСервиса, ",").Найти(ВерсияСервиса) = Неопределено Тогда 
        Ответ = Новый HTTPСервисОтвет(404);
        Ответ.УстановитьТелоИзСтроки(НСтр("ru = 'Версия не существует'"));
        Возврат Ответ;
    КонецЕсли;
    
    // Чтение значения заголовка Content-Type.
    // Если значение заголовка не соответствует допустимому,
    // то отклоняем запрос с описанием причины.
    ТипДопустимогоКонтента = "application/json";
    ContentType = Запрос.Заголовки.Получить("Content-Type");
    Если ContentType <> Неопределено Тогда
        Если СтрНайти(ContentType, ТипДопустимогоКонтента) = 0 Тогда 
            Ответ = Новый HTTPСервисОтвет(415);
            Ответ.УстановитьТелоИзСтроки(НСтр("ru = 'Ожидаемый формат - JSON'"));
            Возврат Ответ;
        КонецЕсли;
    КонецЕсли;
    
    // Чтение обязательного заголовка Client-Name-1C.
    ClientName1C = Запрос.Заголовки.Получить("Client-Name-1C");
    Если ClientName1C = Неопределено Тогда
        Ответ = Новый HTTPСервисОтвет(406);
        Ответ.УстановитьТелоИзСтроки(НСтр("ru = 'Отсутствует обязательный заголовок'"));
        Возврат Ответ;
    Иначе 
        // Запись в журнал регистрации о клиенте.
        // ...
    КонецЕсли;
    
    // Читаем тело запроса как JSON-строку.
    СтроковоеТело = Запрос.ПолучитьТелоКакСтроку();
    ЧтениеJSON = Новый ЧтениеJSON;
    ЧтениеJSON.УстановитьСтроку(СтроковоеТело);
    // Когда задана функция восстановления, то третий параметр игнорируется, хотя в СП написано наоборот.
    ДанныеЗапроса = ПрочитатьJSON(ЧтениеJSON, , , , "ФункцияВосстановленияЗначений", РаботаJSON);
    ЧтениеJSON.Закрыть();
    
    ДанныеОтвета = Новый Структура;
    Если ВерсияСервиса = "v1" Тогда 
        
        // Обрабатываем запрос.
        Период = ДанныеЗапроса.Период;
        Номенклатура = Справочники.Номенклатура.ПолучитьСсылку(ДанныеЗапроса.UUID);
        
        // Выполняем расчет остатков.
        // ...
        
        // Формируем ответ.
        ДанныеОтвета.Вставить("Версия", 1.04);
        ДанныеОтвета.Вставить("НаДату", Период);
        ДанныеОтвета.Вставить("Остаток", 10);
        
    ИначеЕсли ВерсияСервиса = "v2" Тогда
        
        // Обрабатываем запрос.
        Период = ДанныеЗапроса.Период;
        СписокНоменклатур = Новый Массив;
        Для Каждого ТекЭлемент Из ДанныеЗапроса.UUID Цикл 
            СписокНоменклатур.Добавить(Справочники.Номенклатура.ПолучитьСсылку(ТекЭлемент));
        КонецЦикла;
        
        // Выполняем расчет остатков.
        // ...
        
        ОстаткиПоНоменклатурам = Новый Массив;
        Для Каждого ТекЭлемент Из СписокНоменклатур Цикл 
            Остаток = Новый Структура("UUID,Остаток", ТекЭлемент.УникальныйИдентификатор(), 10);
            ОстаткиПоНоменклатурам.Добавить(Остаток);
        КонецЦикла;
        
        // Формируем ответ.
        ДанныеОтвета.Вставить("Версия", 2.15);
        ДанныеОтвета.Вставить("НаДату", Период);
        ДанныеОтвета.Вставить("Остатки", ОстаткиПоНоменклатурам);
        
    КонецЕсли;
    
    // Задаем настройки для записи коллекции значений в JSON-строку.
    НастройкиСериализацииJSON = Новый НастройкиСериализацииJSON;
    НастройкиСериализацииJSON.ВариантЗаписиДаты                 = ВариантЗаписиДатыJSON.ЛокальнаяДатаСоСмещением;
    НастройкиСериализацииJSON.СериализовыватьМассивыКакОбъекты  = Ложь;
    НастройкиСериализацииJSON.ФорматСериализацииДаты            = ФорматДатыJSON.ISO;
    
    // Сохраняем ответ в формате JSON-строки.
    ЗаписьJSON = Новый ЗаписьJSON;
    ЗаписьJSON.УстановитьСтроку();
    ЗаписатьJSON(ЗаписьJSON, ДанныеОтвета, НастройкиСериализацииJSON, "ФункцияПреобразованияЗначений", РаботаJSON); 
    СтроковоеТело = ЗаписьJSON.Закрыть();
    
    // Отвечаем.
    Ответ = Новый HTTPСервисОтвет(200);
    Ответ.Заголовки.Вставить("Content-Type", ТипДопустимогоКонтента + "; charset=utf-8");
    Ответ.УстановитьТелоИзСтроки(СтроковоеТело);    
    
    Возврат Ответ;
    
КонецФункции
  


Комментарии