验证Ruby哈希的形状?

时间:2012-06-19 18:10:03

标签: ruby mongodb

问题的另一个更长的替代措辞是:“什么是一些Ruby库或方法来测试任意数据结构的形状(散列,数组,整数,浮点数,字符串等)?”

首先,让我举一个简单的例子:

hash_1 = {
  k1: 1.0,
  k2: 42,
  k3: {
    k4: "100.00",
    k5: "dollars"
  }
}

接下来,我想验证它 - 我的意思是比较形状/架构/模板,例如:

shape_a = {
  k1: Float,
  k2: Integer,
  k3: Hash
}

或许,更具体的形状:

shape_b = {
  k1: Float,
  k2: Integer,
  k3: {
    k4: String,
    k5: String
  }
}

一种可能的API可能如下所示:

require '_____________'

hash_1.schema = shape_a
hash_1.valid? # => true

hash_1.schema = shape_b
hash_1.valid? # => true

这些只是示例,我对其他方法持开放态度。

大约3年前,我写了schema_hash来抓我自己的痒。我打算更新它,但首先我想了解替代方案和更好的方法。

这个问题的动机来自Mongo用例,但问题不一定是Mongo特定的。

就像我在顶部提到的,我希望看到或构建能力验证任意数据结构:哈希,数组,基元等,以任何嵌套组合。

“您不需要Mongo的架构,所以为什么要关心?”

  • 就像我上面提到的,我不是完全在考虑使用Mongo用例
  • 但即使在Mongo的上下文中,即使您不希望要求数据结构采取某种形状,仍然可以根据形状测试数据结构或模式并采取相应行动。

“为什么不写自定义验证?”

当我处理以前的项目时,这正是我开始的地方。为嵌套哈希重复编写验证会很痛苦。我开始思考什么会让它变得更容易,我想出了类似于我上面分享的语法。

那里有什么?我该怎么办?

所有这些都说,我很好奇其他人在做什么。有“黄金之路”吗?我正在尝试不同的方法,包括嵌入式文档和与Mongoid相关的validates_ ...但是当哈希嵌套超过一个级别或更深时,这些方法看起来有点过分。

我四处寻找Validation on Ruby Toolbox进行验证(双关语),但没找到我要找的东西。当我在那里时,我提出了一个名为“验证”的新类别。

很可能我所要求的内容在“验证”主题领域中更少,而在其他领域则更多,例如“数据结构”和“遍历”。如果是这样,请指出我正确的方向。

7 个答案:

答案 0 :(得分:4)

编辑:重读您的问题,我的回答似乎有点过于简单了。我会把它留给你,我应该删除它,只是通过评论告诉我。

一个非常简单的方法是:

class Hash
  def has_shape?(shape)
    all? { |k, v| shape[k] === v }
  end
end

像这样使用:

hash_1.has_shape?(shape_a) #=> true
shape_b = { k1: Float, k2: Integer, k3: Integer }
hash_1.has_shape?(shape_b) #=> false

这似乎已经很好地照顾了你的第一个描述版本。将它排除在lib之外并不难,因此它不会修复Hash。将递归添加到has_shape?方法将会处理更复杂的案例。

更新:这是一个带递归的版本:

class Hash
  def has_shape?(shape)
    all? do |k, v|
      Hash === v ? v.has_shape?(shape[k]) : shape[k] === v
    end
  end
end

答案 1 :(得分:2)

这可能就是你要找的东西:

https://github.com/JamesBrooks/hash_validator

答案 2 :(得分:1)

我编写了一个用于验证哈希数据http://rubygems.org/gems/validates_simple的gem,并且很容易添加验证,以检查值是否是类的成员。至于验证嵌套值,如果需要,我将不得不更新gem以支持它。添加规则就像

一样简单
  module Validation
    module Rules
      def validates_is_a_member_of(field, class, message='')
         callback = lambda do |data|
           data[field].is_a? Class
         end
         validates(field, callback, message)
      end
    end
  end

之后你可以做validator.validates_is_a_member_of('age',整数,'年龄必须是一个int')并做validator.validate({'age':232423})

答案 3 :(得分:1)

如果您使用Rails并且只想验证密钥,则可以使用assert_valid_keys

{ name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
{ name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
{ name: 'Rob', age: '28' }.assert_valid_keys(:name, :age)   # => passes, raises nothing

https://apidock.com/rails/Hash/assert_valid_keys

答案 4 :(得分:1)

这对Michael Kohl的递归has_shape略有调整吗?适用于shape_a和shape_b的方法(添加了一个测试以检查形状是否具有要递归的子哈希):

  def has_shape?(shape)
    all? do |k, v|
      (Hash === v && Hash === shape[k]) ? v.has_shape?(shape[k]) : shape[k] === v
    end
  end

另外,FWIW我发现is_a吗?比===更具可读性。例如:

  def has_shape?(shape)
    all? do |k, v|
      (v.is_a?(Hash) && shape[k].is_a?(Hash)) ? v.has_shape?(shape[k]) : v.is_a?(shape[k])
    end
  end

但是,如果您的形状兼容JSON,则您可能需要考虑使用Ruby JSON Schema Validator。它比较重且冗长,但是您可以使用它来验证哈希和JSON,并为您提供更多控制权(例如,必需的键,可选的键等)。

答案 5 :(得分:0)

似乎Schemacop这样的场合非常适合这种情况。

答案 6 :(得分:0)

以下是使用表达式列表的版本:

class Hash
  def has_shape?(shape)
    all? do |key, value|
      if value.is_a? Hash
        value.has_shape?(shape[key])
      else
        shape[key].all? { |expression| expression.call(value) }
      end
    end
  end
end

用法:

shape = {
  type: [
    -> (type) { type.is_a? String },
    -> (type) { !type.blank?  },
    -> (type) { %(text number).include? type }
  ]
}

{ type: "text" }.has_shape? shape # => true

通过此操作,您应该能够非常动态地确保哈希的形状。

更新:

使用此版本,您将无法检查键/值对是否丢失,因为我们正在询问所有?从哈希中,我们调用该方法。结果到底是什么?仅迭代该哈希上存在的键/值。

我已更新实现,以尊重“目标”哈希值进行比较。现在has_shape?方法成功确定缺少的键/值对。

class Hash
  def has_shape?(shape)
    shape.all? do |key, _|
      if self[key.to_s].is_a? Hash
        self[key.to_s].has_shape?(shape[key])
      else
        shape[key].all? { |expression| expression.call(self[key.to_s]) }
      end
    end
  end
end