Начало: Зарисовки на тему оформления кода HTTP-сервиса
Новая статья посвящена валидации данных.
Напомню, мы разбили сервис на 4 модуля, определив функции, границы и взаимосвязи. Что важно для их поддержки и развития. В прошлый раз показал код модуля РаботаСЗадачами, на очереди модуль ВалидацияДанных, будет много кода из РаботаСЗадачамиHTTP и кусочек из РаботаСЗадачамиРеализация.
Валидация – это фильтр для входных данных, проверка на соответствие заданным условиям и ограничениям.
Что проверяем:
-
Само наличие
-
Тип и формат
-
Допустимые значения и ссылочную целостность
-
Состав и согласованность (обязательность, актуальность, бизнес правила)
Безопасность, отдельная тема. Права доступа, объемы и «вражеское» содержимое не рассматриваем.
Входные данные в нашем сервисе — это строки (заголовки, параметры и текстовое тело запроса) или файлы, последних не касаемся.
При проверке данные можем преобразовать в доменные объекты или объекты языка: строка в дату, строка в число. Преобразование может быть как часть проверки (через Попытку) и всегда как результат.
Обратная связь
Обратную связь даем в виде текста ошибки. Все ошибки валидации объединены под КодОшибки = «ВалидацияДанных». Можем проверять данные как точечно, так и массово, собирая ошибки в массив.
Способ сбора ошибок в массив (Механизм «СообщенияПользователю + Отказ»):
// ОМ ВалидацияДанных (далее если не указано, то используем именно этот модуль)
Процедура СообщитьПользователю(ТекстОшибки, Отказ)
ОбщегоНазначения.СообщитьПользователю(ТекстОшибки,,,, Отказ)
КонецПроцедуры
Как проверяем
Наличие или обязательность
Функция Пустое(Значение, ТекстОшибки, Отказ) Экспорт
Функция Пустое(Значение, ТекстОшибки, Отказ) Экспорт
Если ЗначениеЗаполнено(Значение) Тогда Возврат Ложь КонецЕсли;
СообщитьПользователю(ТекстОшибки, Отказ);
Возврат Истина;
КонецФункции
Тип и формат
Функция КакДата(Значение, ТекстОшибки, Отказ) Экспорт
Функция КакДата(Значение, ТекстОшибки, Отказ) Экспорт
Если НЕ ЗначениеЗаполнено(Значение) Тогда Возврат Неопределено КонецЕсли;
Попытка
// договорились, что дата передается в таком формате
Возврат ПрочитатьДатуJSON(Значение, ФорматДатыJSON.ISO);
Исключение
СообщитьПользователю(ТекстОшибки, Отказ);
Возврат Неопределено;
КонецПопытки;
КонецФункции
Функция КакЧисло(Значение, ТекстОшибки, Отказ) Экспорт
Функция КакЧисло(Значение, ТекстОшибки, Отказ) Экспорт
Если НЕ ЗначениеЗаполнено(Значение) Тогда Возврат Неопределено КонецЕсли;
Если СтроковыеФункцииКлиентСервер.ТолькоЦифрыВСтроке(Значение) Тогда Возврат Число(Значение) КонецЕсли;
СообщитьПользователю(ТекстОшибки, Отказ);
Возврат Неопределено;
КонецФункции
Функция КакУИД(Значение, ТекстОшибки, Отказ) Экспорт
Функция КакУИД(Значение, ТекстОшибки, Отказ) Экспорт
Если НЕ ЗначениеЗаполнено(Значение) Тогда Возврат Неопределено КонецЕсли;
Если СтроковыеФункцииКлиентСервер.ЭтоУникальныйИдентификатор(Значение) Тогда Возврат Значение КонецЕсли;
СообщитьПользователю(ТекстОшибки, Отказ);
Возврат Неопределено
КонецФункции
Ссылки
Функция КорректнаяСсылка(Значение, ТекстОшибки, Отказ) Экспорт
Функция КорректнаяСсылка(Значение, ТекстОшибки, Отказ) Экспорт
Если ОбщегоНазначения.СсылкаСуществует(Значение) Тогда Возврат Истина КонецЕсли;
СообщитьПользователю(ТекстОшибки, Отказ);
Возврат Ложь;
КонецФункции
Согласованность и бизнес-правила
Проверяем через «Вычислить» условие безопасно. Смотрите далее в разделе «Массовая проверка»
Функция УсловиеВыполнено(ШаблонУсловие, Контекст)
Функция УсловиеВыполнено(ШаблонУсловие, Контекст)
Параметры = Новый Структура("Результат", Контекст);
СтрокаУсловие = СтрШаблон(ШаблонУсловие, "Параметры.Результат");
Возврат ОбщегоНазначения.ВычислитьВБезопасномРежиме(СтрокаУсловие, Параметры)
КонецФункции
Тексты ошибок
Тексты ошибок, как и в случае с типизированными ошибками создает модуль «РаботаСЗадачамиHTTP», пример в реализации метода поиска объекта по переданному идентификатору:
Функция ЗадачаСотрудникаПоИдентификатору(Значение, Отказ)
// ОМ РаботаСЗадачамиHTTP
Функция ЗадачаСотрудникаПоИдентификатору(Значение, Отказ)
ИдентификаторНеЗадан = "Идентификатор задачи не задан";
НеИдентификатор = "Недопустимый идентификатор задачи, идентификатор должен быть в формате UUID";
ОбъектНеСуществует = "Задача с указанным идентификатором не существует";
Если ВалидацияДанных.Пустое(Значение, ИдентификаторНеЗадан, Отказ) Тогда Возврат Неопределено КонецЕсли;
Если ВалидацияДанных.КакУИД(Значение, НеИдентификатор, Отказ) = Неопределено Тогда Возврат Неопределено КонецЕсли;
Ссылка = РаботаСЗадачамиРеализация.ЗадачаСотрудникаПоСсылке(Новый УникальныйИдентификатор(Значение));
Если НЕ ВалидацияДанных.КорректнаяСсылка(Ссылка, ОбъектНеСуществует, Отказ) Тогда Возврат Неопределено КонецЕсли;
Возврат Ссылка
КонецФункции
Массовая проверка
На примере получения списка задач, проверим входящие данные. Дано такое соответствие: имена параметров запроса к БД и имена параметров http-запроса:
// ОМ РаботаСЗадачамиHTTP
Функция СписокЗадачСоответствиеКлючей()
Результат = Новый Соответствие;
Результат.Вставить("ДатаНачала", "startDate");
Результат.Вставить("ДатаОкончания", "endDate");
Результат.Вставить("ИдентификаторИсполнителя", "userId");
Результат.Вставить("ИдентификаторАвтора", "authorId");
Результат.Вставить("НомерСтраницы", "page");
Результат.Вставить("ДоКурсора", "before");
Результат.Вставить("ПослеКурсора", "after");
Результат.Вставить("Лимит", "limit");
Возврат Результат;
КонецФункции
Конструкторы правила и условия
Создадим конструкторы в модуле ВалидацияДанных.
Функция НовоеПравилоВалидации(Ключ, КакПроверять, ТекстОшибки, ЗначениеПоУмолчанию = Неопределено) Экспорт
Функция НовоеПравилоВалидации(Ключ, КакПроверять, ТекстОшибки, ЗначениеПоУмолчанию = Неопределено) Экспорт
Возврат Новый Структура("Ключ,Как,ТекстОшибки,ПоУмолчанию", Ключ, КакПроверять, ТекстОшибки, ЗначениеПоУмолчанию)
КонецФункции
Функция НовоеУсловие(ШаблонУсловие, ШаблонОшибка) Экспорт
Функция НовоеУсловие(ШаблонУсловие, ШаблонОшибка) Экспорт
Возврат Новый Структура("ШаблонУсловие,ШаблонОшибка", ШаблонУсловие, ШаблонОшибка)
КонецФункции
Набор правил
Создаем в модуле РаботаСЗадачамиHTTP
Функция НаборПравилВалидацииПараметровСпискаЗадач()
Функция НовоеПравилоВалидации(Ключ, КакПроверять, ШаблонОшибка, ЗначениеПоУмолчанию = Неопределено)
Возврат ВалидацияДанных.НовоеПравилоВалидации(Ключ, КакПроверять, СтрШаблон(ШаблонОшибка, Ключ), ЗначениеПоУмолчанию)
КонецФункции
Функция НаборПравилВалидацииПараметровСпискаЗадач()
ШаблонОшибкаДата = "Параметр %1 не является корректной датой";
ШаблонОшибкаЧисло = "Параметр %1 не является натуральным числом";
ШаблонОшибкаУИД = "Параметр %1 не является UUID";
Правила = Новый Соответствие;
Правила.Вставить("ДатаНачала", НовоеПравилоВалидации("startDate", "КакДата", ШаблонОшибкаДата));
Правила.Вставить("ДатаОкончания", НовоеПравилоВалидации("endDate", "КакДата", ШаблонОшибкаДата));
Правила.Вставить("ИдентификаторИсполнителя", НовоеПравилоВалидации("userId", "КакУИД", ШаблонОшибкаУИД));
Правила.Вставить("ИдентификаторАвтора", НовоеПравилоВалидации("authorId", "КакУИД", ШаблонОшибкаУИД));
Правила.Вставить("ДоКурсора", НовоеПравилоВалидации("before", "КакУИД", ШаблонОшибкаУИД, ПоследнийИдентификатор()));
Правила.Вставить("ПослеКурсора", НовоеПравилоВалидации("after", "КакУИД", ШаблонОшибкаУИД, ПустойИдентификатор()));
Правила.Вставить("НомерСтраницы", НовоеПравилоВалидации("page", "КакЧисло", ШаблонОшибкаЧисло));
Правила.Вставить("Лимит", НовоеПравилоВалидации("limit", "КакЧисло", ШаблонОшибкаЧисло));
Возврат Правила
КонецФункции
Это набор правил под конкретную задачу, могут быть другие задачи и соответственно другие наборы. Ключом набора является имя параметра запроса к БД, ключом правила – имя параметра http-запроса.
Набор условий
Создаем в модуле РаботаСЗадачамиHTTP
Функция НаборУсловийПараметровСпискаЗадач()
Функция НовоеУсловие(ШаблонУсловие, ШаблонОшибка)
Возврат ВалидацияДанных.НовоеУсловие(ШаблонУсловие, ШаблонОшибка)
КонецФункции
Функция НаборУсловийПараметровСпискаЗадач()
ШаблонОшибкаВзаимноеЗаполнение = "Параметры %1 и %2 не должны быть заполнены одновременно";
ШаблонОшибкаСоответствиеДат = "Параметр начальной даты %1 больше параметра даты окончания %2";
Правила = Новый Соответствие;
Правила.Вставить("ДоКурсора,ПослеКурсора",
НовоеУсловие("ЗначениеЗаполнено(%1.ДоКурсора) И ЗначениеЗаполнено(%1.ПослеКурсора)",
ШаблонОшибкаВзаимноеЗаполнение
)
);
Правила.Вставить("ДатаНачала,ДатаОкончания",
НовоеУсловие("ЗначениеЗаполнено(%1.ДатаНачала) И ЗначениеЗаполнено(%1.ДатаОкончания) И (%1.ДатаНачала > %1.ДатаОкончания"),
ШаблонОшибкаСоответствиеДат
)
);
Возврат Правила
КонецФункции
Это набор для проверки условий согласованности в переданных параметрах http-запроса. Ключ условия - список, участвующих в условие параметров запроса к БД, в значение само условие. Сложность: в тексте ошибки надо вывести ключи, которые пришли из http-запроса. Смотрите метод ТекстОшибкиЗаменитьКлючиНаВходящиеКлючи() в следующем пункте.
Метод, реализующий проверки
В модуле ВалидацииДанных. На входе наборы правил валидации и условий, которые мы обойдем и проверим. Если найдем ошибки, то зафиксируем их и Отказ будет автоматически взведен в Истину.
Функция РезультатВалидации(ВходящиеЗначения, СоответствиеКлючей, Валидация, Условия, Отказ) Экспорт
Функция РезультатВалидации(ВходящиеЗначения, СоответствиеКлючей, Валидация, Условия, Отказ) Экспорт
Результат = Новый Структура;
Для каждого Правило Из Валидация Цикл
Ключ = Правило.Ключ; // ключ для реализации
Источник = Правило.Значение;
КлючВх = Источник.Ключ; // ключ входящий
ЗначениеВх = ВходящиеЗначения.Получить(КлючВх);
Если ЗначениеВх = "" И Источник.ПоУмолчанию <> Неопределено Тогда
//если пусто и есть по умолчанию, то подставим
ЗначениеВх = Источник.ПоУмолчанию;
КонецЕсли;
КакПроверять = Источник.Как;
Значение = Неопределено; // для реализации
Если КакПроверять = "КакДата" Тогда Значение = КакДата(ЗначениеВх, Источник.ТекстОшибки, Отказ);
ИначеЕсли КакПроверять = "КакУИД" Тогда Значение = КакУИД(ЗначениеВх, Источник.ТекстОшибки, Отказ);
ИначеЕсли КакПроверять = "КакЧисло" Тогда Значение = КакЧисло(ЗначениеВх, Источник.ТекстОшибки, Отказ);
КонецЕсли;
Результат.Вставить(Ключ, Значение);
КонецЦикла;
КопияРезультат = Новый Структура(Новый ФиксированнаяСтруктура(Результат));
Для Каждого Правило Из Условия Цикл
Ключи = СтрРазделить(Правило.Ключ, ",", Ложь);
Шаблоны = Правило.Значение;
Если УсловиеВыполнено(Шаблоны.ШаблонУсловие, КопияРезультат) Тогда
ТекстОшибки = ТекстОшибкиЗаменитьКлючиНаВходящиеКлючи(Шаблоны.ШаблонОшибка, Ключи, СоответствиеКлючей);
СообщитьПользователю(ТекстОшибки, Отказ);
КонецЕсли;
КонецЦикла;
Возврат Результат;
КонецФункции
Функция ТекстОшибкиЗаменитьКлючиНаВходящиеКлючи(ШаблонОшибки, Ключи, СоответствиеКлючей)
КлючиВходящие = Новый Массив; // массив для входящих
Для каждого Ключ Из Ключи Цикл
КлючиВходящие.Добавить(СоответствиеКлючей.Получить(Ключ));
КонецЦикла;
Возврат СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтрокуИзМассива(ШаблонОшибки, КлючиВходящие)
КонецФункции
Для полной картины еще раз посмотрим на всю цепочку:
- В модуле сервиса РаботаСЗадачами, вызов:
СписокЗадач = РаботаСЗадачамиHTTP.СписокЗадач(Запрос, КодОшибки, Заголовки);
Функция ПолучитьЗадачи(Запрос)
Функция ПолучитьЗадачи(Запрос)
Если РаботаСЗадачамиHTTP.ЗаблокированоОбновлением() Тогда Возврат ОтветЗаблокированоОбновлением() КонецЕсли;
Заголовки = Новый Соответствие;
Если РаботаСЗадачамиHTTP.ПревышеноЧислоЗапросов(Заголовки) Тогда
Возврат ОтветПревышеноЧислоЗапросов(Заголовки)
КонецЕсли;
Попытка
КодОшибки = "";
СписокЗадач = РаботаСЗадачамиHTTP.СписокЗадач(Запрос, КодОшибки, Заголовки);
Исключение
Возврат ОтветВнутренняяОшибка(ИнформацияОбОшибке());
КонецПопытки;
Если КодОшибки <> "" Тогда Возврат ОтветТипизированнаяОшибка(КодОшибки, Заголовки) КонецЕсли;
Возврат Ответ(, "Список задач", СписокЗадач, Заголовки)
КонецФункции
- В модуле контроллере РаботаСЗадачамиHTTP, вызов:
ПараметрыЗапроса = ВалидацияДанных.РезультатВалидации(…, Отказ)
Функция СписокЗадач(Запрос, КодОшибки, ЗаголовкиРезультат) Экспорт
Функция СписокЗадач(Запрос, КодОшибки, ЗаголовкиРезультат) Экспорт
УстановитьПривилегированныйРежим(Истина);
Отказ = Ложь;
ПараметрыЗапроса = ВалидацияДанных.РезультатВалидации(
Запрос.ПараметрыЗапроса,
СписокЗадачСоответствиеКлючей(),
НаборПравилВалидацииПараметровСпискаЗадач(),
НаборУсловийПараметровСпискаЗадач(),
Отказ
);
Если Отказ Тогда
КодОшибки = "ВалидацияДанных";
Возврат Неопределено;
КонецЕсли;
ФильтрЗадач = РаботаСЗадачамиРеализация.НовыйФильтрЗадач();
ЗаполнитьЗначенияСвойств(ФильтрЗадач, ПараметрыЗапроса);
КоличествоЗадач = РаботаСЗадачамиРеализация.СписокЗадачКоличество(ФильтрЗадач);
Если КоличествоЗадач = 0 Тогда Возврат Новый Массив КонецЕсли;
НастройкиПолученияЗадач = РаботаСЗадачамиРеализация.НовыеНастройкиПолученияЗадач();
ЗаполнитьЗначенияСвойств(НастройкиПолученияЗадач, ПараметрыЗапроса);
Лимит = 20; // ограничиваем количество записей
НомерСтраницы = 0; // не нулевое значение на выходе - выбрана пагинация-страницы
ОпределитьСпособПагинации(НастройкиПолученияЗадач, Лимит, НомерСтраницы);
ЗадачНаСтраницу = Мин(Лимит, КоличествоЗадач - (НомерСтраницы - 1) * Лимит); // актуально для последний страницы
Если ЗадачНаСтраницу <= 0 Тогда Возврат Новый Массив КонецЕсли;
ДанныеЗадач = РаботаСЗадачамиРеализация.СписокЗадачВыгрузка(НастройкиПолученияЗадач, ФильтрЗадач, ЗадачНаСтраницу);
ФайлыЗадач = ФайлыЗадач(РаботаСЗадачамиРеализация.ФайлыЗадачВыборка(ДанныеЗадач.ВыгрузитьКолонку("Ссылка")));
ДанныеЗадач = РаботаСЗадачамиРеализация.ТаблицаЗначенийВМассив(ДанныеЗадач);
ЗаголовкиРезультат = ЗаголовкиПагинации(
КоличествоЗадач, Лимит, НомерСтраницы, // для расчета следующей страницы
ДанныеЗадач[ДанныеЗадач.ВГраница()].УникальныйИдентификатор // следующий курсор
);
Возврат ПодготовленныеЗадачи(ДанныеЗадач, ФайлыЗадач);
КонецФункции
Все получается, довольно емко).
Разделение ответственности
Проверки могут быть на разных уровнях. Кроме нашего сервиса, часть проверок можно поручить конфигурации и БД. Например, нарушение состава обязательных реквизитов можно выявить через вызов у объекта метода ПроверитьЗаполнение() перед записью:
// ОМ РаботаСЗадачамиРеализация
Процедура СоздатьЗадачуСотрудника(ПараметрыЗаполнения, РезультатНомер, Отказ) Экспорт
ДокОбъект = Документы.ЗадачаСотрудника.СоздатьДокумент();
ДокОбъект.Заполнить(ПараметрыЗаполнения);
Если Не ДокОбъект.ПроверитьЗаполнение() Тогда
Отказ = Истина;
Возврат
КонецЕсли;
ДокОбъект.Записать();
РезультатНомер = ДокОбъект.Номер;
КонецПроцедуры
В случае нарушения сама система сформирует текст ошибки и отправит его куда надо, правда нам придется самим позаботиться об Отказе. Таким образом мы облегчаем себе задачу проверки входящих данных при создании объектов.
Проверки ожидаемых значений заголовков и формата тела http-запроса
Такие проверки делаем внутри контроллера сервиса и присваиваем соответствующий типизированный КодОшибки:
Функция НоваяЗадачаСотрудника(Запрос, КодОшибки, ЗаголовкиРезультат, КодРезультат) Экспорт
// ОМ РаботаСЗадачамиHTTP
Функция НоваяЗадачаСотрудника(Запрос, КодОшибки, ЗаголовкиРезультат, КодРезультат) Экспорт
УстановитьПривилегированныйРежим(Истина);
ТипКонтента = ЗначениеПоКлючуВЗаголовках(Запрос.Заголовки, "Content-Type");
ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку();
ВходящиеСвойстваЗадачи = Неопределено;
Попытка
Если СтрНачинаетсяС(НРег(ТипКонтента), "application/json") Тогда
ВходящиеСвойстваЗадачи = ПрочитатьЗначениеJSON(ТелоЗапроса);
ИначеЕсли СтрНачинаетсяС(НРег(ТипКонтента), "application/x-www-form-urlencoded") Тогда
ВходящиеСвойстваЗадачи = ЗадачаПоляUrlEncoded(ТелоЗапроса);
Иначе
КодОшибки = "ТипТела";
Возврат Неопределено;
КонецЕсли;
Исключение
КодОшибки = "ФорматТела";
Возврат Неопределено;
КонецПопытки;
Если ТипЗнч(ВходящиеСвойстваЗадачи) <> Тип("Структура") Тогда
КодОшибки = "ФорматТела";
Возврат Неопределено;
КонецЕсли;
СвойстваЗадачи = НовыеСвойстваЗадачи();
ЗаполнитьЗначенияСвойств(СвойстваЗадачи, ВходящиеСвойстваЗадачи);
Отказ = Ложь;
ПараметрыЗаполнения = РаботаСЗадачамиРеализация.ЗадачаСотрудникаПараметрыЗаполнения();
ПараметрыЗаполнения.Приоритет = ПриоритетЗадачиПоИндентификатору(СвойстваЗадачи.Priority, Отказ);
ПараметрыЗаполнения.Содержание = СвойстваЗадачи.Text;
ПараметрыЗаполнения.Исполнитель = ПользовательПоИдентификатору(СвойстваЗадачи.UserID, Отказ);
ПараметрыЗаполнения.Автор = ПользовательПоИдентификатору(СвойстваЗадачи.AuthorID, Отказ);
РаботаСЗадачамиРеализация.СоздатьЗадачуСотрудника(ПараметрыЗаполнения, СвойстваЗадачи.TaskID, Отказ);
// в теле запроса, может быть указан url для возврата ответа ошибки
НастроитьПеренаправление(ВходящиеСвойстваЗадачи, "error_url", ЗаголовкиРезультат, КодРезультат);
Если Отказ Тогда
КодОшибки = "ВалидацияДанных";
Возврат Неопределено;
КонецЕсли;
Если СвойстваЗадачи.TaskID = Неопределено Тогда
КодОшибки = "ЗадачаНеСоздана";
Возврат Неопределено;
КонецЕсли;
// в теле запроса, может быть указан url для возврата ответа
НастроитьПеренаправление(ВходящиеСвойстваЗадачи, "success_url", ЗаголовкиРезультат, КодРезультат);
Возврат СвойстваЗадачи
КонецФункции
На этом тему закрываю. Модуль ВалидацияДанных является независимым, может быть включен в состав любой конфигурации и переиспользоваться любым http-сервисом или подсистемой для проверок с выводом текстов ошибок.


Критикуйте, делитесь, предлагайте.
Всем успехов в проектировании http-сервисов!