Так или иначе, все сервисы сталкиваются с задачами валидации. Часто они сводятся к простым и однотипным проверкам: заполнены ли все обязательные поля, верен ли формат телефонного номера, кредитной карты и пр. Но существуют проекты, в которых условия и правила проверок более разнообразные, да и те временами требуют серьёзного пересмотра. Внесение же изменений или создание дополнительных правил валидации требует непростых согласований и привлечения внимания нескольких команд разработчиков, обновления документации.
Недавно мне довелось поучаствовать в проекте, особую роль в котором занимают функции форматно-логического контроля входящих документов. Как следствие, у меня появились некоторые варианты решения подобных задач. Одним из них я и хочу поделиться.
Давайте разбираться на абстрактном примере. Предположим, что от поставщика данных мы получаем xml-файл с перечнем водительских удостоверений. Но прежде чем появится возможность их использования, нам предстоит:
- проверить валидность полученного xml;
- провести форматный и логический контроль каждого водительского удостоверения.
В качестве тестовых данных примем source/main.xml. И ещё, дабы не утомлять читателя длинными листингами, исходники и всё необходимое для их исполнения размещено в гит-репозитории.
XML Schema
Первая мысль, которая приходит в голову — использование XML Schema. И действительно, наличие XSD-схемы как минимум позволит нам быть уверенными в валидности XML. К тому же схема поможет провести некоторый форматный контроль.
<!-- schema/main.xsd -->
<!-- пример проверки формата серии удостоверения -->
...
<xs:element name="Series">
<xs:annotation>
<xs:documentation>Серия документа. Две цифры и две русские заглавные буквы для водительского удостоверения, полученного до 1 марта 2011 г., или четыре цифры для водительского удостоверения, полученного после 1 марта 2011. Пример: 44АА или 4403</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:length value="4"/>
<xs:pattern value="([0-9]){2}([А-Я, 0-9]){2}"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
...
Проверить соответствие документа схеме довольно просто:
~/x_validation$ xmllint --schema schema/main.xsd source/main.xml --noout
Но, к сожалению, XML Schema не сможет выполнить все необходимые нам проверки. Внимательный читатель заметил, что формат серии водительского удостоверения зависит от даты выдачи последнего. Такое с помощью XSD мы не проверим. Возникнут проблемы и при проверке, например, возраста водителя, наличия и стажа управления ТС определённых категорий и т. д.
Этапы валидации
Для реализации форматно-логического контроля потребуется ещё один этап проверок. На первом этапе мы убедимся, что xml-файл соответствует xsd-схеме. И если тот окажется валидным, следующим этапом проверим, удовлетворяет ли содержание xml бизнес-правилам.
Для реализации второго этапа отлично подойдёт Schematron. В отличие от XML Schema, Schematron позволяет выполнять проверки на основе содержимого документа. Это даёт возможность накладывать ограничения на один элемент, исходя из содержания другого.
Этапы валидации
Небольшое замечание. В нашем примере мы смотрим на проблему валидации только со стороны получателя данных. Но стоит понимать, что те же самые XSD и Schematron-схемы могут быть использованы и поставщиком данных. Это позволит поставщику убедиться в валидности своих данных локально, то есть до фактического обращения к сервису получателя.
Schematron
По заявлениям ресурса schematron.com, этот инструмент является перьевой метёлкой, позволяющей добраться до углов, недоступных другим языкам схем. На мой взгляд, это крайне точное определение происходящего.
Cхема Schematron выглядит примерно так:
<?xml version="1.0" encoding="UTF-8"?>
<sch:schema queryBinding="xslt2" xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern><!-- набор связанных правил -->
<rule context=""><!-- набор тестов, применимых в определённом контексте -->
<assert test=""> ... </assert>
<report test=""> ... </assert>
</rule>
</pattern>
</schema>
Подробное описание элементов и их атрибутов доступно в шпаргалке. Пока же познакомимся с основными из них.
▍ <rule>, <assert> и <report>
Краеугольным камнем Schematron являются «тесты» — утверждения и отчёты, <assert> и <report> соответственно. Фактически именно они и выполняют всю работу по проверкам. Эти элементы должны иметь атрибут @ test, в котором определено XPath-выражение. Работает это так:
- <assert> — тест проваливается, когда результатом XPath-выражения в @ test является FALSE;
- <report> — тест проваливается, когда в @ test возвращается TRUE.
Шаблон XPath, определённый в атрибуте @ context элемента <rule>, задаёт контекст для дочерних <assert> и <report>. Работает это аналогично @ match в элементе <xsl:template> в XSLT. Как и в случае с XSLT, если в @ context указано, например, Categories/Category, то тесты будут применены к элементам Category только в том случае, если те являются дочерними для Categories.
Каждое утверждение или отчёт может иметь атрибут @ role. Его назначение — определение уровня важности теста. Таким образом, валидация может сводиться не к простому «VALID / INVALID», а к принятию решения о действительности XML в зависимости от выявленных «FATAL», «ERROR», «WARNING» и прочих уровней проваленных тестов.
Вернёмся к нашему примеру. С помощью Schematron проверка корректности дат, поиск дубликатов удостоверений и выявление повторяющихся категорий в этих удостоверениях могут быть выполнены следующим образом:
<!-- ещё больше примеров в schematron/main.sch -->
<sch:schema queryBinding="xslt2" xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:pattern name="Date">
<sch:title>Проверка корректности дат</sch:title>
<sch:rule context="License">
<sch:title>Начало и окончание действия удостоверения</sch:title>
<sch:assert test="xs:date(current-date()) >= xs:date(IssueDate)">Дата выдачи документа не должна быть больше текущей даты</sch:assert>
<sch:assert test="xs:date(ExpDate) = xs:yearMonthDuration('P10Y')+xs:date(IssueDate)" role="WARNING">Удостоверение должно быть выдано на 10 лет</sch:assert>
</sch:rule>
<sch:rule context="License/Categories/Category">
<sch:title>Начало и окончание действия категории</sch:title>
<sch:assert test="xs:date(current-date()) >= xs:date(IssueDate)">Дата начала действия категории не должна быть больше текущей даты</sch:assert>
<sch:assert test="xs:date(../../ExpDate) >= xs:date(ExpDate)">Срок действия категории не может превышать срок действия удостоверения</sch:assert>
</sch:rule>
</sch:pattern>
<sch:pattern name="Dublicate">
<sch:title>Проверка на уникальность удостоверения (в границах файла) и категории ТС (в границах удостоверения)</sch:title>
<sch:rule context="License">
<sch:let name="SeriesNumber" value="string-join((Series,Number),' ')"/>
<sch:assert test="count(Series[$SeriesNumber = ../preceding-sibling::License/string-join((Series, Number), ' ')])=0" role="FATAL" >Удостоверение <sch:value-of select="$SeriesNumber"/> повторяется</sch:assert>
</sch:rule>
<sch:rule context="Categories/Category">
<sch:assert test="count(Name[. = ../preceding-sibling::Category/Name])=0" role="ERROR" >Категория "<sch:value-of select="Name"/>" указана в удостоверении несколько раз</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
В случае провала какого-либо теста будет сгенерировано сообщение — значение элемента <assert> или <report>. В консоли это может выглядеть так:
~/x_validation$ java -jar tools/schxslt-cli.jar -d source/main.xml -s schematron/main.sch -v
[invalid] /home/artirm/x_validation/source/main.xml
[invalid] /home/artirm/x_validation/source/main.xml failed-assert /Q{}DrivingLicenses[1]/Q{}License[8]/Q{}Categories[1]/Q{}Category[1] L - не является категорией ТС
[invalid] /home/artirm/x_validation/source/main.xml failed-assert /Q{}DrivingLicenses[1]/Q{}License[12]/Q{}Driver[1] В фамилии водителя обнаружены недопустимые символы
[invalid] /home/artirm/x_validation/source/main.xml failed-assert /Q{}DrivingLicenses[1]/Q{}License[15]/Q{}Driver[1] В отчестве водителя обнаружены недопустимые символы
[invalid] /home/artirm/x_validation/source/main.xml failed-assert /Q{}DrivingLicenses[1]/Q{}License[5] Удостоверение должно быть выдано на 10 лет
[invalid] /home/artirm/x_validation/source/main.xml failed-assert /Q{}DrivingLicenses[1]/Q{}License[4] Удостоверение 11АБ 123456 повторяется
[invalid] /home/artirm/x_validation/source/main.xml failed-assert /Q{}DrivingLicenses[1]/Q{}License[11]/Q{}Categories[1]/Q{}Category[2] Категория "D" указана в удостоверении несколько раз
[invalid] /home/artirm/x_validation/source/main.xml successful-report /Q{}DrivingLicenses[1]/Q{}License[7]/Q{}Driver[1] В удостоверении указаны категории ТС, для управления которыми водитель должен быть старше 16 лет
[invalid] /home/artirm/x_validation/source/main.xml successful-report /Q{}DrivingLicenses[1]/Q{}License[9]/Q{}Categories[1]/Q{}Category[1] Для получения категории "BE" водитель должен иметь право управления транспортными средствами категории "B" не менее 12 месяцев
[invalid] /home/artirm/x_validation/source/main.xml successful-report /Q{}DrivingLicenses[1]/Q{}License[6] Серия водительского удостоверения, выданного после 1 марта 2011 г., должна содержать две цифры и две буквы кириллицы
[invalid] /home/artirm/x_validation/source/main.xml successful-report /Q{}DrivingLicenses[1]/Q{}License[7] Серия водительского удостоверения, выданного до 1 марта 2011 г, должна содержать четыре цифры
[invalid] /home/artirm/x_validation/source/main.xml successful-report /Q{}DrivingLicenses[1]/Q{}License[13]/Q{}Driver[1] Не указана фамилия или имя водителя
[invalid] /home/artirm/x_validation/source/main.xml successful-report /Q{}DrivingLicenses[1]/Q{}License[14]/Q{}Driver[1] Не указана фамилия или имя водителя
[invalid] /home/artirm/x_validation/source/main.xml successful-report /Q{}DrivingLicenses[1]/Q{}License[16]/Q{}Driver[1] Не указано отчество водителя
Ещё немного о работе со схемами Schematron. При их составлении может потребоваться написание довольно сложных XPath-выражений. Откровенно говоря, составление XPath-запросов — самая трудная часть работы. Тем не менее, уже составленный XPath в принципе пригоден к пониманию и неподготовленным в этом плане человеком.
Интеграция cо Schematron
Как правило, процессор Schematron представляет из себя конвейер XSLT-преобразований. Результатом этих преобразований является отчёт в формате XML, известном как Schematron Validation Report Language (SVRL).
Пример реализации Schematron-процессора
Это даёт нам возможность использовать Schematron везде, где доступен XSLT. А применение XSLT-трансформаций к SVRL позволяет получать результаты проверок в HTML, PDF или любом другом формате.
Простейший пример работы со Schematron в PHP можно увидеть в tools/schematron.php. Для работы потребуется SaxonC-HE.
~/x_validation$ php tools/schematron.php > tools/output/output.html
После выполнения вышеуказанной команды в tools/output/output.html появится перечень выполненных <pattern> и список проваленных тестов.
Пример результата трансформации SVRL в HTML
Включения в схемы
XML Schema и схемы Schematron поддерживают элементы <include>, что позволяет «собирать» схемы из отдельных частей. В некоторых ситуациях эти части имеет смысл генерировать автоматически. Например, при необходимости формировать проверки на основе информации из БД. Реализация подобной генерации может выглядеть так: экспорт данных в XML и последующая XSLT-трансформация в Schematron и XSD-схемы (их «части»).
В качестве живого примера сгенерируем Schematron-схемы на основе файлов source/categories.xml и source/pre-training.xml. Для этого выполните (требуется Java):
~/x_validation$ ./tools/scaffold/schematron.sh
Этот скрипт создаст следующие схемы:
- schematron/include/enumerate_categories.sch — проверка, что указаны только известные категории ТС;
- schematron/include/age_restriction.sch — проверка, что возраст водителя соответствует указанным категориям ТС;
- schematron/include/pre-training.sch — проверка, что водитель имеет право получить указанную категорию, т. к. имеет достаточный стаж управления ТС другой категории.
Для генерации XSD-схемы:
~/x_validation$ ./tools/scaffold/xsd.sh
В результате будет сгенерирован schema/include/enumerate_categories.xsd для включения в XSD-схему всех допустимых категорий ТС.
Трансформируем по полной
По сути говоря, применение XSLT ограничено только лишь нашей фантазией. В качестве ещё одного полезного примера использования трансформаций рассмотрим автоматическое создание пользовательского интерфейса из составленных ранее XSD-схем.
Чтобы создавать HTML5-формы из схемы XML, обратимся к проекту xsd2html2xml. Немного изменим JavaScript, создадим и вызовем bash-скрипт.
~/x_validation$ ./tools/scaffold/xsd2html.sh
Вследствие чего и получим UI для создания валидных XML — tools/output/xsd2html.html.
Выпуск документации
Схема Schematron сама по себе содержит достаточно информации для составления технической документации. Но кроме этого, Schematron игнорирует неизвестную ему разметку и позволяет разработчику оставлять дополнительные метаданные в схеме. Всё это даёт повод задуматься над автоматическим созданием документации на основе имеющихся схем.
Возвратимся к примеру и сгенерируем документацию из нашей схемы schematron/main.sch.
~/x_validation$ ./tools/scaffold/docbook.sh
Скриптом tools/scaffold/docbook.sh будет создано два файла: tools/output/docbook[DateTime].xml и tools/output/docbook[DateTime].html.
Пример результата трансформации Shematron-схемы в HTML-документацию
Заключение
В рамках этой короткой заметки я постарался осветить основные моменты работы со Schematron и его место в процессе валидации XML, привести простые, но ёмкие примеры. Тем не менее, за рамками осталось много интересных и важных моментов. Например, совсем не затронута важнейшая тема тестирования схем Schematron. Ничего не сказано о расширении Schematron Quick Fix. Но я надеюсь, что заинтересованный читатель разберётся в этом самостоятельно, а возможно, и дополнит написанное, оставив свой комментарий.
Автор:
artirm