Validation DSL на Groovy

    Одна из часто встречающихся проблем императивных языков программирования это отсутствие выражений для декларативных структур, таких как, к примеру, древовидные разметки или набор правил для обработки данных. В Groovy эта проблема решена с помощью классов типа Builder и мета-программированием на уровне абстрактного синтаксического дерева.

    В свою очередь, обработка входных данных — один из неотъемлемых этапов работы клиент-серверных приложений. Этот процесс часто описывается последовательностью из if-oв и соответствующих исключений. При большом количестве параметров такой код становится плохо читаем и делает его анализ на предмет корректности нетривиальной задачей. Так, зачастую логика препроцессинга данных «размазана» по нескольким методам и классам. К тому же при появлении нового параметра легко забыть о его проверке и нормализации (форматировании, экранировании символов и т.д.). В качестве решения этой проблемы была написана Groovy библиотека позволяющая описывать эти правила в декларативном стиле с помощью DSL. Например, следующий скрипт обрабатывает шесть входящих параметров: адрес почты, логин, пол, состояние чекбокса «Согласен с регламентом», вес и дату.

    import org.grules.console.Gender
    
    // isEmail это обычный Java/Groovy метод, принимающий один параметр — email. Для использования собственного метода достаточно импортировать его в скрипт с помощью import static.
    email isEmail ["Invalid email"]
    
    // Оператор >> разделяет два последовательных подправила — isAlpha и isUnique. invalidLoginМеssage и dupLoginМеssage это строки сообщений об ошибках. 
    login isAlpha [invalidLoginМеssage] >> isUnique [dupLoginМеssage]
    
    // Gender это Java enum. Если входной параметр gender не задан, то ему присваивается строка "MALE". В качестве значения по умолчанию может выступать любой Groovy объект.
    gender["MALE"] toEnum(Gender)
    
    // m представляет собой ResourceBundle, соответственно m.agreeToTerms — интернационализированная строка.
    termsCondition[""] !isEmpty [m.agreeToTerms]
    
    // Вместо вызoва метода есть возможность использовать замыкания. В этом случае обращение к параметру осуществляется через переменную it.
    weight toPositiveBigDecimal [decimalErr] >> {round it / 1000}
    
    // Поддерживаются логические операции &&, || и !. В качестве значения параметра может выступать как строка так и любой другой тип данных.
    endDate isAfterNow && isBefore(deadline) && {it.day != 1}
    

    Результатом выполнения скрипта является отчет в виде Java объекта с параметрами, разделенными на пять групп:
    • корректные
    • не прошедшие валидацию
    • отсутствующие во входных данных
    • для которых не было найдено соответствующего правила препроцессинга
    • с ненайденными или невалидными зависимостями на другие параметры

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

    Все что необходимо от разработчика чтобы начать использовать DSL это включить библиотеку в classpath проекта (к примеру через Мaven) и создать Groovy скрипт с суфиксом Grules и описанием правил обработки для всех или некоторых параметров.

    «HelloWorld» проект состоит из трех файлов:

    HelloGrules.groovy

    package test
    
    email isEmail ["Invalid email"]
    age toPositiveInt ["Invalid age"] >> {it > 18} ["You must be adult"]
    

    Test.groovy

    package test
    
    import org.grules.Grules
    
    class Test {
    
      public static void main(String[] s) {
        def grules = new Grules()
        def result = grules.applyRules(HelloGrules, [email: "megmail.com", age: "35"])
        assert result.cleanParameters.age == 35
        assert "email" in result.invalidParameters
        assert result.invalidParameters.email.errorId == "Invalid email"
        println result
      }
    }
    

    build.gradle

    apply plugin: 'application'
    apply plugin: 'groovy'
    apply plugin: 'maven'
    
    repositories {
       mavenCentral()
    }
    
    mainClassName = "test.Test"
    
    dependencies {
      groovy 'org.codehaus.groovy:groovy:2.0.5' exclude group: 'asm', module: 'asm' 
      compile 'org.grules:grules:0.2.0.8' exclude group: 'asm', module: 'asm' 
    }
    

    Для запуска выполните команду gradle run в корневой директории.

    Детальнее с проектом можно ознакомиться на следующих ресурсах: wiki, онлайн консоль, github, публикация (английский).
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 3
    • 0
      Очень интересно, но слишком кратко. Вы бы объяснили как реализован этот DSL, пример входного файла и как на нём работает этот DSL
      • 0
        DSL реализован с помощью AST трансформаций на этапе компиляции. Все выражения конвертируются и заменяются на соответствующие вызовы методов библиотеки. После этого на выходе получаем обычный Groovy скрипт. К примеру, файл из статьи HelloGrules.groovy будет выглядеть следующим образом:

        public class test.HelloGrules extends groovy.lang.Script {
        
            public test.HelloGrules() {
            }
        
            public test.HelloGrules(groovy.lang.Binding context) {
                super.setBinding(context)
            }
        
            public static void main(java.lang.String[] args) {
                org.codehaus.groovy.runtime.InvokerHelper.runScript(test.HelloGrules, args)
            }
        
            public java.lang.Object run() {
                this.applyRuleToRequiredParameter('email', {
                    org.grules.script.expressions.SubrulesSeqWrapper.wrap(new org.grules.script.expressions.FunctionTerm({
                        this.isEmail(it)
                    }, 'isEmail') [ 'Invalid email'])
                })
                this.applyRuleToRequiredParameter('age', {
                    org.grules.script.expressions.SubrulesSeqWrapper.wrap(new org.grules.script.expressions.FunctionTerm({
                        this.toPositiveInt(it)
                    }, 'toPositiveInt') [ 'Invalid age']) >> {
                        it  > 18
                    } [ 'You must be and adult']
                })
            }
        }
        

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

        Добавил HelloWorld пример для наглядности. Также есть небольшое видео с туториалом по использованию библиотеки в проекте под управлением Grails (English).

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