另一个用于验证的DSL

我最近写了一个小宝石进行验证,并希望与您分享其实现。


创建库时遵循的想法:


  • 简单性
  • 缺乏魔法
  • 易学
  • 定制的可能性和最小的限制。

几乎所有这些要点都与第一点相关-简单性。 最终的实现非常小,所以我不会花很多时间。


源代码可以在这里找到。


建筑学


我决定不使用类和块方法来使用通常的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, ...) 

  1. 散列用于描述散列。 值用于描述值(类,数组,集合,匿名函数)。 没有魔术方法(不考虑#build ,因为它只是缩写)。
  2. 最终的验证值不是一个复杂的对象,而仅仅是true / false,这是我们最终担心的。 这不是优点,而是简化。
  3. 在Dry中,定义的外部哈希与内部哈希略有不同。 在外部级别,使用Schema.Params方法,在Schema.Params内部#hash
  4. (奖励)在我的情况下,已验证的对象不必是哈希,并且不需要特殊的语法: 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') # ==> false is_int_pair = validator_factory.build([:int, :int]) is_int_pair.call([1, 2]) # ==> true is_int_pair.call([1, '2']) # ==> false 

抱歉,代码有些混乱。 本质上,在这种情况下,数组按索引检查是否符合要求。


总结


但不是他。 我为这个技术解决方案感到骄傲,并想演示一下:)

Source: https://habr.com/ru/post/zh-CN479600/


All Articles