我最近写了一个小宝石进行验证,并希望与您分享其实现。
创建库时遵循的想法:
几乎所有这些要点都与第一点相关-简单性。 最终的实现非常小,所以我不会花很多时间。
源代码可以在这里找到。
建筑学
我决定不使用类和块方法来使用通常的DSL,而是决定使用数据。
因此,例如在Dry中,DSL代替了通常的声明式命令(哈哈,好了,您明白吗?是“声明式命令式”)DSL,而我的DSL只是将一些数据集转换为验证器。 这也意味着该库可以(从理论上)以其他动态语言(例如python)实现,甚至不一定是面向对象的。
我阅读了最后一段,并了解我写了一些乱七八糟的东西。 对不起 首先,我将给出一些定义,然后给出一个示例。
定义
整个库基于三个简单的概念构建: 验证器 ,蓝图和转换 。
- 验证器是库的用途。 一个对象,检查是否满足我们的要求。
- 模式只是描述其他数据的任意数据(验证的目的)。
- 变换是一个函数
t(b, f)
,它接受一个电路和调用该函数的对象(工厂),并返回另一个电路或验证器。
顺便说一句,在数学中上下文中的“转换”一词与“功能”一词同义(无论如何,在我读大学的那本书中)。
该工厂正式执行以下操作:
- 对于一组转换
T1, T2, ..., Tn
,创建成分Ta(Tb(Tc(...)))
(顺序是任意的)。 - 所得的成分会循环应用于电路,直到结果与自变量不同为止。
这让我想起了图灵机。 在输出中,我们应该获得一个验证器(或一个匿名函数)。 其他说明方案和/或转换不正确。
例子
在reddit上,一名男子在Dry中举了一个例子:
user_schema = Dry::Schema.Params do required(:id).value(:integer) required(:name).value(:string) required(:age).value(:integer, included_in?: 0..150) required(:favourite_food).value(array[:string]) required(:dog).maybe do hash do required(:name).value(:string) required(:age).value(:integer) optional(:breed).maybe(:string) end end end user_schema.call(id: 123, name: "John", age: 18, ...).success?
如您所见,魔术以required(..).value
形式使用, required(..).value
和方法如#array
。
与我的示例进行比较:
is_valid_user = StValidation.build( id: Integer, name: String, age: ->(x) { x.is_a?(Integer) && (0..150).cover?(x) }, favourite_food: [String], dog: Set[NilClass, { name: String, age: Integer, breed: Set[NilClass, String] }] ) is_valid_user.call(id: 123, name: 'John', age: 18, ...)
- 散列用于描述散列。 值用于描述值(类,数组,集合,匿名函数)。 没有魔术方法(不考虑
#build
,因为它只是缩写)。 - 最终的验证值不是一个复杂的对象,而仅仅是true / false,这是我们最终担心的。 这不是优点,而是简化。
- 在Dry中,定义的外部哈希与内部哈希略有不同。 在外部级别,使用
Schema.Params
方法,在Schema.Params
内部#hash
。 - (奖励)在我的情况下,已验证的对象不必是哈希,并且不需要特殊的语法:
is_int = StValidation.build(Integer)
。
电路本身的每个元素都是一个电路。 哈希是复杂方案(即由其他方案组成的方案)的示例。
结构形式
整个宝石由少量部分组成:
- 主名称空间(模块)
StValidation
- 负责生成验证器的工厂为
StValidation::ValidatorFactory
。 - 抽象验证程序
StValidation::AbstractValidator
,实际上是一个接口。 - 我包含在
StValidation::Validators
模块的基本“语法”中的一组基本验证StValidation::Validators
- 为了方便起见并结合了所有其他元素,主模块提供了两种方法:
StValidation.build
使用一组标准转换StValidation.with_extra_transformations
使用一组标准转换,但会对其进行扩展。
标准DSL
我在自己的DSL中包含以下元素:
- 类-检查对象的类型(例如Integer)。
除了匿名函数和AbstractValidator的后代(它们是生成器的原语)之外,我的语法中最简单的验证器。 - 很多是计划的结合。 示例:
Set[Integer, ->(x) { x.nil? }]
Set[Integer, ->(x) { x.nil? }]
。
检查对象是否与方案中的至少一项匹配。 甚至类本身也称为UnionValidator
。
最简单的示例是复合验证器。 - 数组就是一个例子:
[Integer]
。
检查该对象是否为数组,并且其所有元素均满足特定方案 。 - 散列是相同的,但散列是相同的。 不允许使用额外的密钥。
转换集如下所示:
def basic_transformations [ ->(bp, _factory) { bp.is_a?(Class) ? class_validator(bp) : bp }, ->(bp, factory) { bp.is_a?(Set) ? union_validator(bp, factory) : bp }, ->(bp, factory) { bp.is_a?(Hash) ? hash_validator(bp, factory) : bp }, ->(bp, factory) { bp.is_a?(Array) && bp.size == 1 ? array_validator(bp[0], factory) : bp } ] end def class_validator(klass) Validators::ClassValidator.new(klass) end def union_validator(blueprint, factory) Validators::UnionValidator.new(blueprint, factory) end
没有地方比这更容易了吗?
错误和#explain
对我个人而言,验证的主要目的是检查对象是否有效。 为什么无效,这是一个附带问题。
但是,了解某些无效的原因很有用。 为此,我在验证器接口中添加了#explain
方法。
本质上,它应该执行与验证相同的操作,但是返回特别错误的内容。
通常,仅通过检查解释结果是否为空,即可将验证本身( #call
)定义为#explain
的特例。
但是,这种验证会比较慢(但这并不重要)。
因为 匿名谓词函数将自身包装在AbstractValidator
子孙中;它们还具有#explain
方法,并且仅指示函数的定义位置。
在编写自定义验证器时, #explain
可以是任意复杂和智能的。
客制化
我的“语法”没有内置在库的心脏中,因此不是必需的。 (请参阅StValidation.build
)。
让我们尝试一个更简单的DSL,该DSL仅包含数字,字符串和数组:
validator_factory = StValidation::ValidatorFactory.new( [ -> (blueprint, _) { blueprint == :int ? ->(x) { x.is_a?(Integer) } : blueprint }, -> (blueprint, _) { blueprint == :str ? ->(x) { x.is_a?(String) } : blueprint }, lambda do |blueprint, factory| return blueprint unless blueprint.is_a?(Array) inner_validators = blueprint.map { |b| factory.build(b) } ->(x) { x.is_a?(Array) && inner_validators.zip(x).all? { |v, e| v.call(e) } } end ] ) is_int = validator_factory.build(:int) is_int.call('123')
抱歉,代码有些混乱。 本质上,在这种情况下,数组按索引检查是否符合要求。
总结
但不是他。 我为这个技术解决方案感到骄傲,并想演示一下:)