Русский
Русский
English
Статистика
Реклама

Validation

Перевод Как разобрать URL в JavaScript?

13.07.2020 16:22:43 | Автор: admin


Доброго времени суток, друзья!

Представляю Вашему вниманию перевод заметки How to Parse URL in JavaScript: hostname, pathname, query, hash автора Dmitri Pavlutin.

Унифицированный указатель ресурса или, сокращенно, URL это ссылка на веб-ресурс (веб-страницу, изображение, файл). URL определяет местонахождения ресурса и способ его получения протокол (http, ftp, mailto).

Например, вот URL данной статьи:

https://dmitripavlutin.com/parse-url-javascript

Часто возникает необходимость получить определенные элементы URL. Это может быть название хоста (hostname, dmitripavlutin.com) или путь (pathname, /parse-url-javascript).

Удобным способом получить отдельные компоненты URL является конструктор URL().

В этой статье мы поговорим о структуре и основных компонентах URL.

1. Структура URL


Изображение лучше тысячи слов. На представленном изображении Вы можете видеть основные компоненты URL:



2. Конструктор URL()


Конструктор URL() это функция, позволяющая разбирать (парсить) компоненты URL:

const url = new URL(relativeOrAbsolute [, absoluteBase])

Аргумент relativeOrAbsolute может быть абсолютным или относительным URL. Если первый аргумент относительная ссылка, то второй аргумент, absoluteBase, является обязательным и представляет собой абсолютный URL основу для первого аргумента.

Например, инициализируем URL() с абсолютным URL:

const url = new URL('http://example.com/path/index.html')url.href // 'http://example.com/path/index.html'

Теперь скомбинируем относительный и абсолютный URL:

const url = new URL('/path/index.html', 'http://example.com')url.href // 'http://example.com/path/index.html'

Свойство href экземпляра URL() возвращает переданную URL-строку.

После создания экземпляра URL(), Вы можете получить доступ к компонентам URL. Для справки, вот интерфейс экземпляра URL():

interface URL {  href:     USVString;  protocol: USVString;  username: USVString;  password: USVString;  host:     USVString;  hostname: USVString;  port:     USVString;  pathname: USVString;  search:   USVString;  hash:     USVString;  readonly origin: USVString;  readonly searchParams: URLSearchParams;  toJSON(): USVString;}

Здесь тип USVString означает, что JavaScript должен возвращать строку.

3. Строка запроса (query string)


Свойство url.search позволяет получить строку запроса URL, начинающуюся с префикса ?:

const url = new URL(    'http://example.com/path/index.html?message=hello&who=world')url.search // '?message=hello&who=world'

Если строка запроса отсутствует, url.search возвращает пустую строку (''):

const url1 = new URL('http://example.com/path/index.html')const url2 = new URL('http://example.com/path/index.html?')url1.search // ''url2.search // ''

3.1. Разбор (парсинг) строки запроса

Вместо получения исходной строки запроса, мы можем получать ее параметры.

Легкий способ это сделать предоставляет свойство url.searchParams. Значением данного свойства является экземпляр интерфейса URLSeachParams.

Объект URLSearchParams предоставляет множество методов для работы с параметрами строки запроса (get(param), has(param) и т.д.).

Давайте рассмотрим пример:

const url = new Url(    'http://example.com/path/index.html?message=hello&who=world')url.searchParams.get('message') // 'hello'url.searchParams.get('missing') // null

url.searchParams.get('message') возвращает значение параметра message строки запроса.

Доступ к несуществующему параметру url.searchParams.get('missing') возвращает null.

4. Название хоста (hostname)


Значением свойства url.hostname является название хоста URL:

const url = new URL('http://example.com/path/index.html')url.hostname // 'example.com'

5. Путь (pathname)


Свойство url.pathname содержит путь URL:

const url = new URL('http://example.com/path/index.html?param=value')url.pathname // '/path/index.html'

Если URL не имеет пути, url.pathname возвращает символ /:

const url = new URL('http://example.com/');url.pathname; // '/'

6. Хеш (hash)


Наконец, хеш может быть получен через свойство url.hash:

const url = new URL('http://example.com/path/index.html#bottom')url.hash // '#bottom'

Если хеш отсутствует, url.hash возвращает пустую строку (''):

const url = new URL('http://example.com/path/index.html')url.hash // ''

7. Проверка (валидация) URL


При вызове конструктора new URL() не только создается экземпляр, но также осуществляется проверка переданного URL. Если URL не является валидным, выбрасывается TypeError.

Например, http ://example.com не валидный URL, поскольку после http имеется пробел.

Попробуем использовать этот URL:

try {    const url = new URL('http ://example.com')} catch (error) {    error // TypeError, "Failed to construct URL: Invalid URL"}

Поскольку 'http ://example.com' неправильный URL, как и ожидалось, new URL('http ://example.com') выбрасывает TypeError.

8. Работа с URL


Такие свойства, как search, hostname, pathname, hash доступны для записи.

Например, давайте изменим название хоста существующего URL с red.com на blue.io:

const url = new URL('http://red.com/path/index.html')url.href // 'http://red.com/path/index.html'url.hostname = 'blue.io'url.href // 'http://blue.io/path/index.html'

Свойства origin, searchParams доступны только для чтения.

9. Заключение


Конструктор URL() является очень удобным способом разбора (парсинга) и проверки (валидации) URL в JavaScript.

new URL(relativeOrAbsolute, [, absoluteBase] в качестве первого параметра принимает абсолютный или относительный URL. Если первый параметр является относительным URL, вторым параметром должен быть абсолютный URL основа для первого аргумента.

После создания экземпляра URL(), Вы можете получить доступ к основным компонентам URL:

  • url.search исходная строка запроса
  • url.searchParams экземпляр URLSearchParams для получения параметров строки запроса
  • url.hostname название хоста
  • url.pathname путь
  • url.hash значение хеша
Подробнее..

Проверяем формы по стандартам с Validation API

22.09.2020 06:06:12 | Автор: admin
В свое время мне почти всем нравился Angular 2+, это хорошо спроектированный фреймворк, который на голову выше остальных популярных фронтенд фреймворков по инженерному уровню исполнения. Но были у него и весьма странные недоработки. Одна из них это невозможность ручного вызова валидации или ревалидации формы, которая наблюдалась как минимум, до 8ой версии. Нельзя сказать чтобы там сильно увлекаются реактивностью, но вот в этой подсистеме похоже какие-то реактивные соображения побудили разработчиков реализовать валидацию только через привязку, вынудив разработчиков прикладных решений обращаться к костылям навроде установки состояния нетронуто для полей и вообще усложняя написание сложных валидаторов с продвинутой логикой и участием нескольких полей сразу. Опыт борьбы с ангуляровским валидатором и некоторыми другими особенностями фреймворка усилил мое впечатление от того насколько элегантным и простым оказалось после этого использование HTML5 API для валидации форм, которое просто работает в любом современном браузере даже без подключения фреймворков и библиотек.

Базой для работы валидаторов являются атрибуты элементов. С помощью атрибутов мы сразу можем задать следующие ограничения:
required поле обязательное, т.е. требует заполнения
min max step минимально и максимально допустимые значения, а также шаг изменения
minlength и maxlength ограничители по количеству допустимых символов ввода
pattern регулярное выражение
Вроде бы не густо, однако, pattern дает нам довольно богатые возможности по проверке значений, легко нагугливаются регулярки позволяющие сходу проверять номера телефонов, емейл адреса и урлы и многое другое востребованное.
Расставленные на элементы формы эти атрибуты автоматически не позволят сработать кнопке из той же формы выполняющей submit значений на сервер, правда сегодня такой кейс может показаться многим анахроничным. Но это еще не беда, т.к. с помощью клиентского кода на JavaScript мы можем точно также и даже лучше пользоваться всеми этими валидаторами. Поэтому мы не будем использовать input type=email, а попробуем сделать свое поле с проверкой вводимого на соответствие правилам формирования адресов электронной почты. Сделаем простую форму:
<form name="myform" id="myform">   <input type="text" pattern="^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" placeholder="email here"/>   <input type="submit"></form>

Валидатор сразу работает и на попытку нажимать кнопку выдает предупреждение на языке локали браузера.



Соответственно ввод mail@example.com дает успешный сабмит формы.
Чтобы развить свое поведение надо получить доступ к инстансу формы, это можно сделать через глобальный document по имени, индексу (id) или порядковому номеру начиная с нуля.
<script type="module">   document.forms.myform.onsubmit = (event) => {       console.log('validate');       return false;   };</script>

или по селектору одним из методов, таким как document.getElementById() или document.querySelector()
для проверки результатов запустим http-server
npx http-server

после того как команда отработает, в браузере можно открывать 127.0.0.1:8080/ или тот адрес, что он напишет вам в консоли и отлаживать результаты.

Заменим сабмит на обычную кнопку, и будем вызывать проверку формы вручную, немного изменив пример.
<form id="myform" action="#">   <input type="text" pattern="^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" required placeholder="email here" />   <input type="button" name="subm" value="OK" /></form><script type="module">;   myform.subm.onclick = (event) => {       console.log(form.checkValidity());       return false;   };</script>

В данном примере видно, что мапинг объектов формы по их id и name работает и для дочерних по отношению к форме элементов, что выглядит очень изящно. Теперь наш код выводит в консоль состояние валидности формы.
Наличие методов ручного запуска валидации, не означает, что она не осуществляется без их вызова.
Результаты ввода и других изменений на форме сразу отражаются на ее состоянии, что проявляется в наличии псевдоклассов стилей valid и invalid. Если добавить выделение цветом, то можно заметить как сразу срабатывает валидация.
<style>  :valid {       border: 1px solid green;   }  :invalid {       border: 1px solid red;   }</style>




Для того, чтобы форма не мозолила глаза красным до того как пользователь попробовал что-то в нее вводить, можно использовать лайфхак с плейсхолдером:
<style>   input:valid {       border: 1px solid green;   }   input:not(:placeholder-shown):invalid {       border: 1px solid red;   }</style>

На элементы формы можно навесить внешние обработчики для событий валидации.
<script type="module">   myform.email.oninvalid = (event) => {       alert('Wrong email !!11');   };   myform.subm.onclick = (event) => {       console.log(form.checkValidity());       return false;   };</script>

В данном случае используется механизм хуков по имени события, если есть какое-то событие поддерживаемое элементом, то присвоив ему функцию с именем on + <имя_эвента> мы можем быть уверены, что она будет вызвана когда он сработает.

И еще один замечательный момент тут в том, что они будут вызываться уже не при вводе данных, а только при программном дергании валидации, т.е. вызове метода checkValidity().

Соответственно мы можем, обработать это поведение:
myform.subm.onclick = (event) => {   if (myform.checkValidity()) {       alert('Valid !');   } else {       alert('Invalid !')   }   return false;};

В реальной жизни нам может потребоваться также вызов event.preventDefault() если валидация не пройдена, чтобы прервать процедуру сабмита формы.

У checkValidity() есть аналог reportValidity(), который возвращает результат не вызывая при этом повторной валидации.

А как узнать какое поле неправильное?

Свойство .validity есть у каждого элемента ввода формы как и возможность вызвать на нем методы валидации, свойство имеет следующую структуру:

ValueState: {
valid общий признак корректности значения
valueMissing значение требуется, но не задано
typeMismatch введено неправильное по типу значение
patternMismatch введен не соответствующее шаблону значение
tooLong значение больше чем maxlength
tooShort значение меньше чем minlength
rangeUnderflow значение меньше min
rangeOverflow значение больше max
stepMismatch значение не соответствует шагу
badInput ввод не может быть приведен к значению
customError произвольная ошибка
}

В основном представлены, как мы видим, свойства ошибок соответствующие стандартным атрибутам валидации, тогда как .customError это наш задел для расширения.
Вызвав метод .setCustomValidity() с аргументом в виде строки с текстом ошибки мы можем обозначить элемент формы как невалидный. Установить или получить текст ошибки можно также через свойство .validationMessage.
Чтобы не задавливать браузерные валидации можно использовать свойство .willValidate, которое сообщает будут ли вызваны стандартные валидации на поле.
Передав пустую строку в качестве аргумента .setCustomValidity() мы можем вернуть его состояние к валидному.
Давайте добавим поддержку собственного атрибута my-pattern, который для наглядности будет точно так же проверять значение на соответствие регулярному выражению.
В случае ошибки сообщение, помимо того как это предусмотрено в браузере, будет выводиться рядом с полем
Валидация будет срабатывать на изменении значения нашего альтернативного поля и при нажатии кнопки.
<form id="myform" action="#">   <div>       <input type="text" name="email" id="email" value="" pattern="^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" required placeholder="email here" />       <span class="msg"></span>   </div>   <div>       <input type="text" name="customInput" id="customInput" my-pattern="^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" required placeholder="text here" />       <span class="msg"></span>   </div>   <button type="submit" name="subm" value="OK">OK</button></form><style>   input:valid {       border: 1px solid green;   }   input:not(:placeholder-shown):invalid {       border: 1px solid red;   }</style><script type="module">   myform.customInput.oninvalid = (event) => {       let el = event.target;       let msg = el.parentElement.querySelector('.msg');       msg.innerText = el.validationMessage;       console.log('oninvalid, id: ', el.id);   };   myform.customInput.oninput = (event) => {       let el = event.currentTarget;       validateWithMyPattern(el);       markValidity(el);   };   function markValidity(el) {       el.checkValidity();       let msg = el.parentElement.querySelector('.msg');       if (el.validity.valid) {           msg.innerText = '';       } else {           msg.innerText = el.validationMessage;       }   }   function validateWithMyPattern(field) {       if (field.value) {           if (field.hasAttribute('my-pattern') &&               field.value.match(field.getAttribute('my-pattern'))) {               field.setCustomValidity('');           } else {               field.setCustomValidity('My pattern error');           }       }   }   myform.subm.onclick = (event) => {       for (let formEl of myform.querySelectorAll('input')) {           validateWithMyPattern(formEl);           markValidity(formEl);       }       if (myform.reportValidity()) {           alert('Valid !');       } else {           alert('Invalid !')       }       return false;   };</script>

Теперь у нас два подобных поля проверяющих значение стандартным валидатором и собственноручно написанным.



Возможности могут показаться не богатыми, однако, с помощью них вы можете реализовать любые валидации в т.ч. групп полей не сильно спотыкаясь об технические ограничения как с популярными фреймворками.

Из ограничений Validation API мне более запомнилось только исходная невалидность полей. Для ее кроме хитрости с placeholder или специальными состояниями a-la untouched можно производить всю валидацию программно на событиях input и submit сочетая собственные валидаторы со стандартными.
Решая свои задачи, я пришел к необходимости создать свой компонент, выполняющий задачи формы заодно для поддержки собственных элементов ввода, позволяющий задавать разное поведение валидации и уведомлений и вешать любые валидаторы и использующий при этом стандартизированный Validation API. Посмотреть на него можно вот тут: https://bitbucket.org/techminded/skinny-widgets/src/master/src/form/
а код примера из этой статьи найти вот тут:
https://bitbucket.org/techminded/myform/
Подробнее..

Странные применения валидации

28.12.2020 00:15:11 | Автор: admin

Валидаторы нужны для валидации. Давайте ради забавы забудем об этом. Давайте с помощью валидатора пройдёмся по вложенной структуре данных. Сумасшествие, скажете вы!


image Nordic YES


По чём бежать будем?


Давайте бежать по телефонной книге:


const phoneBook = {  andrew: ["+345356245254", "+313232312312"],  vasilina: ["+132313123123"],  serhiy: ["+587234878234", "+321323124123"],};

Что хотим получить?


Давайте получим список всех номеров.


Как мы это сделаем?


Мы сделаем это в 4 шага:


  • Подключим библиотеку для валидации данных
  • Создадим обычную функцию валидации
  • Добавим побочный эффект собирания номеров в массив
  • Обернём в функцию

Будем использовать библиотеку валидации quartet:


import { v } from "quartet";

Напишем функцию валидации:


const checkPhoneBook = v({  [v.rest]: v.arrayOf(v.string),});

Теперь мы можем проверить, является ли некое значение телефонной книгой:


checkPhoneBook({}); // truecheckPhoneBook({ andrew: ["123321"] }); // truecheckPhoneBook({ andrew: null }); // false

Теперь добавим немного сумасшествия: проверяя элемент списка номеров будем его добавлять в массив.


const phoneNumbers = [];const checkAndCollect = v({  [v.rest]: v.arrayOf(    v.and(      v.string,      v.custom((phoneNumber) => {        phoneNumbers.push(phoneNumber);        return true;      })    )  ),});

Вызовем эту функцию валидации на конкретной телефонной книге:


checkAndCollect({  andrew: ["+345356245254", "+313232312312"],  vasilina: ["+132313123123"],  serhiy: ["+587234878234", "+321323124123"],});

Вернулось true. Но нас это не интересует! Главное: в массиве phoneNumbers теперь хранятся все номера.


console.log(phoneNumbers);// [//   '+345356245254',//   '+313232312312',//   '+132313123123',//   '+587234878234',//   '+321323124123'// ]

Обернём это в функцию для переиспользования:


import { v } from "quartet";/** * @param {Record<string, string[]>} phoneBook * @returns {string[]} phone numbers */function collectPhoneNumbers(phoneBook) {  const phoneNumbers = [];  const checkAndCollect = v({    [v.rest]: v.arrayOf(      v.and(        v.string,        v.custom((phoneNumber) => {          phoneNumbers.push(phoneNumber);          return true;        })      )    ),  });  checkAndCollect(phoneBook);  return phoneNumbers;}

Оценка


Использование таких трюков может быть забавным. Но я бы не писал так в production коде. И по трём причинам:


  • Это не оптимальное решение по скорости работы. Создание функции валидации для итерации по книге номеров лишнее действие.
  • Это не идиоматический код. Библиотека валидации не предназначена для итераций.
  • Есть код более подходящий для этой задачи:

/** * @param {Record<string, string[]>} phoneBook * @returns {string[]} phone numbers */function collectPhoneNumbers(phoneBook) {  const phoneNumbers = [];  const personNames = Object.keys(phoneBook);  for (const personName of personNames) {    const personPhoneNumbers = phoneBook[personName];    phoneNumbers.push(...personPhoneNumbers);  }  return phoneNumbers;}

Послесловие


Вот такую забаву я придумал в Воскресение вечером. А что странного приходит в вашу голову? Напишите в комментах.

Подробнее..
Категории: Javascript , Js , Validation , Iteration , Quartet

React за 60 секунд валидация формы

02.02.2021 08:07:36 | Автор: admin


Доброго времени суток, друзья!

В этом небольшом туториале я хочу продемонстировать вам пример клиент-серверной валидации формы.

Клиент будет реализован на React, сервер на Express.

Мы не будем изобретать велосипеды, а воспользуемся готовыми решениями: для валидации формы на стороне клиента будет использоваться react-hook-form (+: используются хуки, русский язык), а на стороне сервера express-validator.

Для стилизации будет использоваться styled-components (CSS-in-JS или All-in-JS, учитывая JSX).

Исходный код примера находится здесь.

Поиграть с кодом можно здесь.

Без дальнейших предисловий.

Клиент


Создаем проект с помощью create-react-app:

yarn create react-app form-validation# илиnpm init react-app form-validation# илиnpx create-react-app form-validation

В дальнейшем для установки зависимостей и выполнения команд я буду использовать yarn.

Структура проекта после удаления лишних файлов:

public  index.htmlsrc  App.js  index.js  styles.jsserver.js...

Устанавливаем зависимости:

# для клиентаyarn add styled-components react-hook-form# для сервера (производственные зависимости)yarn add express express-validator cors# для сервера (зависимость для разработки)yarn add -D nodemon# для одновременного запуска серверовyarn add concurrently

Поскольку styled-components не умеет импотировать шрифты, нам придется добавить их в public/index.html:

<head>  ...  <link rel="preconnect" href="http://personeltest.ru/aways/fonts.gstatic.com" />  <link    href="http://personeltest.ru/aways/fonts.googleapis.com/css2?family=Comfortaa&display=swap"    rel="stylesheet"  /></head>

Наша форма будет состоять из трех полей: имя пользователя, его адрес электронной почты и пароль. Условия, которым должны удовлетворять данные:

  • Имя
    • от 2 до 10 символов
    • кириллица

  • Email
    • особых требований не предъявляется

  • Пароль
    • 8-12 символов
    • латиница: буквы в любом регистре, цифры, нижнее подчеркивание и дефис


Начнем со стилизации (src/styles.js; для подстветки синтаксиса я использую расширение для VSCode vscode-styled-components):

// импорт инструментовimport styled, { createGlobalStyle } from 'styled-components'// глобальные стилиconst GlobalStyle = createGlobalStyle`  body {    margin: 0;    min-height: 100vh;    display: grid;    place-items: center;    background-color: #1c1c1c;    font-family: 'Comfortaa', cursive;    font-size: 14px;    letter-spacing: 1px;    color: #f0f0f0;  }`// заголовокconst StyledTitle = styled.h1`  margin: 1em;  color: orange;`// формаconst StyledForm = styled.form`  margin: 0 auto;  width: 320px;  font-size: 1.2em;  text-align: center;`// подписьconst Label = styled.label`  margin: 0.5em;  display: grid;  grid-template-columns: 1fr 2fr;  align-items: center;  text-align: left;`// проект поля для ввода данныхconst BaseInput = styled.input`  padding: 0.5em 0.75em;  font-family: inherit;  font-size: 0.9em;  letter-spacing: 1px;  outline: none;  border: none;  border-radius: 4px;`// обычное полеconst RegularInput = styled(BaseInput)`  background-color: #f0f0f0;  box-shadow: inset 0 0 2px orange;  &:focus {    background-color: #1c1c1c;    color: #f0f0f0;    box-shadow: inset 0 0 4px yellow;  }`// поле для отправки данных на серверconst SubmitInput = styled(BaseInput)`  margin: 1em 0.5em;  background-image: linear-gradient(yellow, orange);  cursor: pointer;  &:active {    box-shadow: inset 0 1px 3px #1c1c1c;  }`// проект сообщения с текстомconst BaseText = styled.p`  font-size: 1.1em;  text-align: center;  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);`// сообщение об ошибкеconst ErrorText = styled(BaseText)`  font-size: ${(props) => (props.small ? '0.8em' : '1.1em')};  color: red;`// сообщение об успехеconst SuccessText = styled(BaseText)`  color: green;`// экспорт стилизованных компонентовexport {  GlobalStyle,  StyledTitle,  StyledForm,  Label,  RegularInput,  SubmitInput,  ErrorText,  SuccessText}

Импортируем и подключаем глобальные стили в src/index.js:

import React from 'react'import ReactDOM from 'react-dom'// импортируем глобальные стилиimport { GlobalStyle } from './styles'import App from './App'ReactDOM.render(  <React.StrictMode>    {/* подключаем глобальные стили */}    <GlobalStyle />    <App />  </React.StrictMode>,  document.getElementById('root'))

Переходим к основному файлу клиента (src/App.js):

import { useState } from 'react'// импорт хука для валидации формыimport { useForm } from 'react-hook-form'// импорт стилизованных компонентовimport {  StyledTitle,  StyledForm,  Label,  RegularInput,  SubmitInput,  ErrorText,  SuccessText} from './styles'// компонент заголовкаfunction Title() {  return <StyledTitle>Валидация формы</StyledTitle>}// компонент формыfunction Form() {  // инициализируем начальное состояние  const [result, setResult] = useState({    message: '',    success: false  })  // извлекаем средства валидации:  // регистрация проверяемого поля  // ошибки и обработка отправки формы  const { register, errors, handleSubmit } = useForm()  // общие валидаторы  const validators = {    required: 'Не может быть пустым'  }  // функция отправки формы  async function onSubmit(values) {    console.log(values)    const response = await fetch('http://localhost:5000/server', {      method: 'POST',      headers: {        'Content-Type': 'application/json'      },      body: JSON.stringify(values)    })    const result = await response.json()    // обновляем состояние    setResult({      message: result,      success: response.ok    })  }  // нажатие кнопки сброса полей в исходное состояние приводит к перезагрузке страницы  function onClick() {    window.location.reload()  }  return (    <>      <StyledForm onSubmit={handleSubmit(onSubmit)}>        <Label>          Имя:          <RegularInput            type='text'            name='name'            // поля являются неуправляемыми            // это повышает производительность            ref={register({              ...validators,              minLength: {                value: 2,                message: 'Не менее двух букв'              },              maxLength: {                value: 10,                message: 'Не более десяти букв'              },              pattern: {                value: /[А-ЯЁ]{2,10}/i,                message: 'Только киррилица'              }            })}            defaultValue='Иван'          />        </Label>        {/* ошибки */}        <ErrorText small>{errors.name && errors.name.message}</ErrorText>        <Label>          Email:          <RegularInput            type='email'            name='email'            ref={register({              ...validators,              pattern: {                value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,                message: 'Неправильный адрес электронной почты'              }            })}            defaultValue='email@example.com'          />        </Label>        <ErrorText small>{errors.email && errors.email.message}</ErrorText>        <Label>          Пароль:          <RegularInput            type='password'            name='password'            ref={register({              ...validators,              pattern: {                value: /^[A-Z0-9_-]{8,12}$/i,                message:                  'От 8 до 12 символов: латиница, цифры, нижнее подчеркивание и дефис'              }            })}            defaultValue='password'          />        </Label>        <ErrorText small>          {errors.password && errors.password.message}        </ErrorText>        <SubmitInput type='submit' defaultValue='Отправить' />        {/* обратите внимание на атрибут "as", он позволяет превратить "инпут" в кнопку с аналогичными стилями */}        <SubmitInput as='button' onClick={onClick}>          Сбросить        </SubmitInput>      </StyledForm>      {/* результат отправки формы */}      {result.success ? (        <SuccessText>{result.message}</SuccessText>      ) : (        <ErrorText>{result.message}</ErrorText>      )}    </>  )}export default function App() {  return (    <>      <Title />      <Form />    </>  )}

Метод register() хука useForm() поддерживает все атрибуты тега input. Полный список таких атрибутов. В случае с именем, мы могли бы ограничиться регулярным выражением.

Запускаем сервер для клиента с помощью yarn start и тестируем форму:



Замечательно. Валидация на стороне клиента работает, как ожидается. Но ее всегда можно отключить. Поэтому нужна валидация на сервере.

Сервер


Приступаем к реализации сервера (server.js):

const express = require('express')// body читает тело запроса// validationResult - результат валидацииconst { body, validationResult } = require('express-validator')const cors = require('cors')const app = express()const PORT = process.env.PORT || 5000app.use(cors())app.use(express.json())app.use(express.urlencoded({ extended: false }))// валидаторыconst validators = [  body('name').trim().notEmpty().isAlpha('ru-RU').escape(),  body('email').normalizeEmail().isEmail(),  // кастомный валидатор  body('password').custom((value) => {    const regex = /^[A-Z0-9_-]{8,12}$/i    if (!regex.test(value)) throw new Error('Пароль не соответствует шаблону')    return true  })]// валидаторы передаются в качестве middlewareapp.post('/server', validators, (req, res) => {  // извлекаем массив с ошибками из результата валидации  const { errors } = validationResult(req)  console.log(errors)  // если массив с ошибками не является пустым  if (errors.length) {    res.status(400).json('Регистрация провалилась')  } else {    res.status(201).json('Регистрация прошла успешно')  }})app.listen(PORT, () => {  console.log(`Сервер готов. Порт: ${PORT}`)})

Полный список доступных валидаторов можно посмотреть здесь.

Добавим в package.json парочку скриптов server для запуска сервера и dev для одновременного запуска серверов:

"scripts": {  "start": "react-scripts start",  "build": "react-scripts build",  "server": "nodemon server",  "dev": "concurrently \"yarn server\" \"yarn start\""}

Выполняем yarn dev и тестируем отправку формы:





Прекрасно. Кажется, у нас все получилось.

Мы с вами рассмотрели очень простой вариант клиент-серверной валидации формы. Вместе с тем, более сложные варианты предполагают лишь увеличение количества валидаторов, общие принципы остаются такими же. Также стоит отметить, что валидацию формы на стороне клиента вполне можно реализовать средствами HTML (GitHub, CodeSandbox).

Благодарю за внимание и хорошего дня.
Подробнее..

Из песочницы Валидация данных в C с использованием библиотеки cpp-validator

27.10.2020 12:09:43 | Автор: admin


Казалось бы, валидация данных это одна из базовых задач в программировании, которая встретится и в начале изучения языка вместе с "Hello world!", и в том или ином виде будет присутствовать в множестве зрелых проектов. Тем не менее, Google до сих пор выдает ноль релевантных результатов при попытке найти универсальную библиотеку валидации данных с открытым исходным кодом на C++.


В лучшем случае, там найдутся или инструменты проверки самого кода C++, или библиотеки валидации определенных форматов, например, таких как JSON или XML. Похоже на то, что либо разработчики для каждого случая реализуют валидацию данных вручную, либо инструменты валидации создаются под конкретный проект и плохо приспособлены для использования в качестве универсальных библиотек.


Если в комментариях кто-то сможет привести примеры открытых библиотек валидации данных на C++ помимо отдельных GUI-форм, то буду очень признателен и добавлю соответствующий список в статью.


Содержание



Мотивация


Стоить отметить, что мотивом к разработке валидатора данных для C++ послужило не столько отсутствие подобной библиотеки, сколько желание получить инструмент, при помощи которого можно было бы единообразно описывать:


  • условия выборки записей в базе данных;
  • условия проверки на сервере содержимого команд, поступающих от клиентов;
  • условия проверки данных, вводимых через пользовательский интерфейс на клиенте, перед их записью в объект и отправкой на сервер.

Т.е., если с прикладной точки зрения во всех трех случаях речь идет о разных представлениях одной и той же сущности, то почему бы не сделать так, чтобы правила проверки характеристик этой сущности описывались бы одинаковым образом? И уже потом эти правила обрабатывать разными способами применительно к каждому конкретному случаю например, транслировать в строку запроса к базе данных или в проверку набора содержимого ранее заполненного объекта или в проверку отдельных переменных перед записью в объект.


В итоге, библиотека валидации разрабатывалась с учетом основного требования, чтобы было четкое разделение между:


  • описанием правил валидации;
  • реализацией обработчиков правил валидации;
  • обработкой конкретных правил валидации конкретным обработчиком.

То есть, правила валидации должны предварительно описываться в отдельном месте, желательно с применением синтаксиса, похожего на декларативный. В другом месте должны быть реализованы обработчики правил валидации. Причем, разные обработчики могут транслировать те же самые правила в разные операции например, один обработчик использует правила для фактической проверки данных, а другой обработчик транслирует правила в запросы SQL. И, собственно, третий участок это непосредственно применение правил валидации конкретным обработчиком в момент вызова процедуры валидации.


Возможности библиотеки


cpp-validator является header-only библиотекой для современного C++ с поддержкой стандартов C++14/C++17. В коде cpp-validator активно используется метапрограммирование на шаблонах и библиотека Boost.Hana.


Основные возможности библиотеки cpp-validator перечислены ниже.


  • Валидация данных для различных конструкций языка:
    • простых переменных;
    • свойств объектов, включая:
      • переменные классов;
      • методы классов вида getter;
    • содержимого и свойств контейнеров;
    • иерархических типов данных, таких как вложенные объекты и контейнеры.
  • Пост-валидация объектов, когда проверяется содержимое уже заполненного объекта на соответствие сразу всем правилам.
  • Пре-валидация данных, когда перед записью в объект проверяются только те свойства, которые планируется изменить.
  • Комбинация правил с использованием логических связок AND, OR и NOT.
  • Массовая проверка элементов контейнеров с условиями ALL или ANY.
  • Частично подготовленные правила валидации с отложенной подстановкой аргументов (lazy operands).
  • Сравнение друг с другом разных свойств одного и того же объекта.
  • Автоматическая генерация описания ошибок валидации:
    • широкие возможности по настройке генерации текста ошибок;
    • перевод текста ошибок на различные языки с учетом грамматических атрибутов слов, например, числа, рода и т.д.
  • Расширяемость:
    • регистрация новых свойств объектов, доступных для валидации;
    • добавление новых операторов правил валидации;
    • добавление новых обработчиков правил валидации (адаптеров).
  • Операторы, уже встроенные в библиотеку:
    • сравнения;
    • лексикографические, с учетом и без учета регистра;
    • существования элементов;
    • проверки вхождения в интервал или набор;
    • регулярные выражения.
  • Широкая поддержка платформ и компиляторов, включая компиляторы Clang, GCC, MSVC и операционные системы Windows, Linux, macOS, iOS, Android.

Использование библиотеки


Базовая валидация данных с использованием cpp-validator выполняется в три шага:


  1. сперва создается валидатор, содержащий правила валидации, описанные с использованием почти-декларативного языка;
  2. затем валидатор применяется к объекту валидации;
  3. в конце проверяется результат валидации, для работы с которым может использоваться либо специальный объект ошибки, либо исключение.

// определение валидатораauto container_validator=validator(   _[size](eq,1), // размер контейнера должен быть равен 1   _["field1"](exists,true), // поле "field1" должно существовать в контейнере   _["field1"](ne,"undefined") // поле "field1" должно быть не равно "undefined");// успешная валидацияstd::map<std::string,std::string> map1={{"field1","value1"}};validate(map1,container_validator);// неуспешная валидация, с объектом ошибкиerror_report err;std::map<std::string,std::string> map2={{"field2","value2"}};validate(map2,container_validator,err);if (err){    std::cerr<<err.message()<<std::endl;    /* напечатает:    field1 must exist    */}// неуспешная валидация, с исключениемtry{    std::map<std::string,std::string> map3={{"field1","undefined"}};    validate(map3,container_validator);}catch(const validation_error& ex){    std::cerr<<ex.what()<<std::endl;    /* напечатает:    field1 must be not equal to undefined    */}

При расширенном использовании cpp-validator можно регистрировать новые валидируемые свойства объектов, настраивать генерацию текстовых описаний ошибок, добавлять новые операторы или создавать полностью новые адаптеры-обработчики правил валидаций, которые, вообще говоря, могут использоваться даже не для валидации как таковой, а для выполнения других специфических задач.


Текущий статус библиотеки


Библиотека cpp-validator доступна на GitHub по адресу https://github.com/evgeniums/cpp-validator и готова к использованию на момент написания статьи номер стабильной версии 1.0.2. Библиотека распространяется под лицензией Boost 1.0.


Приветствуются замечания, пожелания и дополнения.


Примеры


Тривиальная валидация числа


// определение валидатораauto v=validator(gt,100); // больше чем 100// объект ошибкиerror err;// условия не выполненыvalidate(90,v,err);if (err){  // валидация неуспешна}// условия выполненыvalidate(200,v,err);if (!err){  // валидация успешна}

Валидация с исключением


// определение валидатораauto v=validator(gt,100); // больше чем 100try{    validate(200,v); // успешно    validate(90,v); // генерирует исключение}catch (const validation_error& err){    std::cerr << err.what() << std::endl;    /* напечатает:    must be greater than 100    */}

Явное применение валидатора к переменной


// определение валидатораauto v=validator(gt,100); // больше чем 100// применить валидатор к переменнымint value1=90;if (!v.apply(value1)){  // валидация неуспешна}int value2=200;if (v.apply(value2)){  // валидация успешна}

Составной валидатор


// валидатор: размер меньше 15 и значение бинарно больше или равно "sample string"auto v=validator(  length(lt,15),  value(gte,"sample string"));// явное применение валидатора к переменнымstd::string str1="sample";if (!v.apply(str1)){  // валидация неупешна потому что sample бинарно меньше, чем sample string}std::string str2="sample string+";if (v.apply(str2)){  // валидация успешна}std::string str3="too long sample string";if (!v.apply(str3)){  // валидация неуспешна, потому что длина строки больше 15 символов}

Проверить, что число входит в интервал, и напечатать описание ошибки


// валидатор: входит в интервал [95,100]auto v=validator(in,interval(95,100));// объект ошибкиerror_report err;// проверить значениеsize_t val=90;validate(val,v,err);if (err){    std::cerr << err.message() << std::endl;     /* напечатает:    must be in interval [95,100]    */}

Составной валидатор для проверки элемента контейнера


// составной валидаторauto v=validator(                _["field1"](gte,"xxxxxx")                 ^OR^                _["field1"](size(gte,100) ^OR^ value(gte,"zzzzzzzzzzzz"))            );// валидация контейнера и печать ошибкиerror_report err;std::map<std::string,std::string> test_map={{"field1","value1"}};validate(test_map,v,err);if (err){    std::cerr << err.message() << std::endl;    /* напечатает:    field1 must be greater than or equal to xxxxxx OR size of field1 must be greater than or equal to 100 OR field1 must be greater than or equal to zzzzzzzzzzzz    */}

Проверить элементы вложенных контейнеров


// составной валидатор элементов вложенных контейнеровauto v=validator(                _["field1"][1](in,range({10,20,30,40,50})),                _["field1"][2](lt,100),                _["field2"](exists,false),                _["field3"](empty(flag,true))            );// валидация вложенного контейнера и печать ошибкиerror_report err;std::map<std::string,std::map<size_t,size_t>> nested_map={            {"field1",{{1,5},{2,50}}},            {"field3",{}}        };validate(nested_map,v,err);if (err){    std::cerr << err.message() << std::endl;    /* напечатает:    element #1 of field1 must be in range [10, 20, 30, 40, 50]    */}

Провести валидацию кастомного свойства объекта


// структура с getter методомstruct Foo{    bool red_color() const    {        return true;    }};// зарегистрировать новое свойство red_colorDRACOSHA_VALIDATOR_PROPERTY_FLAG(red_color,"Must be red","Must be not red");// валидатор зарегистрированного свойства red_colorauto v=validator(    _[red_color](flag,false));// провести валидацию кастомного свойства и напечатать ошибкуerror_report err;Foo foo_instance;validate(foo_instance,v,err);if (err){    std::cerr << err.message() << std::endl;    /* напечатает:    "Must be not red"    */}

Пре-валидация данных перед записью


// структура с переменными и методом вида setterstruct Foo{    std::string bar_value;    uint32_t other_value;    size_t some_size;    void set_bar_value(std::string val)    {        bar_value=std::move(val);    }};using namespace DRACOSHA_VALIDATOR_NAMESPACE;// зарегистрировать кастомные свойстваDRACOSHA_VALIDATOR_PROPERTY(bar_value);DRACOSHA_VALIDATOR_PROPERTY(other_value);// специализация шаблона класса set_member_t для записи свойства bar_value структуры FooDRACOSHA_VALIDATOR_NAMESPACE_BEGINtemplate <>struct set_member_t<Foo,DRACOSHA_VALIDATOR_PROPERTY_TYPE(bar_value)>{    template <typename ObjectT, typename MemberT, typename ValueT>    void operator() (            ObjectT& obj,            MemberT&&,            ValueT&& val        ) const    {        obj.set_bar_value(std::forward<ValueT>(val));    }};DRACOSHA_VALIDATOR_NAMESPACE_END// валидатор с кастомными свойствамиauto v=validator(    _[bar_value](ilex_ne,"UNKNOWN"), // лексикографическое "не равно" без учета регистра    _[other_value](gte,1000) // больше или равно 1000);Foo foo_instance;error_report err;// запись валидного значение в свойство bar_value объекта foo_instanceset_validated(foo_instance,bar_value,"Hello world",v,err);if (!err){    // свойство bar_value объекта foo_instance успешно записано}// попытка записи невалидного значение в свойство bar_value объекта foo_instanceset_validated(foo_instance,bar_value,"unknown",v,err);if (err){    // запись не удалась    std::cerr << err.message() << std::endl;    /* напечатает:     bar_value must be not equal to UNKNOWN     */}

Один и тот же валидатор для пост-валидации и пре-валидации


#include <iostream>#include <dracosha/validator/validator.hpp>#include <dracosha/validator/validate.hpp>using namespace DRACOSHA_VALIDATOR_NAMESPACE;namespace validator_ns {// зарегистрировать getter свойства "x"DRACOSHA_VALIDATOR_PROPERTY(GetX);// валидатор GetXauto MyClassValidator=validator(   /*    "x" в кавычках - это имя поля, которое писать в отчете вместо GetX;   interval.open() - модификатор открытого интервала без учета граничных точек   */   _[GetX]("x")(in,interval(0,500,interval.open())) );}using namespace validator_ns;// определение тестового класса  class MyClass {  double x;public:  // Конструктор с пост-валидацией  MyClass(double _x) : x(_x) {      validate(*this,MyClassValidator);  }  // Getter  double GetX() const noexcept  {     return _x;  }  // Setter с пре-валидацией  void SetX(double _x) {    validate(_[validator_ns::GetX],_x,MyClassValidator);    x = _x;  }};int main(){// конструктор с валидным аргументомtry {    MyClass obj1{100.0}; // ok}catch (const validation_error& err){}// конструктор с невалидным аргументомtry {    MyClass obj2{1000.0}; // значение вне интервала}catch (const validation_error& err){    std::cerr << err.what() << std::endl;    /*     напечатает:     x must be in interval(0,500)    */}MyClass obj3{100.0};// запись с валидным аргументомtry {    obj3.SetX(200.0); // ok}catch (const validation_error& err){}// попытка записи с невалидным аргументомtry {    obj3.SetX(1000.0); // значение вне интервала}catch (const validation_error& err){    std::cerr << err.what() << std::endl;    /*     напечатает:     x must be in interval (0,500)    */}return 0;}

Перевод ошибок валидации на русский язык


// переводчик ключей контейнера на русский язык с учетом рода, падежа и числаphrase_translator tr;tr["password"]={                    {"пароль"},                    {"пароля",grammar_ru::roditelny_padezh}               };tr["hyperlink"]={                    {{"гиперссылка",grammar_ru::zhensky_rod}},                    {{"гиперссылки",grammar_ru::zhensky_rod},grammar_ru::roditelny_padezh}                };tr["words"]={                {{"слова",grammar_ru::mn_chislo}}            };/* финальный переводчик включает в себя встроенный переводчик на русскийvalidator_translator_ru() и переводчик tr для имен элементов*/auto tr1=extend_translator(validator_translator_ru(),tr);// контейнер для валидацииstd::map<std::string,std::string> m1={    {"password","123456"},    {"hyperlink","zzzzzzzzz"}};// адаптер с генерацией отчета об ошибке на русском языкеstd::string rep;auto ra1=make_reporting_adapter(m1,make_reporter(rep,make_formatter(tr1)));// различные валидаторы и печать ошибок на русском языкеauto v1=validator(    _["words"](exists,true) );if (!v1.apply(ra1)){    std::cerr<<rep<<std::endl;    /*    напечатает:    слова должны существовать    */}rep.clear();auto v2=validator(    _["hyperlink"](eq,"https://www.boost.org") );if (!v2.apply(ra1)){    std::cerr<<rep<<std::endl;    /*    напечатает:    гиперссылка должна быть равна https://www.boost.org    */}rep.clear();auto v3=validator(    _["password"](length(gt,7)) );if (!v3.apply(ra1)){    std::cerr<<rep<<std::endl;    /*    напечатает:    длина пароля должна быть больше 7    */}rep.clear();auto v4=validator(    _["hyperlink"](length(lte,7)) );if (!v4.apply(ra1)){    std::cerr<<rep<<std::endl;    /*    напечатает:    длина гиперссылки должна быть меньше или равна 7    */}rep.clear();
Подробнее..

Json api сервис на aiohttp middleware и валидация

28.02.2021 18:05:07 | Автор: admin

В этой статье я опишу один из подходов для создания json api сервиса с валидацией данных.


Сервис будет реализован на aiohttp. Это современный, постоянно развивающийся фреймворк на языке python, использующий asyncio.


Об аннотациях:


Появление аннотаций в python позволило сделать код более понятным. Так же, аннотации открывают некоторые дополнительные возможности. Именно аннотации играют ключевую роль при валидации данных у обработчиков api-методов в этой статье.


Используемые библиотеки:


  • aiohttp фреймворк для создания web-приложений
  • pydantic классы, которые позволяют декларативно описывать данные и валидировать их
  • valdec декоратор для валидации аргументов и возвращаемых значений у функций

Оглавление:



1. Файлы и папки приложения


- sources - Папка с кодом приложения    - data_classes - Папка с модулями классов данных        - base.py - базовый класс данных        - person.py - классы данных о персоне        - wraps.py - классы данных оболочек для запросов/ответов    - handlers - Папка с модулями обработчиков запросов        - kwargs.py - обработчики для примера работы с `KwargsHandler.middleware`        - simple.py - обработчики для примера работы с `SimpleHandler.middleware`        - wraps.py - обработчики для примера работы с `WrapsKwargsHandler.middleware`    - middlewares - Папка с модулями для middlewares        - exceptions.py - классы исключений        - kwargs_handler.py - класс `KwargsHandler`        - simple_handler.py - класс `SimpleHandler`        - utils.py - вспомогательные классы и функции для middlewares        - wraps_handler.py - класс `WrapsKwargsHandler`    - requirements.txt - зависимости приложения    - run_kwargs.py - запуск с `KwargsHandler.middleware`    - run_simple.py - запуск c `SimpleHandler.middleware`    - run_wraps.py - запуск c `WrapsKwargsHandler.middleware`    - settings.py - константы с настройками приложения- Dockerfile - докерфайл для сборки образа

Код доступен на гитхаб: https://github.com/EvgeniyBurdin/api_service


2. json middlewares


middleware в aiohttp.web.Application() является оболочкой для обработчиков запросов.


Если в приложении используется middleware, то поступивший запрос сначала попадает в неё, и только потом передается в обработчик. Обработчик формирует и отдает ответ. Этот ответ снова сначала попадает в middleware и уже она отдает его наружу.


Если в приложении используются нескольно middleware, то каждая из них добавляет новый уровень вложенности.


Между middleware и обработчиком не обязательно должны передаваться "запрос" и "ответ" в виде web.Request и web.Response. Допускается передавать любые данные.


Таким образом, в middleware можно выделить действия над запросами/ответами, которые будут одинаковыми для всех обработчиков.


Это довольно упрощенное описание, но достаточное для понимания того что будет дальше.


2.1. Простая middleware для json сервиса


Обычно, объявление обработчика запроса в приложении aiohttp.web.Application() выглядит, примерно, так:


from aiohttp import webasync def some_handler(request: web.Request) -> web.Response:    data = await request.json()    ...    text = json.dumps(some_data)    ...    return web.Response(text=text, ...)

Для доступа к данным обработчику необходимо "вытащить" из web.Request объект, который был передал в json. Обработать его, сформировать объект с данными для ответа. Закодировать ответ в строку json и отдать "наружу" web.Response (можно отдать и сразу web.json_response()).


2.1.1. Объявление обработчика


Все обработчики нашего приложения должны выполнять подобные шаги. Поэтому, имеет смысл создать middleware, которая возьмет на себя одинаковые действия по подготовке данных и обработке ошибок, а сами обработчики бы стали такими:


from aiohttp import webasync def some_handler(request: web.Request, data: Any) -> Any:    ...    return some_data

Каждый из обработчиков имеет два позиционных аргумента. В первый будет передан оригинальный экземпляр web.Request (на всякий случай), во второй уже готовый объект python, с полученными данными.


В примере, второй аргумент имеет такое объявление: data: Any. Имя у него может быть любым (как и у первого аргумента), а вот в аннотации лучше сразу указать тип объекта, который "ждет" обработчик. Это пожелание справедливо и для возврата.


То есть, в реальном коде, объявление обработчика может быть таким:


from aiohttp import webfrom typing import Union, Listasync def some_handler(    request: web.Request, data: Union[str, List[str]]) -> List[int]:    ...    return some_data

2.1.2. Класс SimpleHandler для middleware


Класс SimpleHandler реализует метод для самой middleware и методы, которые впоследствии помогут изменять/дополнять логику работы middleware (ссылка на код класса).


Остановлюсь подробнее только на некоторых.


2.1.2.1. Метод middleware

    @web.middleware    async def middleware(self, request: web.Request, handler: Callable):        """ middleware для json-сервиса.        """        if not self.is_json_service_handler(request, handler):            return await handler(request)        try:            request_body = await self.get_request_body(request, handler)        except Exception as error:            response_body = self.get_error_body(request, error)            status = 400        else:            # Запуск обработчика            response_body, status = await self.get_response_body_and_status(                request, handler, request_body            )        finally:            # Самостоятельно делаем дамп объекта python (который находится в            # response_body) в строку json.            text, status = await self.get_response_text_and_status(                request, response_body, status            )        return web.Response(            text=text, status=status, content_type="application/json",        )

Именно этот метод надо будет добавить в список middlewares в процессе создания приложения.


Например, так:


    ...    app = web.Application()    service_handler = SimpleHandler()    app.middlewares.append(service_handler.middleware)    ...

2.1.2.2. Метод для получения данных ответа с ошибкой

Так как у нас json сервис, то, желательно, чтобы ошибки во входящих данных (с кодом 400), и внутренние ошибки сервиса (с кодом 500), отдавались в формате json.


Для этого создан метод формирования "тела" для ответа с ошибкой:


    def get_error_body(self, request: web.Request, error: Exception) -> dict:        """ Отдает словарь с телом ответа с ошибкой.        """        return {"error_type": str(type(error)), "error_message": str(error)}

Хочу обратить внимание на то, что этот метод должен отработать без исключений и вернуть объект с описанием ошибки, который можно кодировать в json. Если работа этого метода завершиться исключением, то мы не увидим json в теле ответа.


2.1.2.3. Метод запуска обработчика

В текущем классе он очень простой:


    async def run_handler(        self, request: web.Request, handler: Callable, request_body: Any    ) -> Any:        """ Запускает реальный обработчик, и возвращает результат его работы.        """        return await handler(request, request_body)

Запуск выделен в отдельный метод для того, чтобы можно было добавить логику до/после выполнения самого обработчика.


2.1.3. Примеры


Имеется такой обработчик:


async def some_handler(request: web.Request, data: dict) -> dict:    return data

Будем посылать запросы на url этого обработчика.


текст примеров...
2.1.3.1. Ответ с кодом 200

Запрос POST на /some_handler:


{    "name": "test",    "age": 25}

ожидаемо вернет ответ с кодом 200:


{    "name": "test",    "age": 25}

2.1.3.2. Ответ с кодом 400

Сделаем ошибку в теле запроса.


Запрос POST на /some_handler:


{    "name": "test", 111111111111    "age": 25}

Теперь ответ сервиса выглядит так:


{    "error_type": "<class 'json.decoder.JSONDecodeError'>",    "error_message": "Expecting property name enclosed in double quotes: line 2 column 21 (char 22)"}

2.1.3.3. Ответ с кодом 500

Добавим в код обработчика исключение (эмулируем ошибку сервиса).


async def handler500(request: web.Request, data: dict) -> dict:    raise Exception("Пример ошибки 500")    return data

Запрос POST на /handler500:


{    "name": "test",    "age": 25}

в ответ получит такое:


{    "error_type": "<class 'Exception'>",    "error_message": "Пример ошибки 500"}

2.2. middleware для "kwargs-обработчиков"


middleware из предыдущего раздела уже можно успешно использовать.


Но проблема дублирования кода в обработчиках не решена до конца.


Рассмотрим такой пример:


async def some_handler(request: web.Request, data: dict) -> dict:    storage = request.app["storage"]    logger = request.app["logger"]    user_id = request.match_info["user_id"]    # и т.д. и т.п...    return data

Так как storage, или logger (или что-то еще), могут быть нужны и в других обработчиках, то везде придется "доставать" их одинаковым образом.


2.2.1. Объявление обработчика


Хотелось бы, чтобы обработчики объявлялись, например, так:


async def some_handler_1(data: dict) -> int:    # ...    return some_dataasync def some_handler_2(storage: StorageClass, data: List[int]) -> dict:    # ...    return some_dataasync def some_handler_3(    data: Union[dict, List[str]], logger: LoggerClass, request: web.Request) -> str:    # ...    return some_data

То есть, чтобы нужные для обработчика сущности объявлялись в его сигнатуре и сразу были бы доступны в коде.


2.2.2. Вспомогательный класс ArgumentsManager


Про нужные для обработчика сущности должна знать middleware, чтобы она смогла "вытащить" небходимые для обработчика и "подсунуть" ему при вызове.


За регистрацию, хранение и "выдачу" таких сущностей отвечает класс ArgumentsManager. Он объявлен в модуле middlewares/utils.py (ссылка на код класса).


Для хранения связи "имя аргумента" "действие по извлечению значения для аргумента" в этом классе определен простой словарь, где ключем является "имя аргумента", а значением ссылка на метод, который будет вызван для извлечения "значения аргумента".


Звучит немного запутано, но на самом деле всё просто:


@dataclassclass RawDataForArgument:    request: web.Request    request_body: Any    arg_name: Optional[str] = Noneclass ArgumentsManager:    """ Менеджер для аргументов обработчика.        Связывает имя аргумента с действием, которое надо совершить для        получения значения аргумента.    """    def __init__(self) -> None:        self.getters: Dict[str, Callable] = {}    # Тело json запроса ------------------------------------------------------    def reg_request_body(self, arg_name) -> None:        """ Регистрация имени аргумента для тела запроса.        """        self.getters[arg_name] = self.get_request_body    def get_request_body(self, raw_data: RawDataForArgument):        return raw_data.request_body    # Ключи в request --------------------------------------------------------    def reg_request_key(self, arg_name) -> None:        """ Регистрация имени аргумента который хранится в request.        """        self.getters[arg_name] = self.get_request_key    def get_request_key(self, raw_data: RawDataForArgument):        return raw_data.request[raw_data.arg_name]    # Ключи в request.app ----------------------------------------------------    def reg_app_key(self, arg_name) -> None:        """ Регистрация имени аргумента который хранится в app.        """        self.getters[arg_name] = self.get_app_key    def get_app_key(self, raw_data: RawDataForArgument):        return raw_data.request.app[raw_data.arg_name]    # Параметры запроса ------------------------------------------------------    def reg_match_info_key(self, arg_name) -> None:        """ Регистрация имени аргумента который приходит в параметрах запроса.        """        self.getters[arg_name] = self.get_match_info_key    def get_match_info_key(self, raw_data: RawDataForArgument):        return raw_data.request.match_info[raw_data.arg_name]    # Можно добавить и другие регистраторы...

Регистрация имен аргументов выполняется при создании экземпляра web.Application():


# ...app = web.Application()arguments_manager = ArgumentsManager()# Регистрация имени аргумента обработчика, в который будут передаваться# данные полученные из json-тела запросаarguments_manager.reg_request_body("data")# Регистрация имени аргумента обработчика, в который будет передаваться# одноименный параметр запроса из словаря request.match_infoarguments_manager.reg_match_info_key("info_id")# В приложении будем использовать хранилище# (класс хранилища "взят с потолка" и здесь просто для примера)app["storage"] = SomeStorageClass(login="user", password="123")# Регистрация имени аргумента обработчика, в который будет передаваться# экземпляр хранилищаarguments_manager.reg_app_key("storage")# ...

Теперь экземпляр ArgumentsManager хранит информацию о возможных аргументах обработчиков. Он передается при создании экземпляра класса для middleware:


...service_handler = KwargsHandler(arguments_manager=arguments_manager)app.middlewares.append(service_handler.middleware)...

Сейчас менеджер очень простой. Можно добавить в него регистрацию сразу нескольких ключей одного вида, правила для разрешения конфликтов имен, и проч например, и то, что потом можно будет использовать при сборке документации.


2.2.3. Класс KwargsHandler для middleware


Класс KwargsHandler является наследником SimpleHandler и расширяет его возможности тем, что позволяет создавать обработчики согласно требованию п.2.2.1.


В этом классе переопределяется один метод run_handler, и добавляется еще два make_handler_kwargs и build_error_message_for_invalid_handler_argument (ссылка на код класса).


2.2.3.1. Метод запуска обработчика

Переопределяется метод родительского класса:


    async def run_handler(        self, request: web.Request, handler: Callable, request_body: Any    ) -> Any:        """ Запускает реальный обработчик, и возвращает результат его работы.            (Этот метод надо переопределять, если необходима дополнительная            обработка запроса/ответа/исключений)        """        kwargs = self.make_handler_kwargs(request, handler, request_body)        return await handler(**kwargs)

Как можно заметить, теперь аргументы в обработчик передаются именованными. Таким образом, в обработчиках становится не важен порядок следования аргументов в сигнатуре. Но стали важны сами имена аргументов.


2.2.3.2. Метод формирования словаря с именами аргументов и их значениями

Метод make_handler_kwargs был добавлен в текущий класс. Он реализует заполнение словаря с именами аргументов и их значениями, который будет потом использован при вызове обработчика. Заполнение словаря происходит при помощи уже подготовленного экземпляра ArgumentsManager.


Напомню, что в сигнатурах обработчиков сейчас можно использовать только имена аргументов, которые были зарегистрированы в экземпляре класса ArgumentsManager.


Но у этого требования есть одно исключение. А именно, аргумент с экземпляром web.Request может иметь в сигнатуре обработчика любое имя, но он обязательно должен иметь аннотацию типом web.Request (например, r: web.Request или req: web.Request или request: web.Request). То есть, экземпляр web.Request "зарегистрирован" по умолчанию, и может быть использован в любом обработчике.


И еще одно замечание: все аргументы обработчика должны иметь аннотацию.


Метод build_error_message_for_invalid_handler_argument просто формирует строку с сообщением об ошибке. Он создан для возможности изменить сообщение на свой вкус.


2.2.4. Примеры


Сигнатуры методов такие:


async def create(    data: Union[dict, List[dict]], storage: dict,) -> Union[dict, List[dict]]:    # ...async def read(storage: dict, data: str) -> dict:    # ...async def info(info_id: int, request: web.Request) -> str:    # ...

Первые два обслуживают POST запросы, последний GET (просто, для примера)


текст примеров...
2.2.4.1. Метод /create

Запрос:


[    {        "name": "Ivan"    },    {        "name": "Oleg"    }]

Ответ:


[    {        "id": "5730bab1-9c1b-4b01-9979-9ad640ea5fc1",        "name": "Ivan"    },    {        "id": "976d821a-e871-41b4-b5a2-2875795d6166",        "name": "Oleg"    }]

2.2.4.2. Метод /read

Запрос:


"5730bab1-9c1b-4b01-9979-9ad640ea5fc1"

Ответ:


{    "id": "5730bab1-9c1b-4b01-9979-9ad640ea5fc1",    "name": "Ivan"}

Примечание: читайте данные с одним из UUID которые получили в предыдущем примере, иначе будет ответ с ошибкой 500 PersonNotFound.


2.2.4.3. Метод /info/{info_id}

Запрос GET на /info/123:


"any json"

Ответ:


"info_id=123 and request=<Request GET /info/123 >"

2.3. middleware c оболочками запроса/ответа и валидацией


Иногда, требования для api-сервиса включают в себя стандартизированные оболочки для запросов и ответов.


Например, тело запроса к методу create может быть таким:


{    "data": [        {            "name": "Ivan"        },        {            "name": "Oleg"        }    ],    "id": 11}

а ответ таким:


{    "success": true,    "result": [        {            "id": "9738d8b8-69da-40b2-8811-b33652f92f1d",            "name": "Ivan"        },        {            "id": "df0fdd43-4adc-43cd-ac17-66534529d440",            "name": "Oleg"        }    ],    "id": 11}

То есть, данные для запроса в ключе data а от ответа в result.


Имеется ключ id, который в ответе должен иметь такое же значение как и в запросе.


Ключ ответа success является признаком успешности запроса.


А если запрос закончился неудачно, то ответ может быть таким:


Запрос к методу read:


{    "data":  "ddb0f2b1-0179-44b7-b94d-eb2f3b69292d",    "id": 3}

Ответ:


{    "success": false,    "result": {        "error_type": "<class 'handlers.PersonNotFound'>",        "error_message": "Person whith id=ddb0f2b1-0179-44b7-b94d-eb2f3b69292d not found!"    },    "id": 3}

Уже представленные классы для json middleware позволяют добавить логику работы с оболочками в новый класс для middleware. Надо будет дополнить метод run_handler, и заменить (или дополнить) метод get_error_body.


Таким образом, в обработчики будут "прилетать" только данные, необходимые для их работы (в примере это значение ключа data). Из обработчиков будет возвращаться только положительный результат (значение ключа result). А исключения будет обрабатывать middleware.


Так же, если это необходимо, можно добавить и валидацию данных.


Чтобы "два раза не вставать", я сразу покажу как добавить и оболочки и валидацию. Но сначала необходимо сделать некоторые пояснения по выбранным инструментам.


2.3.1. Класс данных pydantic.BaseModel


pydantic.BaseModel позволяет декларативно объявлять данные.


При создании экземпляра происходит валидация данных по их аннотациям (и не только). Если валидация провалилась поднимается исключение.


Небольшой пример:


from pydantic import BaseModelfrom typing import Union, Listclass Info(BaseModel):    foo: intclass Person(BaseModel):    name: str    info: Union[Info, List[Info]]kwargs = {"name": "Ivan", "info": {"foo": 0}}person = Person(**kwargs)assert person.info.foo == 0kwargs = {"name": "Ivan", "info": [{"foo": 0}, {"foo": 1}]}person = Person(**kwargs)assert person.info[1].foo == 1kwargs = {"name": "Ivan", "info": {"foo": "bar"}}  # <- Ошибка, str не intperson = Person(**kwargs)# Возникло исключение:# ...# pydantic.error_wrappers.ValidationError: 2 validation errors for Person# info -> foo#  value is not a valid integer (type=type_error.integer)# info#  value is not a valid list (type=type_error.list)

Тут мы видим, что после успешной валидации, поля экземпляра получают значения входящих данных. То есть, был словарь, стал экземпляр класса.


В аннотациях к полям мы можем использовать алиасы из typing.


Если в аннотации к полю присутствует класс-потомок pydantic.BaseModel, то данные "маппятся" и в него (и так с любой вложенностью хотя, на счет "любой" не проверял).


Провал валидации сопровождается довольно информативным сообщением об ошибке. В примере мы видим, что на самом деле было две ошибки: info.foo не int, и info не list, что соответствует аннотации и сопоставленному с ней значению.


При использовании pydantic.BaseModel есть нюансы, на которые я хочу обратить внимание.


2.3.1.1. Строгие типы

Если в любом из приведенных выше примеров заменить целое на строку, содержащую только цифры, то валидация всё равно закончится успешно:


kwargs = {"name": "Ivan", "info": {"foo": "0"}}person = Person(**kwargs)assert person.info.foo == 0

То есть, имеем неявное приведение типов. И такое встречается не только с str->int (более подробно про типы pydantic см. в документации).


Приведение типов, в определенных ситуациях, может оказаться полезным, например строка с UUID -> UUID. Но, если приведение некоторых типов недопустимо, то в аннотациях надо использовать типы, наименование у которых начинается со Strict.... Например, pydantic.StrictInt, pydantic.StrictStr, и т.п...


2.3.1.2. Строгая сигнатура при создании экземпляра

Если, для определенных выше классов, попробовать выполнить такой пример:


kwargs = {"name": "Ivan", "info": {"foo": 0}, "bar": "BAR"}person = Person(**kwargs)

То создание экземпляра пройдет без ошибок.


Это тоже может оказаться не тем, что ожидаешь по умолчанию.


Для строгой проверки аргументов, при создании экземпляра, необходимо переопределить базовый класс:


from pydantic import BaseModel, Extra, StrictInt, StrictStrfrom typing import Union, Listclass BaseApi(BaseModel):    class Config:        # Следует ли игнорировать (ignore), разрешать (allow) или        # запрещать (forbid) дополнительные атрибуты во время инициализации        # модели, подробнее:        # https://pydantic-docs.helpmanual.io/usage/model_config/        extra = Extra.forbidclass Info(BaseApi):    foo: StrictIntclass Person(BaseApi):    name: StrictStr    info: Union[Info, List[Info]]kwargs = {"name": "Ivan", "info": {"foo": 0}, "bar": "BAR"}person = Person(**kwargs)# ...# pydantic.error_wrappers.ValidationError: 1 validation error for Person# bar#   extra fields not permitted (type=value_error.extra)

Теперь все нормально, валидация провалилась.


2.3.2. Декоратор valdec.validate


Декоратор valdec.validate позволяет валидировать аргументы и/или возвращаемое значение функции или метода.


Можно валидировать только те аргументы, для которых указана аннотация.


Если у возврата нет аннотации, то считается что функция должна вернуть None (имеет аннотацию -> None:).


Определен декоратор как для обычных функций/методов:


from valdec.decorators import validate@validate  # Валидируем все аргументы с аннотациями, и возвратdef foo(i: int, s: str) -> int:    return i@validate("i", "s")  # Валидируем только "i" и "s"def bar(i: int, s: str) -> int:    return i

так и для асинхронных.


# Импортируем асинхронный вариантfrom valdec.decorators import async_validate as validate@validate("s", "return", exclude=True)  # Валидируем только "i"async def foo(i: int, s: str) -> int:    return int(i)@validate("return")  # Валидируем только возвратasync def bar(i: int, s: str) -> int:    return int(i)

2.3.2.1. Функции-валидаторы

Декоратор получает данные об аргументах/возврате функции, и передает их в функцию-валидатор (это сильно упрощенно, но по сути так), которая и производит, непосредственно, валидацию.


Сигнатура функции-валидатора:


def validator(    annotations: Dict[str, Any],    values: Dict[str, Any],    is_replace: bool,    extra: dict) -> Optional[Dict[str, Any]]:

Аргументы:


  • annotations Словарь, который содержит имена аргументов и их аннотации.
  • values Словарь, который содержит имена аргументов и их значения.
  • is_replace управляет тем, что возвращает функция-валидатор, а именно возвращать отвалидированные значения или нет.
    • Если True, то функция должна вернуть словарь с именами отвалидированных аргументов и их значениями после валидации. Таким образом, например, если у аргумента была аннотация с наследником BaseModel и данные для него поступили в виде словаря, то они будут заменены на экземпляр BaseModel, и в декорируемой функции к ним можно будет обращаться "через точку".
    • Если параметр равен False, то функция вернет None, а декорируемая функция получит оригинальные данные (то есть, например, словарь так и останется словарем, а не станет экземпляром BaseModel).
  • extra Словарь с дополнительными параметрами.

По умолчанию, в декораторе validate используется функция-валидатор на основе pydantic.BaseModel.


В ней происходит следующее:


  • На основании словаря с именами аргументов и их аннотаций создается класс данных (потомок pydantic.BaseModel)
  • Создается экземпляр этого класса в который передается словарь с именами и значениями. В этот момент и происходит валидация.
  • Возвращает функция аргументы после валидации (которые уже буду содержать значения из созданного экземпляра), или ничего не возвращает, зависит от аргумента is_replace.

Вызов функции происходит один раз для всех аргументов, и второй раз, отдельно, для возврата. Конечно, если есть что валидировать, как в первом, так и во втором случае.


Функция-валидатор может быть реализована на основе любого валидирующего класса (в репозитарии valdec есть пример реализации на ValidatedDC). Но необходимо учесть следующее: далее в статье, я буду использовать потомков pydantic.BaseModel в аннотациях аргументов у обработчиков. Соответственно, при другом валидирующем классе, в аннотациях необходимо будет указывать потомков этого "другого" класса.


2.3.2.2. Настройка декоратора

По умолчанию, декоратор "подменяет" исходные данные на данные экземпляра валидирующего класса:


from typing import List, Optionalfrom pydantic import BaseModel, StrictInt, StrictStrfrom valdec.decorators import validateclass Profile(BaseModel):    age: StrictInt    city: StrictStrclass Student(BaseModel):    name: StrictStr    profile: Profile@validate("group")def func(group: Optional[List[Student]] = None):    for student in group:        assert isinstance(student, Student)        assert isinstance(student.name, str)        assert isinstance(student.profile.age, int)data = [    {"name": "Peter", "profile": {"age": 22, "city": "Samara"}},    {"name": "Elena", "profile": {"age": 20, "city": "Kazan"}},]func(data)

Обратите внимание на assert'ы.


Это работает и для возврата:


@validate  # Валидируем всёdef func(group: Optional[List[Student]] = None, i: int) -> List[Student]:    #...    return [        {"name": "Peter", "profile": {"age": 22, "city": "Samara"}},        {"name": "Elena", "profile": {"age": 20, "city": "Kazan"}},    ]

Здесь, несмотря на то, что в return явно указан список словарей, функция вернет список экземпляров Student (подмену выполнит декоратор).


Но Нам не всегда надо именно такое поведение. Иногда, бывает полезно отвалидировать, а данные не подменять (например, если речь о входящих данных, чтобы сразу отдать их в БД). И этого можно добиться изменив настройки декоратора:


from valdec.data_classes import Settingsfrom valdec.decorators import validate as _validatefrom valdec.validator_pydantic import validatorcustom_settings = Settings(    validator=validator,     # Функция-валидатор.    is_replace_args=False,   # Делать ли подмену в аргументах    is_replace_result=False, # Делать ли подмену в результате    extra={}                 # Дополнительные параметры, которые будут                             # передаваться в функцию-валидатор)# Определяем новый декораторdef validate_without_replacement(*args, **kwargs):    kwargs["settings"] = custom_settings    return _validate(*args, **kwargs)# Используем@validate_without_replacementdef func(group: Optional[List[Student]] = None, i: int) -> List[Student]:    #...    return [        {"name": "Peter", "profile": {"age": 22, "city": "Samara"}},        {"name": "Elena", "profile": {"age": 20, "city": "Kazan"}},    ]

И теперь func вернет список словарей, так как is_replace_result=False. И получит тоже список словарей, так как is_replace_args=False.


Но сама валидация данных будет работать как и раньше, не будет лишь подмены.


Есть один нюанс линтер, иногда, может "ругаться" на различия в типах. Да, это минус. Но всегда лучше иметь выбор, чем его не иметь.


Может возникнуть вопрос что будет, если декоратор получит, допустим, не словарь, а уже готовый экземпляр класса? Ответ будет выполнена обычная проверка экземпляра на соответствие типу.


Как можно заметить, в настройках указывается и функция-валидатор, и если вы захотите использовать свою именно там нужно ее подставить.


2.3.2.3. Еще раз про приведение типов

Рассмотрим такой пример применения декоратора:


from valdec.decorators import validate@validatedef foo(i: int):    assert isinstance(i, int)foo("1")

Мы вызываем функцию и передаем ей строку. Но валидация прошла успешно, и в функцию прилетело целое.


Как я уже говорил, по умолчанию, в декораторе validate, используется функция-валидатор на основе pydantic.BaseModel. В п.2.3.1.1. можно еще раз почитать про неявное приведение типов в этом классе.


В нашем же примере, для того чтобы получить желаемое поведение (ошибку валидации), необходимо сделать так:


from valdec.decorators import validatefrom pydantic import StrictInt@validatedef foo(i: StrictInt):    passfoo("1")# ...# valdec.errors.ValidationArgumentsError: Validation error# <class 'valdec.errors.ValidationError'>: 1 validation error for# argument with the name of:# i#  value is not a valid integer (type=type_error.integer).

Вывод такой: Используя декоратор на основе валидирующего класса, аннотации к аргументам функции надо писать по правилам этого класса.


Не забывайте про это.


2.3.2.4. Исключения

  • valdec.errors.ValidationArgumentsError "поднимается" если валидация аргументов функции потерпела неудачу
  • valdec.errors.ValidationReturnError если не прошел валидацию возврат

Само сообщение с описанием ошибки берется из валидирующего класса. В нашем примере это сообщение об ошибке от pydantic.BaseModel.


2.3.3. Базовый класс данных


Как я уже говорил, в этой статье используем классы-наследники от pydantic.BaseModel.


Cначала обязательно определим базовый класс данных:


data_classes/base.py


from pydantic import BaseModel, Extraclass BaseApi(BaseModel):    """ Базовый класс данных для api.    """    class Config:        extra = Extra.forbid

2.3.4. Объявление обработчика


Класс для middleware, над созданием которого мы сейчас работаем, позволит объявлять обработчики, например, так:


from typing import List, Unionfrom valdec.decorators import async_validate as validatefrom data_classes.person import PersonCreate, PersonInfo@validate("data", "return")async def create(    data: Union[PersonCreate, List[PersonCreate]], storage: dict,) -> Union[PersonInfo, List[PersonInfo]]:    # ...    return result

Что здесь добавилось (по сравнению с обработчиками из прошлых глав):


  • декоратор validate валидирует поступившие данные и ответ, и "подменяет" их на экземпляры валидирующих классов
  • в аннотациях у данных указаны уже конкретные классы.

Про оболочки запросов/ответов обработчик ничего не знает, ему это и не надо.


Позволю себе небольшую ремарку: если в аргументах у обработчика нет экземпляра web.Request(), то он является обычной функцией, которую можно использовать не только в wep.Aplication(). На самом деле, даже если он там есть, эту функцию по-прежнему можно будет использовать в приложениях другого типа, если обеспечить совместимый с web.Request() экземпляр данных.


Соответственно, классы данных для этого обработчика могут быть такими:


data_classes/person.py


from uuid import UUIDfrom pydantic import Field, StrictStrfrom data_classes.base import BaseApiclass PersonCreate(BaseApi):    """ Данные для создания персоны.    """    name: StrictStr = Field(description="Имя.", example="Oleg")class PersonInfo(BaseApi):    """ Информация о персоне.    """    id: UUID = Field(description="Идентификатор.")    name: StrictStr = Field(description="Имя.")

2.3.5. Классы данных для оболочек


В самом начале п.2.3. были обозначены тебования к оболочкам запроса и ответа.


Для их выполнения создадим классы данных.


data_classes/wraps.py


from typing import Any, Optionalfrom pydantic import Field, StrictIntfrom data_classes.base import BaseApi_ID_DESCRIPTION = "Идентификатор запроса к сервису."class WrapRequest(BaseApi):    """ Запрос.    """    data: Any = Field(description="Параметры запроса.", default=None)    id: Optional[StrictInt] = Field(description=_ID_DESCRIPTION)class WrapResponse(BaseApi):    """ Ответ.    """    success: bool = Field(description="Статус ответа.", default=True)    result: Any = Field(description="Результат ответа.")    id: Optional[StrictInt] = Field(description=_ID_DESCRIPTION)

Эти классы будут использоваться в классе для middleware при реализации логики оболочек.


2.3.6. Класс WrapsKwargsHandler для middleware


Класс WrapsKwargsHandler является наследником KwargsHandler и расширяет его возможности тем, что позволяет использовать оболочки для данных запросов и ответов и их валидацию (ссылка на код класса).


В этом классе переопределяются два метода run_handler и get_error_body.


2.3.6.1. Метод запуска обработчика

Переопределяется метод родительского класса:


async def run_handler(        self, request: web.Request, handler: Callable, request_body: Any    ) -> dict:        id_ = None        try:            # Проведем валидацию оболочки запроса            wrap_request = WrapRequest(**request_body)        except Exception as error:            message = f"{type(error).__name__} - {error}"            raise InputDataValidationError(message)        # Запомним поле id для ответов        id_ = wrap_request.id        request[KEY_NAME_FOR_ID] = id_        try:            result = await super().run_handler(                request, handler, wrap_request.data            )        except ValidationArgumentsError as error:            message = f"{type(error).__name__} - {error}"            raise InputDataValidationError(message)        # Проведем валидацию оболочки ответа        wrap_response = WrapResponse(success=True, result=result, id=id_)        return wrap_response.dict()

Сначала мы проверим оболочку запроса. Исключение InputDataValidationError поднимется в следующих случаях:


  • если в теле запроса не словарь (пусть даже пустой)
  • если есть поля с ключами отличными от data и id
  • если есть ключ id но его значение не StrictInt и не None

Если в запросе нет ключа id, то wrap_request.id получит значение None. Ключ data может иметь любое значение и валидироваться не будет. Так же, его может вообще не быть во входящих данных, тогда wrap_request.data получит значение None.


Затем мы запоминаем wrap_request.id в request. Это необходимо для формирования ответа с ошибкой на текущий запрос (если она произойдет).


После этого вызывается обработчик, но для его входящих данных передается только wrap_request.data (напомню, что во wrap_request.data сейчас объект python в том виде, как он был получен из json). При этом, исключение InputDataValidationError поднимается если получено исключение valdec.errors.ValidationArgumentsError.


Если обработчик отработал нормально, и был получен результат его работы, то создаем экземпляр класса оболочки ответа WrapResponse в варианте для успешного ответа.


Все просто, но хотел бы обратить внимание на такой момент. Можно было бы обойтись без создания wrap_response, а сразу сформировать словарь (как это и будет сделано для ответа с ошибкой). Но, в случае успешного ответа мы не знаем что пришло в ответе от обработчика, это может быть, например, как список словарей, так и список экземпляров BaseApi. А на выходе из метода мы должны гарантированно отдать объект, готовый для кодирования в json. Поэтому, мы "заворачиваем" любые данные с результом во WrapResponse.result и уже из wrap_response получаем окончательный ответ для метода при помощи wrap_response.dict() (ссылка на документацию).


2.3.6.2. Метод для получения данных ответа с ошибкой

Заменяется метод родительского класса:


def get_error_body(self, request: web.Request, error: Exception) -> dict:        """ Формирует и отдает словарь с телом ответа с ошибкой.        """        result = dict(error_type=str(type(error)), error_message=str(error))        # Так как мы знаем какая у нас оболочка ответа, сразу сделаем словарь        # с аналогичной "схемой"        response = dict(            # Для поля id используется сохраненное в request значение.            success=False, result=result, id=request.get(KEY_NAME_FOR_ID)        )        return response

Здесь можно было бы применить и наследование (вызвать super() для получения result), но для наглядности я оставил так. Вы можете сделать как сочтете нужным.


2.3.7. Примеры


Сигнатуры методов такие:


@validate("data", "return")async def create(    data: Union[PersonCreate, List[PersonCreate]], storage: dict,) -> Union[PersonInfo, List[PersonInfo]]:    # ...@validate("data", "return")async def read(storage: dict, req: web.Request, data: UUID) -> PersonInfo:    # ...@validate("info_id")async def info(info_id: int, request: web.Request) -> Any:    return f"info_id={info_id} and request={request}"

Первые два обслуживают POST запросы, последний GET (просто, для примера)


текст примеров...
2.3.7.1. Метод /create

  • Запрос 1:

{    "data": [        {            "name": "Ivan"        },        {            "name": "Oleg"        }    ],    "id": 1}

Ответ:


{    "success": true,    "result": [        {            "id": "af908a90-9157-4231-89f6-560eb6a8c4c0",            "name": "Ivan"        },        {            "id": "f7d554a0-1be9-4a65-bfc2-b89dbf70bb3c",            "name": "Oleg"        }    ],    "id": 1}

  • Запрос 2:

{    "data": {        "name": "Eliza"    },    "id": 2}

Ответ:


{    "success": true,    "result": {        "id": "f3a45a19-acd0-4939-8e0c-e10743ff8e55",        "name": "Eliza"    },    "id": 2}

  • Запрос 3:

Попробуем передать в data невалидное значение


{    "data": 123,    "id": 3}

Ответ:


{    "success": false,    "result": {        "error_type": "<class 'middlewares.exceptions.InputDataValidationError'>",        "error_message": "ValidationArgumentsError - Validation error <class 'valdec.errors.ValidationError'>: 2 validation errors for argument with the name of:\ndata\n  value is not a valid dict (type=type_error.dict)\ndata\n  value is not a valid list (type=type_error.list)."    },    "id": 3}

2.3.7.2. Метод /read

  • Запрос 1:

{    "data": "f3a45a19-acd0-4939-8e0c-e10743ff8e55",    "id": 4}

Ответ:


{    "success": true,    "result": {        "id": "f3a45a19-acd0-4939-8e0c-e10743ff8e55",        "name": "Eliza"    },    "id": 4

  • Запрос 2:

Попробуем сделать ошибку в оболочке.


{    "some_key": "f3a45a19-acd0-4939-8e0c-e10743ff8e55",    "id": 5}

Ответ:


{    "success": false,    "result": {        "error_type": "<class 'middlewares.exceptions.InputDataValidationError'>",        "error_message": "ValidationError - 1 validation error for WrapRequest\nsome_key\n  extra fields not permitted (type=value_error.extra)"    },    "id": null}

2.3.7.3. Метод /info/{info_id}

  • Запрос GET на /info/123:

{}

Ответ:


{    "success": true,    "result": "info_id=123 and request=<Request GET /info/123 >",    "id": null}

3. О нереализованной документации


У обработчиков, которые используются с классом WrapsKwargsHandler, есть всё, чтобы автоматически собрать документацию. К ним более не надо ничего добавлять. Так как классы pydantic.BaseModel позволяют получать json-schema, то остается только сделать скрипт сборки документации (если кратко, то надо: перед запуском приложения пройтись по всем обработчикам и у каждого заменить докстринг на swagger-описание, построенное на основе уже имеющегося докстринга и json-схем входящих данных и возврата).


И я эту документацию собираю. Но не стал рассказывать про это в статье. Причина в том, что я не нашел библиотеки для swagger и aiohttp, которая бы работала полностью как надо (или я не нашел способа заставить работать как надо).


Например, библиотека aiohttp-swagger некорректно отображает аргумент (в областях с примерами), если в аннотации есть алиас Union.


Библиотека aiohttp-swagger3, напротив, все прекрасно показывает, но не работает если в приложении есть sub_app.


Если кто-то знает как решить эти проблемы, или, возможно, кто-то знает библиотеку, которая работает стабильно буду очень благодарен за комментарий.


4. Заключение


В итоге у нас имеются три класса для json middleware с разными возможностями. Любой из них можно изменить под свои нужды. Или создать на их основе новый.


Можно создавать любые оболочки для содержимого запросов и ответов. Так же, можно гибко настраивать валидацию, и применять ее только там, где она действительно необходима.


Не сомневаюсь в том, что примеры которые я предложил, можно реализовать и по другому. Но надеюсь, что мои решения, если и не пригодятся полностью, то поспособствуют нахождению иных, более подходящих.


Спасибо за уделенное время. Буду рад замечаниям, и уточнениям.


При публикации статьи использовал MarkConv

Подробнее..

Разворачиваем сервер для проверки In-app purchase за 60 минут

12.11.2020 06:13:44 | Автор: admin

Всем привет! Сегодня расскажу вам как развернуть сервер для проверки In-app Purchase и In-app Subscription для iOS и Android (server-server validation).


На хабре есть статья от 2013 года про серверную проверку покупок. В статье говорится о том, что валидация в первую очередь необходима для предотвращения доступа к платному контенту при помощи джейлбрейка и другого софта. На мой взгляд в 2020 году эта проблема не так актуальна, и в первую очередь сервер с проверкой покупок необходима для синхронизации покупок в рамках одного аккаунта на нескольких устройствах


В проверке чеков покупок нет никакой технической сложности, по факту сервер просто проксирует запрос и сохраняет данные о покупке.




То есть задачу такого сервера можно разделить на 4 этапа:


  • Получение запроса с чеком, отправленным приложением после покупки
  • Запрос в Apple/Google на проверку чека
  • Сохранение данных о транзакции
  • Ответ приложению

В рамках статьи опустим 3 пункт, ибо он сугубо индивидуален.


Код в статье будет написан на Node.js, но по сути логика универсальна и не составит труда использовать ее написать валидацию на любом языке программирования.


Еще есть статья хорошая То, что нужно знать о проверке чека App Store (App Store receipt), ребята делают сервис для работы с подписками. В статье детально описано, что такое чек (receipt) и для чего нужна проверка покупок.


Сразу скажу, что в сниппетах кода используются вспомогательные классы и интерфейсы, весь код доступен в репозитории по ссылке https://github.com/denjoygroup/inapppurchase. В приведенном ниже фрагментах кода, я постарался дать названия используемым методам такие, чтобы приходилось делать отсылки к этим функциям.


iOS


Для проверки вам нужен Apple Shared Secret это ключ, который вы должны получить в iTunnes Connect, он нужен для проверки чеков.


В первую очередь зададим параметры для создания запросов:


 apple: any = {    password: process.env.APPLE_SHARED_SECRET, // ключ, укажите свой    host: 'buy.itunes.apple.com',    sandbox: 'sandbox.itunes.apple.com',    path: '/verifyReceipt',    apiHost: 'api.appstoreconnect.apple.com',    pathToCheckSales: '/v1/salesReports' }

Теперь создадим функцию для отправки запроса. В зависимости от среды, с которой работаете, вы должны отправлять запрос либо на sandbox.itunes.apple.com для тестовых покупок, либо в прод buy.itunes.apple.com


/*** receiptValue - чек, который проверяете* sandBox - среда разработк**/async _verifyReceipt(receiptValue: string, sandBox: boolean) {    let options = {        host: sandBox ? this._constants.apple.sandbox : this._constants.apple.host,        path: this._constants.apple.path,        method: 'POST'    };    let body = {        'receipt-data': receiptValue,        'password': this._constants.apple.password    };    let result = null;    let stringResult = await this._handlerService.sendHttp(options, body, 'https');    result = JSON.parse(stringResult);    return result;}

Если запрос прошел успешно, то в ответе от сервера Apple в поле status вы получите данные о вашей покупке.


У статуса возможны несколько значений, в зависимости от которых вы должны обработать покупку


21000 Запрос был отправлен не методом POST


21002 Чек поврежден, не удалось его распарсить


21003 Некорректный чек, покупка не подтверждена


21004 Ваш Shared Secret некорректный или не соответствует чеку


21005 Сервер эпла не смог обработать ваш запрос, стоит попробовать еще раз


21006 Чек недействителен


21007 Чек из SandBox (тестовой среды), но был отправлен в prod


21008 Чек из прода, но был отправлен в тестовую среду


21009 Сервер эпла не смог обработать ваш запрос, стоит попробовать еще раз


21010 Аккаунт был удален


0 Покупка валидна


Пример ответа от iTunnes Connect выглядит следующим образом


{    "environment":"Production",    "receipt":{        "receipt_type":"Production",        "adam_id":1527458047,        "app_item_id":1527458047,        "bundle_id":"BUNDLE_ID",        "application_version":"0",        "download_id":34089715299389,        "version_external_identifier":838212484,        "receipt_creation_date":"2020-11-03 20:47:54 Etc/GMT",        "receipt_creation_date_ms":"1604436474000",        "receipt_creation_date_pst":"2020-11-03 12:47:54 America/Los_Angeles",        "request_date":"2020-11-03 20:48:01 Etc/GMT",        "request_date_ms":"1604436481804",        "request_date_pst":"2020-11-03 12:48:01 America/Los_Angeles",        "original_purchase_date":"2020-10-26 19:24:19 Etc/GMT",        "original_purchase_date_ms":"1603740259000",        "original_purchase_date_pst":"2020-10-26 12:24:19 America/Los_Angeles",        "original_application_version":"0",        "in_app":[            {                "quantity":"1",                "product_id":"PRODUCT_ID",                "transaction_id":"140000855642848",                "original_transaction_id":"140000855642848",                "purchase_date":"2020-11-03 20:47:53 Etc/GMT",                "purchase_date_ms":"1604436473000",                "purchase_date_pst":"2020-11-03 12:47:53 America/Los_Angeles",                "original_purchase_date":"2020-11-03 20:47:54 Etc/GMT",                "original_purchase_date_ms":"1604436474000",                "original_purchase_date_pst":"2020-11-03 12:47:54 America/Los_Angeles",                "expires_date":"2020-12-03 20:47:53 Etc/GMT",                "expires_date_ms":"1607028473000",                "expires_date_pst":"2020-12-03 12:47:53 America/Los_Angeles",                "web_order_line_item_id":"140000337829668",                "is_trial_period":"false",                "is_in_intro_offer_period":"false"            }        ]    },    "latest_receipt_info":[        {            "quantity":"1",            "product_id":"PRODUCT_ID",            "transaction_id":"140000855642848",            "original_transaction_id":"140000855642848",            "purchase_date":"2020-11-03 20:47:53 Etc/GMT",            "purchase_date_ms":"1604436473000",            "purchase_date_pst":"2020-11-03 12:47:53 America/Los_Angeles",            "original_purchase_date":"2020-11-03 20:47:54 Etc/GMT",            "original_purchase_date_ms":"1604436474000",            "original_purchase_date_pst":"2020-11-03 12:47:54 America/Los_Angeles",            "expires_date":"2020-12-03 20:47:53 Etc/GMT",            "expires_date_ms":"1607028473000",            "expires_date_pst":"2020-12-03 12:47:53 America/Los_Angeles",            "web_order_line_item_id":"140000447829668",            "is_trial_period":"false",            "is_in_intro_offer_period":"false",            "subscription_group_identifier":"20675121"        }    ],    "latest_receipt":"RECEIPT",    "pending_renewal_info":[        {            "auto_renew_product_id":"PRODUCT_ID",            "original_transaction_id":"140000855642848",            "product_id":"PRODUCT_ID",            "auto_renew_status":"1"        }    ],    "status":0}

Также перед отправкой запроса и после отправки стоит сверить id продукта, который запрашивает клиент и который мы получаем в ответе.


Полезная для нас информация содержится в свойствах in_app и latest_receipt_info, и на первый взгляд содержимое этих свойств идентичны, но:


latest_receipt_info содержит все покупки.


in_app содержит Non-consumable и Non-Auto-Renewable покупки.


Будем использовать latest_receipt_info, соотвественно в этом массиве ищем нужный нам продукт по свойству product_id и проверяем дату, если это подписка. Конечно, стоит еще проверить не начислили ли мы уже эту покупку пользователю, особенно актуально для Consumable Purchase. Проверять можно по свойству original_transaction_id, заранее сохранив в базе, но в рамках этого гайдлайна мы этого делать не будем.


Тогда проверка покупки будет выглядеть примерно так


/*** product - id покупки* resultFromApple - ответ от Apple, полученный выше* productType - тип покупки (подписка, расходуемая или non-consumable)* sandBox - тестовая среда или нет***/async parseResponse(product: string, resultFromApple: any, productType: ProductType, sandBox: boolean) {    let parsedResult: IPurchaseParsedResultFromProvider = {        validated: false,        trial: false,        checked: false,        sandBox,        productType: productType,        lastResponseFromProvider: JSON.stringify(resultFromApple)    };    switch (resultFromApple.status) {        /**        * Валидная подписка        */        case 0: {            /**            * Ищем в ответе информацию о транзакции по запрашиваемому продукту            **/            let currentPurchaseFromApple = this.getCurrentPurchaseFromAppleResult(resultFromApple, product!, productType);            if (!currentPurchaseFromApple) break;            parsedResult.checked = true;            parsedResult.originalTransactionId = this.getTransactionIdFromAppleResponse(currentPurchaseFromApple);            if (productType === ProductType.Subscription) {                parsedResult.validated = (this.checkDateIsAfter(currentPurchaseFromApple.expires_date_ms)) ? true : false;                parsedResult.expiredAt = (this.checkDateIsValid(currentPurchaseFromApple.expires_date_ms)) ?                this.formatDate(currentPurchaseFromApple.expires_date_ms) : undefined;            } else {                parsedResult.validated = true;            }            parsedResult.trial = !!currentPurchaseFromApple.is_trial_period;            break;        }        default:            if (!resultFromApple) console.log('empty result from apple');            else console.log('incorrect result from apple, status:', resultFromApple.status);    }    return parsedResult;}

После этого можно возвращать ответ на клиент по нашей покупке, которая хранится в переменной parsedResult. Формировать структуру этого объекта вы можете по своему усмотрению, зависит от ваших потребностей, но самое главное, что на этом шаге мы уже знаем валидна покупка или нет, и информацию об этом хранится в parsedResult.validated.


Если интересно, то могу написать отдельную статью о том, как обрабатывать ответ от iTunnes Connect по каждому свойству, ибо вот это далеко нетривиальная задача. Так же возможно будет полезно рассказать о том, как работать с проверкой автовозобновляемых покупок, когда их проверять и как, потому что по времени истечения подписки запускать крон недостаточно однозначно возникнут проблемы и пользователь останется без оплаченных покупок, а в этом случае сразу будут отзывы с одной звездой в мобильном сторе.


Android


Для гугла достаточно сильно отличается формат запроса, ибо сначала надо авторизоваться посредством OAuth и потом только отправлять запрос на проверку покупки.


Для гугла нам понадобится чуть больше входных параметров:


google: any = {    host: 'androidpublisher.googleapis.com',    path: '/androidpublisher/v3/applications',    email: process.env.GOOGLE_EMAIL,    key: process.env.GOOGLE_KEY,    storeName: process.env.GOOGLE_STORE_NAME}

Получить эти данные можно воспользовавшись инструкцией по ссылке.


Окей, гугл, прими запрос:


/*** product - название продукта* token - чек* productType  тип покупки, подписка или нет**/async getPurchaseInfoFromGoogle(product: string, token: string, productType: ProductType) {    try {        let options = {            email: this._constants.google.email,            key: this._constants.google.key,            scopes: ['https://www.googleapis.com/auth/androidpublisher'],        };        const client = new JWT(options);        let productOrSubscriptionUrlPart = productType === ProductType.Subscription ? 'subscriptions' : 'products';        const url = `https://${this._constants.google.host}${this._constants.google.path}/${this._constants.google.storeName}/purchases/${productOrSubscriptionUrlPart}/${product}/tokens/${token}`;        const res = await client.request({ url });        return res.data as ResultFromGoogle;    } catch(e) {        return e as ErrorFromGoogle;    }}

Для авторизации воспользуемся библиотекой google-auth-library и класс JWT.


Ответ от гугла выглядит примерно так:


{    startTimeMillis: "1603956759767",    expiryTimeMillis: "1603966728908",    autoRenewing: false,    priceCurrencyCode: "RUB",    priceAmountMicros: "499000000",    countryCode: "RU",    developerPayload: {        "developerPayload":"",        "is_free_trial":false,        "has_introductory_price_trial":false,        "is_updated":false,        "accountId":""    },    cancelReason: 1,    orderId: "GPA.3335-9310-7555-53285..5",    purchaseType: 0,    acknowledgementState: 1,    kind: "androidpublisher#subscriptionPurchase"}

Теперь перейдем к проверке покупки



parseResponse(product: string, result: ResultFromGoogle | ErrorFromGoogle, type: ProductType) {    let parsedResult: IPurchaseParsedResultFromProvider = {        validated: false,        trial: false,        checked: true,        sandBox: false,        productType: type,        lastResponseFromProvider: JSON.stringify(result),    };    if (this.isResultFromGoogle(result)) {        if (this.isSubscriptionResult(result)) {            parsedResult.expiredAt = moment(result.expiryTimeMillis, 'x').toDate();            parsedResult.validated = this.checkDateIsAfter(parsedResult.expiredAt);        } else if (this.isProductResult(result)) {            parsedResult.validated = true;        }    }    return parsedResult;}

Тут все достаточно тривиально. На выходе мы также получаем parsedResult, где самое важное хранится в свойстве validated прошла покупка проверку или нет.


Итог


По существу буквально в 2 метода можно проверить покупку. Репозиторий с полным кодом доступен по ссылке https://github.com/denjoygroup/inapppurchase (автор кода Алексей Геворкян)


Конечно, мы упустили очень много нюансов обработки покупки, которые стоит учитывать при работе с реальными покупками.


Есть два хороших сервиса, которые предоставляют сервис для проверки чеков: https://ru.adapty.io/ и https://apphud.com/. Но, во-первых, для некоторых категорий приложений нельзя передавать данные 3 стороне, а во-вторых, если вы хотите отдавать платный контент динамически при совершении пользователем покупки, то вам придется разворачивать свой сервер.


P.S.


Ну, и, конечно, самое важное в серверной разработке это масштабируемость и устойчивость. Если у вас большая аудитория пользователей и при этом сервер не способен выдерживать нагрузки, то лучше и не реализовывать проверку покупок самим, а отправлять запросы сразу в iTunnes Connect и в Google API, иначе ваши пользователи сильно расстроятся.

Подробнее..

Валидация в PHP. Красота или лапша?

30.09.2020 02:10:03 | Автор: admin
Выбирая лучший PHP-валидатор из десятка популярных, я столкнулся с дилеммой. Что для меня важнее? Следование всем SOLID / ООП-канонам или удобство работы и наглядность кода? Что предпочтут пользователи фреймворка Comet? Если вы считаете, что вопрос далеко не прост добро пожаловать под кат в длинное путешествие по фрагментам кода :)


Помимо озабоченности вопросами быстродействия для REST API и микросервисов, я очень переживаю за читаемость кода, который мы ежедневно набиваем для решения рабочих задач, в том числе валидации данных.

Хочу показать куски кода из собственных бенчмарков, чтобы вы смогли оценить широту подходов к решению одной и той же проблемы. Представим, что к нам прилетел следующий набор данных:

$form = [    'name'           => 'Elon Mask',     'name_wrong'     => 'Mask',    'login'          => 'mask',     'login_wrong'    => 'm@sk',     'email'          => 'elon@tesla.com',     'email_wrong'    => 'elon@tesla_com',     'password'       => '1q!~|w2o<z',     'password_wrong' => '123456',    'date'           => '2020-06-05 15:52:00',    'date_wrong'     => '2020:06:05 15-52-00',    'ipv4'           => '192.168.1.1',    'ipv4_wrong'     => '402.28.6.12',    'uuid'           => '70fcf623-6c4e-453b-826d-072c4862d133',    'uuid_wrong'     => 'abcd-xyz-6c4e-453b-826d-072c4862d133',    'extra'          => 'that field out of scope of validation',    'empty'          => ''];

Наша цель прогнать этот массив через набор правил валидации, получив на выходе список всех полей с ошибками и стандартные сообщения для демонстрации пользователю.

Отраслевой стандарт и икона чистого ООП конечно же Symfony



use Symfony\Component\Validator\Constraints\Length;use Symfony\Component\Validator\Constraints\NotBlank;use Symfony\Component\Validator\Validation;use Symfony\Component\Validator\Constraints as Assert;use Symfony\Component\Translation\MessageSelector;$validator = Validation::createValidator();$constraint = new Assert\Collection([        'name' => new Assert\Regex('/^[A-Za-z]+\s[A-Za-z]+$/u'),       'login' => new Assert\Regex('/^[a-zA-Z0-9]-_+$/'),    'email' => new Assert\Email(),    'password' => [        new Assert\NotBlank(),        new Assert\Length(['max' => 64]),        new Assert\Type(['type' => 'string'])    ],    'agreed' => new Assert\Type(['type' => 'boolean'])]);$violations = $validator->validate($form, $constraint);$errors = [];if (0 !== count($violations)) {    foreach ($violations as $violation) {        $errors[] = $violation->getPropertyPath() . ' : ' . $violation->getMessage();    }} return $errors;

Вырвиглазный код на чистом PHP


$errors = [];if (!preg_match('/^[A-Za-z]+\s[A-Za-z]+$/u', $form['name']))    $errors['name'] = 'should consist of two words!';if (!preg_match('/^[A-Za-z]+\s[A-Za-z]+$/u', $form['name_wrong']))    $errors['name_wrong'] = 'should consist of two words!';if (!preg_match('/^[a-zA-Z0-9-_]+$/', $form['login']))    $errors['login'] = 'should contain only alphanumeric!';if (!preg_match('/^[a-zA-Z0-9]-_+$/', $form['login_wrong']))    $errors['login_wrong'] = 'should contain only alphanumeric!';if (filter_var($form['email'], FILTER_VALIDATE_EMAIL) != $form['email'])    $errors['email'] = 'provide correct email!';if (filter_var($form['email_wrong'], FILTER_VALIDATE_EMAIL) != $form['email_wrong'])    $errors['email_wrong'] = 'provide correct email!';if (!is_string($form['password']) ||    $form['password'] == '' ||    strlen($form['password']) < 8 ||    strlen($form['password']) > 64 )    $errors['password'] = 'provide correct password!';if (!is_string($form['password_wrong']) ||    $form['password_wrong'] == '' ||    strlen($form['password_wrong']) < 8 ||    strlen($form['password_wrong']) > 64 )    $errors['password_wrong'] = 'provide correct password!';if (!preg_match('/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/', $form['date']))    $errors['date'] = 'provide correct date!';if (!preg_match('/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/', $form['date_wrong']))    $errors['date_wrong'] = 'provide correct date!';if (filter_var($form['ipv4'], FILTER_VALIDATE_IP) != $form['ipv4'])    $errors['ipv4'] = 'provide correct ip4!';if (filter_var($form['ipv4_wrong'], FILTER_VALIDATE_IP) != $form['ipv4_wrong'])    $errors['ipv4_wrong'] = 'provide correct ip4!';if (!preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $form['uuid']))    $errors['uuid'] = 'provide correct uuid!';if (!preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $form['uuid_wrong']))    $errors['uuid_wrong'] = 'provide correct uuid!';if (!isset($form['agreed']) || !is_bool($form['agreed']) || $form['agreed'] != true)    $errors['agreed'] = 'you should agree with terms!';return $errors;

Решение на базе одной из самых популярных библитек Respect Validation


use Respect\Validation\Validator as v;use Respect\Validation\Factory;Factory::setDefaultInstance(    (new Factory())        ->withRuleNamespace('Validation')        ->withExceptionNamespace('Validation'));$messages = [];try {    v::attribute('name', v::RespectRule())        ->attribute('name_wrong', v::RespectRule())        ->attribute('login', v::alnum('-_'))        ->attribute('login_wrong', v::alnum('-_'))        ->attribute('email', v::email())        ->attribute('email_wrong', v::email())        ->attribute('password', v::notEmpty()->stringType()->length(null, 64))        ->attribute('password_wrong', v::notEmpty()->stringType()->length(null, 64))        ->attribute('date', v::date())        ->attribute('date_wrong', v::date())        ->attribute('ipv4', v::ipv4())        ->attribute('ipv4_wrong', v::ipv4())        ->attribute('uuid', v::uuid())        ->attribute('uuid_wrong', v::uuid())        ->attribute('agreed', v::trueVal())        ->assert((object) $form);} catch (\Exception $ex) {    $messages = $ex->getMessages();}return $messages;

Еще одно известное имя: Valitron


use Valitron\Validator;Validator::addRule('uuid', function($field, $value) {    return (bool) preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $value);}, 'UUID should confirm RFC style!');$rules = [    'required'  => [ 'login', 'agreed' ],    'regex'     => [ ['name', '/^[A-Za-z]+\s[A-Za-z]+$/'] ],    'lengthMin' => [ [ 'password', '8'], [ 'password_wrong', '8'] ],    'lengthMax' => [ [ 'password', '64'], [ 'password_wrong', '64'] ],    'slug'      => [ 'login', 'login_wrong' ],    'email'     => [ 'email', 'email_wrong' ],    'date'      => [ 'date', 'date_wrong' ],    'ipv4'      => [ 'ipv4', 'ipv4_wrong' ],    'uuid'      => [ 'uuid', 'uuid_wrong' ],    'accepted'  => 'agreed'];$validator = new Validator($form);$validator->rules($rules);$validator->rule('accepted', 'agreed')->message('You should set {field} value!');$validator->validate();return $validator->errors());

Прекрасный Sirius


$validator = new \Sirius\Validation\Validator;$validator    ->add('name', 'required | \Validation\SiriusRule')    ->add('login', 'required | alphanumhyphen', null, 'Only latin chars, underscores and dashes please.')    ->add('email', 'required | email', null, 'Give correct email please.')    ->add('password', 'required | maxlength(64)', null, 'Wrong password.')    ->add('agreed', 'required | equal(true)', null, 'Where is your agreement?');$validator->validate($form);$errors = [];foreach ($validator->getMessages() as $attribute => $messages) {    foreach ($messages as $message) {        $errors[] = $attribute . ' : '. $message->getTemplate();    }}return $errors;

А вот так валидируют в Laravel


use Illuminate\Validation\Factory as ValidatorFactory;use Illuminate\Translation\Translator;use Illuminate\Translation\ArrayLoader;use Symfony\Component\Translation\MessageSelector;use Illuminate\Support\Facades\Validator as FacadeValidator;$rules = array(    'name' => ['regex:/^[A-Za-z]+\s[A-Za-z]+$/u'],    'name_wrong' => ['regex:/^[A-Za-z]+\s[A-Za-z]+$/u'],    'login' => ['required', 'alpha_num'],    'login_wrong' => ['required', 'alpha_num'],    'email' => ['email'],    'email_wrong' => ['email'],    'password' => ['required', 'min:8', 'max:64'],    'password_wrong' => ['required', 'min:8', 'max:64'],    'date' => ['date'],    'date_wrong' => ['date'],    'ipv4' => ['ipv4'],    'ipv4_wrong' => ['ipv4'],    'uuid' => ['uuid'],    'uuid_wrong' => ['uuid'],    'agreed' => ['required', 'boolean']);$messages = [    'name_wrong.regex' => 'Username is required.',    'password_wrong.required' => 'Password is required.',    'password_wrong.max' => 'Password must be no more than :max characters.',    'email_wrong.email' => 'Email is required.',    'login_wrong.required' => 'Login is required.',    'login_wrong.alpha_num' => 'Login must consist of alfa numeric chars.',    'agreed.required' => 'Confirm radio box required.',);$loader = new ArrayLoader();$translator = new Translator($loader, 'en');$validatorFactory = new ValidatorFactory($translator);$validator = $validatorFactory->make($form, $rules, $messages);return $validator->messages();

Неожиданный бриллиант Rakit Validation


$validator = new \Rakit\Validation\Validator;$validator->addValidator('uuid', new \Validation\RakitRule);$validation = $validator->make($form, [    'name'           => 'regex:/^[A-Za-z]+\s[A-Za-z]+$/u',    'name_wrong'     => 'regex:/^[A-Za-z]+\s[A-Za-z]+$/u',    'email'          => 'email',    'email_wrong'    => 'email',    'password'       => 'required|min:8|max:64',    'password_wrong' => 'required|min:8|max:64',    'login'          => 'alpha_dash',    'login_wrong'    => 'alpha_dash',    'date'           => 'date:Y-m-d H:i:s',    'date_wrong'     => 'date:Y-m-d H:i:s',    'ipv4'           => 'ipv4',    'ipv4_wrong'     => 'ipv4',    'uuid'           => 'uuid',    'uuid_wrong'     => 'uuid',    'agreed'         => 'required|accepted']); $validation->setMessages([    'uuid'     => 'UUID should confirm RFC rules!',    'required' => ':attribute is required!',    // etc]);$validation->validate();return $validation->errors()->toArray();

Ну так что? Какой из примеров кода наиболее наглядный, идиоматичный, корректный и вообще правильный? Мой личный выбор в доках на Comet: github.com/gotzmann/comet

В заключение небольшой опрос для потомков.
Подробнее..

Валидация данных в Spring Boot

10.01.2021 12:14:24 | Автор: admin

Не редко пользователи пытаются передать в приложение некорректные данные. Это происходит либо из злого умысла, либо по ошибке. Поэтому стоит проверять данные на соответствие бизнес-требованиям.


Эту задачу решает Bean Validation. Он интегрирован со Spring и Spring Boot. Hibernate Validator считается эталонной реализацией Bean Validation.


Основы валидации Bean


Для проверки данных используются аннотации над полями класса. Это декларативный подход, который не загрязняет код.


При передаче размеченного таким образом объекта класса в валидатор, происходит проверка на ограничения.


Настройка


Добавьте следующие зависимости в проект:


<dependency>    <groupId>javax.validation</groupId>    <artifactId>validation-api</artifactId>    <version>2.0.1.Final</version></dependency><dependency>    <groupId>org.hibernate</groupId>    <artifactId>hibernate-validator</artifactId>    <version>7.0.0.Final</version></dependency>

dependencies {    compile group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'    compile group: 'org.hibernate', name: 'hibernate-validator', version: '7.0.0.Final'}

Валидация в Spring MVC Controller


Сначала данные попадают в контроллер. У входящего HTTP-запроса возможно проверить следующие параметры:


  • тело запроса
  • переменные пути (например, id в /foos/{id})
  • параметры запроса

Рассмотрим каждый из них подробнее.


Валидация тела запроса


Тело запроса POST и PUT обычно содержит данные в формате JSON. Spring автоматически сопоставляет входящий JSON с объектом Java.


Проверяем соответствует ли входящий Java объект нашим требованиям.


class Input {     @Min(1)     @Max(10)     private int numberBetweenOneAndTen;     @Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$")     private String ipAddress;     // ...}

  • Поле numberBetweenOneAndTen должно быть от 1 до 10, включительно.
  • Поле ipAddress должно содержать строку в формате IP-адреса.

Контроллер REST принимает объект Input и выполняет проверку:


@RestControllerclass ValidateRequestBodyController {  @PostMapping("/validateBody")  public ResponseEntity<String> validateBody(@Valid @RequestBody Input input) {    return ResponseEntity.ok("valid");  }}

Достаточно добавить в параметр input аннотацию @Valid, чтобы сообщить спрингу передать объект Валидатору, прежде чем делать с ним что-либо еще.


Если класс содержит поле с другим классом, который тоже необходимо проверить это поле необходимо пометить аннотацией Valid.


Исключение MethodArgumentNotValidException выбрасывается, когда объект не проходит проверку. По умолчанию, Spring переведет это исключение в HTTP статус 400.


Проверка переменных пути и параметров запроса


Проверка переменных пути и параметров запроса работает по-другому.


Не проверяются сложные Java-объекты, так как path-переменные и параметры запроса являются примитивными типами, такими как int, или их аналогами: Integer или String.


Вместо аннотации поля класса, как описано выше, добавляют аннотацию ограничения (в данном случае @Min) непосредственно к параметру метода в контроллере Spring:


@Validated@RestControllerclass ValidateParametersController {  @GetMapping("/validatePathVariable/{id}")  ResponseEntity<String> validatePathVariable(      @PathVariable("id") @Min(5) int id  ) {    return ResponseEntity.ok("valid");  }  @GetMapping("/validateRequestParameter")  ResponseEntity<String> validateRequestParameter(      @RequestParam("param") @Min(5) int param  ) {     return ResponseEntity.ok("valid");  }}

Обратите внимание, что необходимо добавить @Validated Spring в контроллер на уровне класса, чтобы сказать Spring проверять ограничения на параметрах метода.


В этом случае аннотация @Validated устанавливается на уровне класса, даже если она присутствует на методах.


В отличии валидации тела запроса, при неудачной проверки параметра вместо метода MethodArgumentNotValidException будет выброшен ConstraintViolationException. По умолчанию последует ответ со статусом HTTP 500 (Internal Server Error), так как Spring не регистрирует обработчик для этого исключения.


Вернем HTTP статус 400, так как клиент предоставил недействительный параметр. Для этого добавляем пользовательский обработчик исключений в контоллер:


@RestController@Validatedclass ValidateParametersController {  // request mapping method omitted  @ExceptionHandler(ConstraintViolationException.class)  @ResponseStatus(HttpStatus.BAD_REQUEST)  public ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {    return new ResponseEntity<>("not valid due to validation error: " + e.getMessage(), HttpStatus.BAD_REQUEST);  }}

Позже рассмотрим, как вернуть структурированный ответ об ошибке, содержащий подробности обо всех неудачных подтверждениях для проверки клиентом.


Валидация в сервисном слое


Можно проверять данные на любых компонентах Spring. Для этого используется комбинация аннотаций @Validated и @Valid.


@Service@Validatedclass ValidatingService{    void validateInput(@Valid Input input){      // do something    }}

Аннотация @Validated устанавливается только на уровне класса, так что не ставьте ее на метод в данном случае.


Валидация сущностей JPA


Persistence Layer это последняя линия проверки данных. По умолчанию Spring Data использует Hibernate, который поддерживает Bean Validation из коробки.


Обычно мы не хотим делать проверку так поздно, поскольку это означает, что бизнес-код работал с потенциально невалидными объектами, что может привести к непредвиденным ошибкам.


Допустим, необходимо хранить объекты нашего класса Input в базе данных. Сначала добавляем нужную JPA аннотацию @Entity, а так же поле id:


@Entitypublic class Input {  @Id  @GeneratedValue  private Long id;  @Min(1)  @Max(10)  private int numberBetweenOneAndTen;  @Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$")  private String ipAddress;  // ...}

Когда репозиторий пытается сохранить невалидный Input, чьи аннотации ограничений нарушаются, выбрасывается ConstraintViolationException.


Bean Validation запускается Hibernate только после того как EntityManager вызовет flush.


Чтобы отключить Bean Validation в репозиториях Spring, достаточно установить свойство Spring Boot spring.jpa.properties.javax.persistence.validation.mode равным null.


Валидация конфигурации приложения


Spring Boot аннотация @ConfigurationProperties используется для связывания свойств из application.properties с Java объектом.


Данные из application необходимы для стабильной работы приложения. Bean Validation поможет обнаружить ошибку в этих данных при старте приложения.


Допустим имеется следующий конфигурационный класс:


@Validated@ConfigurationProperties(prefix="app.properties")class AppProperties {  @NotEmpty  private String name;  @Min(value = 7)  @Max(value = 30)  private Integer reportIntervalInDays;  @Email  private String reportEmailAddress;  // getters and setters}

При попытке запуска с недействительным адресом электронной почты получаем ошибку:


***************************APPLICATION FAILED TO START***************************Description:Binding to target org.springframework.boot.context.properties.bind.BindException:  Failed to bind properties under 'app.properties' to  io.reflectoring.validation.AppProperties failed:    Property: app.properties.reportEmailAddress    Value: manager.analysisapp.com    Reason: must be a well-formed email addressAction:Update your application's configuration

Стандартные ограничения


Библиотека javax.validation имеет множество аннотаций для валидации.


Каждая аннотация имеет следующие поля:


  • message указывает на ключ свойства в ValidationMessages.properties, который используется для отправки сообщения в случае нарушения ограничения.
  • groups позволяет определить, при каких обстоятельствах будет срабатывать эта проверка (о группах проверки поговорим позже).
  • payload позволяет определить полезную нагрузку, которая будет передаваться сс проверкой.
  • @Constraint указывает на реализацию интерфейса ConstraintValidator.

Рассмотрим популярные ограничения.


@NotNull и @Null


@NotNull аннотированный элемент не должен быть null. Принимает любой тип.
@Null аннотированный элемент должен быть null. Принимает любой тип.


@NotBlank и @NotEmpty


@NotBlank аннотированный элемент не должен быть null и должен содержать хотя бы один непробельный символ. Принимает CharSequence.
@NotEmpty аннотированный элемент **не** должен быть null или пустым. Поддерживаемые типы:


  • CharSequence
  • Collection. Оценивается размер коллекции
  • Map. Оценивается размер мапы
  • Array. Оценивается длина массива

@NotBlank применяется только к строкам и проверяет, что строка не пуста и не состоит только из пробелов.


@NotNull применяется к CharSequence, Collection, Map или Array и проверяет, что объект не равен null. Но при этом он может быть пуст.


@NotEmpty применяется к CharSequence, Collection, Map или Array и проверяет, что он не null имеет размер больше 0.


Аннотация @Size(min=6) пропустит строку состоящую из 6 пробелов и/или символов переноса строки, а @NotBlank не пропустит.


@Size


Размер аннотированного элемента должен быть между указанными границами, включая сами границы. null элементы считаются валидными.


Поддерживаемые типы:


  • CharSequence. Оценивается длина последовательности символов
  • Collection. Оценивается размер коллекции
  • Map. Оценивается размер мапы
  • Array. Оценивается длина массива

Добавление пользовательского валидатора


Если имеющихся аннотаций ограничений недостаточно, то создайте новые.


В классе Input использовалось регулярное выражение для проверки того, что строка является IP адресом. Регулярное выражение не является полным: оно позволяет сокеты со значениями больше 255, таким образом "111.111.111.333" будет считаться действительным.


Давайте напишем валидатор, который реализует эту проверку на Java. Потому что как говорится, до решения проблемы регулярным выражением у вас была одна проблема, а теперь стало двe :smile:


Сначала создаем пользовательскую аннотацию @IpAddress:


@Target({ FIELD })@Retention(RUNTIME)@Constraint(validatedBy = IpAddressValidator.class)@Documentedpublic @interface IpAddress {  String message() default "{IpAddress.invalid}";  Class<?>[] groups() default { };  Class<? extends Payload>[] payload() default { };}

Реализация валидатора выглядит следующим образом:


class IpAddressValidator implements ConstraintValidator<IpAddress, String> {  @Override  public boolean isValid(String value, ConstraintValidatorContext context) {    Pattern pattern =       Pattern.compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$");    Matcher matcher = pattern.matcher(value);    try {      if (!matcher.matches()) {        return false;      } else {        for (int i = 1; i <= 4; i++) {          int octet = Integer.valueOf(matcher.group(i));          if (octet > 255) {            return false;          }        }        return true;      }    } catch (Exception e) {      return false;    }  }}

Теперь можно использовать аннотацию @IpAddress, как и любую другую аннотацию ограничения.


class InputWithCustomValidator {  @IpAddress  private String ipAddress;  // ...}

Принудительный вызов валидации


Для принудительного вызова проверки, без использования Spring Boot, создайте валидатор вручную.


class ProgrammaticallyValidatingService {  void validateInput(Input input) {    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();    Validator validator = factory.getValidator();    Set<ConstraintViolation<Input>> violations = validator.validate(input);    if (!violations.isEmpty()) {      throw new ConstraintViolationException(violations);    }  }}

Тем не менее, Spring Boot предоставляет предварительно сконфигурированный экземпляр валидатора. Внедрив этот экземпляр в сервис не придется создавать его вручную.


@Serviceclass ProgrammaticallyValidatingService {  private Validator validator;  public ProgrammaticallyValidatingService(Validator validator) {    this.validator = validator;  }  public void validateInputWithInjectedValidator(Input input) {    Set<ConstraintViolation<Input>> violations = validator.validate(input);    if (!violations.isEmpty()) {      throw new ConstraintViolationException(violations);    }  }}

Когда этот сервис внедряется Spring, в конструктор автоматически вставляется экземпляр валидатора.


Группы валидаций {#validation-groups}


Некоторые объекты участвуют в разных вариантах использования.


Возьмем типичные операции CRUD: при обновлении и создании, скорее всего, будет использоваться один и тот же класс. Тем не менее, некоторые валидации должны срабатывать при различных обстоятельствах:


  • только перед созданием
  • только перед обновлением
  • или в обоих случаях

Функция Bean Validation, которая позволяет нам внедрять такие правила проверки, называется "Validation Groups".


Все аннотации ограничений должны иметь поле groups. Это поле быть использовано для передачи любых классов, каждый из которых определяет группу проверки.


Для нашего примера CRUD определим два маркерных интерфейса OnCreate и OnUpdate:


interface OnCreate {}interface OnUpdate {}

Затем используем эти интерфейсы с любой аннотацией ограничения:


class InputWithGroups {  @Null(groups = OnCreate.class)  @NotNull(groups = OnUpdate.class)  private Long id;  // ...}

Это позволит убедиться, что id пуст при создании и заполнен при обновлении.


{{< admonition type=warning title="" open=true >}}
Spring поддерживает группы проверки только с аннотацией @Validated
{{< /admonition >}}


@Service@Validatedclass ValidatingServiceWithGroups {    @Validated(OnCreate.class)    void validateForCreate(@Valid InputWithGroups input){      // do something    }    @Validated(OnUpdate.class)    void validateForUpdate(@Valid InputWithGroups input){      // do something    }}

Обратите внимание, что аннотация @Validated применяется ко всему классу. Чтобы определить, какая группа проверки активна, она также применяется на уровне метода.


Использование групп проверки может легко стать анти-паттерном. При использовании групп валидации сущность должна знать правила валидации для всех случаев использования (групп), в которых она используется.


Возвращение структурных ответов на ошибки


Когда проверка не удается, лучше вернуть клиенту понятное сообщение об ошибке. Для этого необходимо вернуть структуру данных с сообщением об ошибке для каждой проверки, которая не прошла валидацию.


Сначала нужно определить эту структуру данных. Назовем ее ValidationErrorResponse и она содержит список объектов Violation:


public class ValidationErrorResponse {  private List<Violation> violations = new ArrayList<>();  // ...}public class Violation {  private final String fieldName;  private final String message;  // ...}

Затем создадим глобальный ControllerAdvice, который обрабатывает все ConstraintViolationExventions, которые пробрасываются до уровня контроллера. Чтобы отлавливать ошибки валидации и для тел запросов, мы также будем работать с MethodArgumentNotValidExceptions:


@ControllerAdviceclass ErrorHandlingControllerAdvice {  @ExceptionHandler(ConstraintViolationException.class)  @ResponseStatus(HttpStatus.BAD_REQUEST)  @ResponseBody  ValidationErrorResponse onConstraintValidationException(      ConstraintViolationException e) {    ValidationErrorResponse error = new ValidationErrorResponse();    for (ConstraintViolation violation : e.getConstraintViolations()) {      error.getViolations().add(        new Violation(violation.getPropertyPath().toString(), violation.getMessage()));    }    return error;  }  @ExceptionHandler(MethodArgumentNotValidException.class)  @ResponseStatus(HttpStatus.BAD_REQUEST)  @ResponseBody  ValidationErrorResponse onMethodArgumentNotValidException(      MethodArgumentNotValidException e) {    ValidationErrorResponse error = new ValidationErrorResponse();    for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {      error.getViolations().add(        new Violation(fieldError.getField(), fieldError.getDefaultMessage()));    }    return error;  }}

Здесь информацию о нарушениях из исключений переводится в нашу структуру данных ValidationErrorResponse.


Обратите внимание на аннотацию @ControllerAdvice, которая делает методы обработки исключений глобально доступными для всех контроллеров в контексте приложения.

Подробнее..
Категории: Java , Spring , Validation , Spring boot , Spring framework

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru