Language Independent Validation Rules (LIVR) for Raku
我刚刚将 LIVR 移植到了 Raku。在 Raku 中编写代码非常有趣。而且,LIVR 的测试套件让我能够在 Raku 的 Email::Valid 模块中发现 bug,而在 Rakudo 中则发现另一个 bug。更有趣的是,不仅仅实现了一个模块,而且还帮助其他开发人员进行了一些测试:)
什么是 LIVR? LIVR 代表“语言独立验证规则”。所以,它就像 “Mustache” ,但在验证的世界。所以,LIVR 由以下几部分组成:
LIVR 有如下语言的实现:
- Perl 5 (LIVR 2.0) available at CPAN, 维护者 @koorchik
- Raku (LIVR 2.0) available at CPAN, 维护者 @koorchik
- JavaScript (LIVR 2.0) available at npm, 维护者 @koorchik
- PHP (LIVR 2.0) available at packagist, 维护者 @WebbyLab
- Python (LIVR 2.0) available at pypi, 维护者 @asholok
- OLIFER Erlang (LIVR 2.0), 维护者 @Prots
- LIVER Erlang (LIVR 2.0), 维护者 @erlangbureau
- Java (LIVR 2.0), 维护者 @vlbaluk
- Ruby (LIVR 0.4, previous version) at rubygems, 维护者 @maktwin
我会在这里给你一个关于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 链接
- The source code of all examples
- 文章 “LIVR – Data Validation Without Any Issues”
- LIVR specifications and docs (the latest version – 2.0)
- Universal test suite
- 你可以在线玩 LIVR Playground
- 你可以在线玩 LIVR Multi-Language Playground
我希望你会喜欢 LIVR。我会很感激任何反馈。