第十九天-Language Independent Validation Rules (LIVR) for Raku

Raku 语言独立验证规则

Language Independent Validation Rules (LIVR) for Raku

我刚刚将 LIVR 移植到了 Raku。在 Raku 中编写代码非常有趣。而且,LIVR 的测试套件让我能够在 Raku 的 Email::Valid 模块中发现 bug,而在 Rakudo 中则发现另一个 bug。更有趣的是,不仅仅实现了一个模块,而且还帮助其他开发人员进行了一些测试:)

什么是 LIVR? LIVR 代表“语言独立验证规则”。所以,它就像 “Mustache” ,但在验证的世界。所以,LIVR 由以下几部分组成:

LIVR 有如下语言的实现:

我会在这里给你一个关于LIVE的简短介绍,但是对于细节,我强烈推荐阅读这篇文章 “LIVR – Data Validation Without Any Issues”

LIVR 介绍

数据验证是一项非常普遍的任务。我确信每个开发者都会一次又一次面对它。尤其是,当您开发Web应用程序时,这一点很重要。这是一条通用规则 - 绝对不要相信用户的输入。看起来,如果任务如此普遍,应该有大量的图库。是的,但它是很难找到一个理想的。有些库做了太多事情(如 HTML 表单生成等),其他库很难扩展,有些没有分层数据支持等。

而且,如果您是一名 Web 开发人员,则可能需要在服务器和客户端上进行相同的验证。

在 WebbyLab 中,我们主要使用 3 种编程语言 - Perl,JavaScript,PHP。因此,对我们来说,重用跨语言的类似验证方法是理想的选择。

因此,决定创建一个可以跨不同语言工作的通用验证器。

验证器要求

在尝试了大量的验证库之后,我们对我们想要解决的问题有了一些想法。以下是验证器的要求:

  • 规则是声明式并独立于语言的。因此,验证规则只是一个数据结构,而不是方法调用等。您可以对其进行转换,在对其他数据结构进行更改时进行更改
  • 每个字段的任何数量的规则
  • 验证器应该返回所有字段的错误。例如,我们想突出显示表单中的所有错误
  • 剪掉所有没有描述验证规则的字段。 (否则,你不能依赖你的验证,如果验证器不符合这个属性,总有一天你会遇到安全问题)
  • 可以验证复杂的层次结构。特别适用于 JSON APIs
  • 易于描述和理解验证
  • 返回可理解的错误代码(既不是错误消息也不是数字代码)
  • 易于实现自己的规则(通常你会在每个项目中有几个)
  • 规则应该能够改变结果输出(例如,“trim”,“nested_object”)
  • 多用途(用户输入验证,配置验证等)
  • Unicode 支持

LIVR规范

由于该任务设置为独立于编程语言(某种胡须/句柄的东西)创建验证器,但在数据验证领域内,我们从规范的组成开始。

规范的目标是:

  • 标准化数据描述格式。
  • 描述每个实现必须支持的最小验证规则集。
  • 标准化错误代码。
  • 成为所有实现的单个基本文档。
  • 具有一组测试数据,可以检查实现是否符合规范。
  • 基本思想是验证规则的描述必须看起来像数据方案,并且尽可能与数据类似,但是使用规则而不是值。

该规范可在 http://livr-spec.org/ 获得。

这是基本的介绍。更多细节在我上面提到的文章中。

LIVR和Raku

让我们玩得开心,玩一段代码。我将通过几个例子,并在每个例子后提供一些内部细节。所有示例的源代码都可以在 GitHub 上找到

首先,从 CPAN 安装 Raku 的 LIVR 模块

zef install LIVR

示例1:注册数据验证

use LIVR;

# Automatically trim all values before validation
LIVR::Validator.default-auto-trim(True);

my $validator = LIVR::Validator.new(livr-rules => {
    name      => 'required',
    email     => [ 'required', 'email' ],
    gender    => { one_of => ['male', 'female'] },
    phone     => { max_length => 10 },
    password  => [ 'required', {min_length => 10} ],
    password2 => { equal_to_field => 'password' }
});

my $user-data = {
    name      => 'Viktor',
    email     => 'viktor@mail.com',
    gender    => 'male',
    password  => 'mypassword123',
    password2 => 'mypassword123'
}


if my $valid-data = $validator.validate($user-data) {
    # $valid-data is clean and does contain only fields 
    # which have validation and have passed it
    $valid-data.say;
} else {
    my $errors = $validator.errors();
    $errors.say;
}

那么,如何理解规则?

这个想法很简单。每条规则都是一个散列. key - 验证规则的名称。value - 一个参数数组。

例如:

{ 
    name  => { required => [] },
    phone => { max_length => [10] }
}

但如果只有一个参数,则可以使用较短的形式:

{ 
    phone => { max_length => 10 }
}

如果没有参数,则可以将规则的名称作为字符串传递:

{ 
    name => 'required'
}

您可以在数组中给字段传递一个规则列表:

{ 
    name => [ 'required', { max_length => 10 } ]
}

在这种情况下,规则将陆续应用。因此,在这个例子中,首先,“required” 规则将被应用,“max_length” 之后,并且只有当 “required” 成功通过时。

这里是 LIVR 规范的细节

你可以在这里找到标准规则的列表。

例2:分层数据结构的验证

use LIVR;

my $validator = LIVR::Validator.new(livr-rules => {
    name  => 'required',
    phone => {max_length => 10},
    address => {'nested_object' => {
        city => 'required', 
        zip  => ['required', 'positive_integer']
    }}
});

my $user-data = {
    name  => "Michael",
    phone => "0441234567",
    address => {
        city => "Kiev", 
        zip  => "30552"
    }
}

if my $valid-data = $validator.validate($user-data) {
    # $valid-data is clean and does contain only fields 
    # which have validation and have passed it
    $valid-data.say;
} else {
    my $errors = $validator.errors();
    $errors.say;
}

这个例子中有趣的是什么?

  • 模式(验证规则)形状与数据形状非常相似。例如,读取比 JSON Schema 容易得多。
  • 看起来 “nested_object” 是一种特殊的语法,但它不是。验证器在 “required”,“nested_object”,“max_length” 之间没有任何区别。所以,核心非常小,您可以轻松地使用自定义规则引入新功能。
  • 通常你想重用复杂的验证规则,比如 “address”,并且可以使用别名来完成。
  • 您将收到分层错误消息。例如,如果您错过 city 和 name,错误对象将显示 {name =>'REQUIRED',address => {city =>'REQUIRED'}}

别名

use LIVR;

LIVR::Validator.register-aliased-default-rule({
    name  => 'short_address', # names of the rule
    rules => {'nested_object' => {
        city => 'required', 
        zip  => ['required', 'positive_integer']
    }},
    error => 'WRONG_ADDRESS' # custom error (optional)
});

my $validator = LIVR::Validator.new(livr-rules => {
    name    => 'required',
    phone   => {max_length => 10},
    address => 'short_address'
});

my $user-data = {
    name  => "Michael",
    phone => "0441234567",
    address => {
        city => "Kiev", 
        zip  => "30552"
    }
}

if my $valid-data = $validator.validate($user-data) {
    # $valid-data is clean and does contain only fields 
    # which have validation and have passed it
    $valid-data.say;
} else {
    my $errors = $validator.errors();
    $errors.say;
}

如果你愿意,你可以只为你的验证器实例注册别名:

use LIVR;

my $validator = LIVR::Validator.new(livr-rules => {
    password => ['required', 'strong_password']
});

$validator.register-aliased-rule({
    name  => 'strong_password',
    rules => {min_length => 6},
    error => 'WEAK_PASSWORD'
});

示例3:数据修改,流水线 有规则可以做数据修改。以下是他们的列表:

  • trim
  • to_lc
  • to_uc
  • remove
  • leave_only
  • default

你可以在这里阅读细节

用这种方法,你可以创建某种管道。

use LIVR;

my $validator = LIVR::Validator.new(livr-rules => {
    email => [ 'trim', 'required', 'email', 'to_lc' ]
});

my $input-data = { email => ' EMail@Gmail.COM ' };
my $output-data = $validator.validate($input-data);

$output-data.say;

这里重要的是什么?

  • 正如我之前提到的,对于验证器来说,任何规则都没有区别。它以同样的方式处理 “trim”,“default”,“required”,“nested_object”。
  • 规则一个接一个地应用。规则的输出将被传递给下一个规则的输入。这就像一个 bash 管道 echo ' EMail@Gmail.COM ' | trim | required | email | to_lc
  • $input-data 永远不会改变 $output-data 是验证后使用的数据。

示例4:自定义规则

您可以使用别名作为自定义规则,但有时这还不够。编写自己的自定义规则绝对没问题。你可以用自定义规则做几乎所有事情。

通常,我们在每个项目中都有 1-5 个自定义规则。此外,您可以将自定义规则组织为单独的可重用模块(甚至可以将其上传到 CPAN)。

那么,如何为 LIVR 编写自定义规则?

这里是’strong_password’的例子:

use LIVR;

my $validator = LIVR::Validator.new(livr-rules => {
    password => ['required', 'strong_password']
});

$validator.register-rules( 'strong_password' =>  sub (@rule-args, %builders) {
    # %builders - are rules from original validator
    # to allow you create new validator with all supported rules
    # my $validator = LIVR::Validator.new(livr-rules => $livr).register-rules(%builders).prepare();
    # See "nested_object" rule implementation for example
    # https://github.com/koorchik/raku-livr/blob/master/lib/LIVR/Rules/Meta.pm6#L5

    # Return closure that will take value and return error
    return sub ($value, $all-values, $output is rw) {
        # We already have "required" rule to check that the value is present
        return if LIVR::Utils::is-no-value($value); # so we skip empty values

        # Return value is a string
        return 'FORMAT_ERROR' if $value !~~ Str && $value !~~ Numeric;

        # Return error in case of failed validation
        return 'WEAK_PASSWORD' if $value.chars < 6;

        # Change output value. We want always return value be a string
        $output = $value.Str; 
        return;
    };
});

查看更多示例的现有规则实现:

示例5:Web 应用程序

LIVR 适用于 REST API。通常,很多 REST API 在返回可理解的错误方面存在问题。如果您的 API 用户将收到 HTTP 错误 500,它不会帮助他。更好的时候,他会得到类似的错误:

{
    "name": "REQUIRED",
    "phone": "TOO_LONG",
    "address": {
        "city": "REQUIRED",
        "zip": "NOT_POSITIVE_INTEGER"
    }
}

而不仅仅是“服务器错误”。

所以,让我们试着做一个带有两个端点的小型 Web 服务:

  • GET /notes -> get list of notes
  • POST /notes -> create a note

您需要为其安装 Bailador:

zef install Bailador

我们来创建一些服务。我更喜欢带有 “run”模板方法的服务中的 “Command”模式。

我们将有 2 项服务:

  • Service::Notes::Create
  • Service::Notes::List

服务使用示例:

my %CONTEXT = (storage => my @STORAGE);

my %note = title => 'Note1', text => 'Note text';

my $new-note = Service::Notes::Create.new( 
    context => %CONTEXT 
).run(%note);

my $list = Service::Notes::Create.new( 
    context => %CONTEXT 
).run({});

有了上下文,你可以注入任何依赖关系。 “run” 方法接受用户传递的数据。

以下是创建笔记服务的源代码:

use Service::Base;
my $LAST_ID = 0;
class Service::Notes::Create is Service::Base {
    has %.validation-rules = (
        title => ['required', {max_length => 20} ],
        text  => ['required', {max_length => 255} ]
    );

    method execute(%note) {
        %note<id> = $LAST_ID++;
        $.context<storage>.push(%note);
        
        return %note;
    }
}

和 Service::Base 类:

use LIVR;
LIVR::Validator.default-auto-trim(True);

class Service::Base {
    has $.context = {};

    method run(%params) {
        my %clean-data = self!validate(%params);
        return self.execute(%params);
    }

    method !validate($params) {
        return $params unless %.validation-rules.elems;

        my $validator = LIVR::Validator.new(
            livr-rules => %.validation-rules
        );

        if my $valid-data = $validator.validate($params) {
            return $valid-data;
        } else {
            die $validator.errors();
        }
    }
}

“run” 方法保证所有过程都被保留:

  • 数据已经过验证。
  • “execute” 仅在验证后才会调用。
  • “execute” 将只收到干净的数据。
  • 在验证错误的情况下引发异常。
  • 在调用“execute”之前可以检查权限。
  • 可以执行额外的工作,如缓存验证器对象等。

这是完整的工作示例

运行应用程序:

raku app.pl6

创建一个 note:

curl -H "Content-Type: application/json" -X POST -d '{"title":"New Note","text":"Some text here"}' http://localhost:3000/notes

检查验证:

curl -H "Content-Type: application/json" -X POST -d '{"title":"","text":""}' http://localhost:3000/notes

获取notes列表:

curl http://localhost:3000/notes

LIVR 链接

我希望你会喜欢 LIVR。我会很感激任何反馈。

LIVR 
comments powered by Disqus