【问题标题】:Recursive Hash transformation function in rubyruby 中的递归哈希转换函数
【发布时间】:2018-03-14 01:43:26
【问题描述】:

我有一个响应模式的以下招摇 (openAPI) 定义:

h = { "type"=>"object",
      "properties"=>{
        "books"=>{
          "type"=>"array",
          "items"=>{
            "type"=>"object",
            "properties"=>{
              "urn"  =>{ "type"=>"string" },
              "title"=>{ "type"=>"string" }
            }
          }
        }
      }
    }

并希望将其转换为以下格式,以便能够将此响应显示为树:

{ "name"=>"200",
  "children"=> [
    {
      "name"=>"books (array)",
      "children"=> [
        {"name"=>"urn (string)" },
        {"name"=>"title (string)" }
      ]
    }
  ]
}

在 swagger 模式格式中,节点可以是对象(具有属性),也可以是项目数组,它们本身就是对象。下面是我写的函数:schema参数是上图swagger格式的hash,tree变量包含{name: "200"}

      def build_tree(schema, tree)
        if schema.class == ActiveSupport::HashWithIndifferentAccess
          case schema[:type]
          when 'object'
            tree[:children] = []
            schema[:properties].each do |property_name, property_schema|
               tree[:children] <<
                 { name: property_name, children: build_tree(property_schema, tree) }
            end
          when 'array'
            schema[:items].each do |property_name, property_schema|
              tree[:children] <<
                { name: property_name, children: build_tree(property_schema, tree) }
            end
          when nil
            tree[:name] == schema
          end
          else
            tree[:name] == schema
          end
        end

不幸的是,我认为我在某处犯了错误,因为这会返回以下哈希:

{ :name=>"200",
  :children=>[
    { :name=>"type", :children=>false },
    { :name=>"properties", :children=>false },
    { :name=>"books",
      :children=>{
        "type"=>"object",
        "properties"=>{
          "urn"=>{"type"=>"string"},
          "title"=>{"type"=>"string"}
        }
      }
    }
  ]
}

我一定是在递归中错过了一步或以错误的方式传递了树,但我担心我没有足够的脑力来弄清楚:) 也许是一个善良的灵魂,有写美丽的红宝石的天赋代码会帮我一把!

【问题讨论】:

    标签: ruby recursion hashmap iteration


    【解决方案1】:

    所以项目数组不是项目数组而是子模式的属性数组。这是考虑到这一事实的新解决方案:

    schema = 
        { "type"=>"object",
          "properties"=>{
            "books"=>{
              "type"=>"array",
              "items"=> {
                  "type"=>"object",
                  "properties" => {
                    "urn"   => { "type"=>"string" },
                    "title" => { "type"=>"string" }
                                  }
              } # end items
            } # end books
          } # end properties
        } # end schema
    
    tree = {"name"=>"200"}
    
    def build_tree(schema, tree, level)
        puts
        puts "level=#{level} schema[:type]=#{schema['type'].inspect}, schema class is #{schema.class}"
        puts "level=#{level} tree=#{tree}"
        case schema['type']
        when 'object'
            puts "in when object for #{schema['properties'].size} properties :"
            i = 0
            schema['properties'].each_key{ | name | puts "#{i+=1}. #{name}" }
            tree[:children] = []
            schema['properties'].each do | property_name, property_schema |
                puts "object level=#{level}, property_name=#{property_name}"
                type, sub_tree = build_tree(property_schema, {}, level + 1)
                puts "object level=#{level} after recursion, type=#{type} sub_tree=#{sub_tree}"
                child = { name: property_name + type }
                sub_tree.each { | k, v | child[k] = v }
                tree[:children] << child
            end
            puts "object level=#{level} about to return tree=#{tree}"
            tree
        when 'array'
            puts "in when array"
            case schema['items']
            when Hash
                puts "in when Hash"
                puts "the schema has #{schema['items'].keys.size} keys :"
                schema['items'].keys.each{ | key | puts key }
                # here you could raise an error if the two keys are NOT "type"=>"object" and "properties"=>{ ... }
                puts "Hash level=#{level} about to recurs"
                return ' (array)', build_tree(schema['items'], {}, level + 1)
            else
                puts "oops ! Hash expected"
                "oops ! Hash expected"
            end
        when 'string'
            puts "in when string, schema=#{schema}"
            return ' (string)', {}
        else
            puts "in else"
            tree[:name] == schema # ???? comparison ?
        end
    end
    
    build_tree(schema, tree, 1)
    puts 'final result :'
    puts tree
    

    结果已编辑(使用 ruby​​ 2.3.3p222 测试):

    { "name"=>"200", 
      :children=> [
        {
          :name=>"books (array)", 
          :children=> [
            {:name=>"urn (string)"}, 
            {:name=>"title (string)"}
          ]
        }
      ]
    }
    

    不要将其视为出色的代码。每次发生 12 级地震时,我都会编写 Ruby 代码。目的是解释代码中的哪些问题并引起人们注意在递归调用中使用新变量(现在是空哈希)。有很多情况需要测试并引发错误。

    正确的方法是 BDD 和 @moveson 一样:首先为所有情况编写 RSpec 测试,尤其是边缘情况,然后编写代码。我知道它给人的感觉太慢了,但从长远来看,它是值得的,并且取代了跟踪的调试和打印。

    更多测试

    此代码很脆弱:例如,如果类型键未与属性键关联,它将在 schema['properties'].eachundefined method 'each' for nil:NilClass 处失败。像这样的规格 context 'when a type object has no properties' do let(:schema) { {"type" => "object", "xyz" => ...

    将有助于添加代码来检查前置条件。我也懒得将 RSpec 用于小脚本,但对于认真的开发,我会努力,因为我已经认识到它的好处。花在调试上的时间永远消失了,花在规范上的时间在发生变化时提供了安全性,以及关于代码做什么或不做什么的清晰易读的缩进报告。我推荐全新的Rspec 3 book

    关于访问哈希的另一个词:如果您有字符串和符号的混合,这是问题的根源。

    some_key = some_data # sometimes string, sometimes symbol
    schema[some_key]...
    

    如果内部键与外部数据的类型不同,则不会找到该元素。在创建哈希时选择一种类型,例如符号,并将访问变量系统地转换为符号:

    some_key = some_data # sometimes string, sometimes symbol
    schema[some_key.to_sym]...
    

    或全部为字符串:

    some_key = some_data # sometimes string, sometimes symbol
    schema[some_key.to_s]...
    

    【讨论】:

    • 您好,伯纳德,非常感谢您更新您的答案 - 我在写问题时应该更清楚;从谷歌告诉我的情况来看,12 级地震并不经常发生,所以我非常感谢你花时间重新审视我的问题。 BDD 确实可能是要走的路,我只能怪我的懒惰没有更严格地采用这种方法。正如您所写,利大于弊。我已经尝试使用您的第一个答案自行解决,但到目前为止,我仍然在智力上不知所措 - 再次感谢您如此彻底!
    • @DizzyPazzy 编辑了 when 'array' ... when Hash 中的评论,并在末尾添加了 More on tests 评论。
    【解决方案2】:

    @BernardK 建议的解决方案的长度令人印象深刻,但我无法让它发挥作用。这是我更谦虚的解决方案。我将它包装在一个类中,以便我可以正确测试它。

    您的代码的一个问题是,在几个地方,您返回tree[:name] == schema,其计算结果为false。我想你的意思是分配tree[:name] = schema,然后返回tree

    与@BernardK 一样,我假设“数组”类型的模式将具有一组事物作为其值。如果这不是它的工作原理,那么您能否提供一个示例,其中“数组”不仅仅是围绕“对象”的附加层?

    希望在这个答案和另一个答案之间,你可以从中做出一些适合你的东西。

    # swagger.rb
    
    class Swagger
    
      def self.build_tree(schema, tree)
        if schema.class == ActiveSupport::HashWithIndifferentAccess
          case schema['type']
          when 'object'
            tree['children'] = schema['properties'].map do |property_name, property_schema|
              build_tree(property_schema, {'name' => property_name})
            end
            tree
          when 'array'
            schema['items'].map do |item|
              build_tree(item, {'name' => "#{tree['name']} (array)"})
            end
          when 'string'
            {'name' => "#{tree['name']} (string)"}
          end
        else
          raise ArgumentError, "Expected a HashWithIndifferentAccess but got #{schema.class}: #{schema}"
        end
      end
    end
    

    这是规范文件:

    # /spec/swagger_spec.rb
    
    require_relative '../swagger'
    
    describe Swagger do
      describe '.build_tree' do
        context 'when given a Hash whose type is string' do
          let(:tree) { {"name" => "urn"} }
          let(:schema) { {"type" => "string"}.with_indifferent_access }
          let(:expected) { {"name" => "urn (string)"} }
    
          it 'returns a Hash with "name" as the key and the tree value and its type as the value' do
            expect(Swagger.build_tree(schema, tree)).to eq(expected)
          end
        end
    
        context 'when given a simple schema' do
          let(:tree) { {"name" => "200"} }
          let(:schema) { {"type" => "object",
                          "properties" => {
                              "urn" => {"type" => "string"},
                              "title" => {"type" => "string"}
                          }}.with_indifferent_access }
    
          let(:expected) { {"name" => "200",
                            "children" => [{"name" => "urn (string)"},
                                           {"name" => "title (string)"}
                            ]} }
    
          it 'transforms the tree into swagger (openAPI) format' do
            expect(Swagger.build_tree(schema, tree)).to eq(expected)
          end
        end
    
        context 'when given a complicated schema' do
          let(:tree) { {"name" => "200"} }
          let(:schema) { {"type" => "object",
                          "properties" =>
                              {"books" =>
                                   {"type" => "array",
                                    "items" =>
                                        [{"type" => "object",
                                          "properties" =>
                                              {"urn" => {"type" => "string"}, "title" => {"type" => "string"}}
                                         }] # <-- added brackets
                                   }
                              }
          }.with_indifferent_access }
    
          let(:expected) { {"name" => "200",
                            "children" =>
                                [[{"name" => "books (array)",
                                  "children" => [{"name" => "urn (string)"}, {"name" => "title (string)"}]
                                  }]]
          } }
    
          it 'transforms the tree into swagger (openAPI) format' do
            expect(Swagger.build_tree(schema, tree)).to eq(expected)
          end
        end
    
        context 'when given a schema that is not a HashWithIndifferentAccess' do
          let(:tree) { {"name" => "200"} }
          let(:schema) { ['random array'] }
    
          it 'raises an error' do
            expect { Swagger.build_tree(schema, tree) }.to raise_error ArgumentError
          end
        end
      end
    end
    

    【讨论】:

    • 您好@moveson,感谢您的详尽回答-我无法访问包含数组的响应的其他招摇定义,但会确保在早上的第一件事上为您提供- 再次感谢您花时间查看我的问题,甚至为它编写规范!
    【解决方案3】:

    递归!试试 Elixir :-)

    为了跟踪递归方法,我写了很多 puts 并添加了一个级别号。

    因为我没有 Rails,所以我删除了 Rails 的东西。通过这个轻微的修改,您的输入(其中书籍数组不是数组!)和您的代码:

    schema = 
        { "type"=>"object",
          "properties"=>{
            "books"=>{
              "type"=>"array",
              "items"=>{
                "type"=>"object",
                "properties"=>{
                  "urn"  =>{ "type"=>"string" },
                  "title"=>{ "type"=>"string" }
                }
              }
            }
          }
        }
    
    tree = {}
    
    def build_tree(schema, tree, level)
        puts "level=#{level} schema[:type]=#{schema['type'].inspect}, schema class is #{schema.class}"
        case schema['type']
        when 'object'
            puts "in when object for #{schema['properties'].size} properties :"
            i = 0
            schema['properties'].each_key{ | name | puts "#{i+=1}. #{name}" }
            tree[:children] = []
            schema['properties'].each do | property_name, property_schema |
                puts "level=#{level} property_name=#{property_name}"
                tree[:children] << { name: property_name, children: build_tree(property_schema, tree, level + 1) }
            end
        when 'array'
            puts "in when array for #{schema['items'].size} items will process following items :"
            i = 0
            schema['items'].each_key{ | name | puts "#{i+=1}. #{name}" }
            schema['items'].each do | property_name, property_schema |
                puts "level=#{level} property_name=#{property_name}, property_schema=#{property_schema.inspect}"
                tree[:children] << { name: property_name, children: build_tree(property_schema, tree, level + 1) }
            end
        when nil
            puts "in when nil"
            tree[:name] == schema
        end
    end
    
    build_tree(schema, tree, 1)
    puts tree
    

    结果就是你得到的:

    $ ruby -w t_a.rb 
    level=1 schema[:type]="object", schema class is Hash
    in when object for 1 properties :
    1. books
    level=1 property_name=books
    level=2 schema[:type]="array", schema class is Hash
    in when array for 2 items will process following items :
    1. type
    2. properties
    level=2 property_name=type, property_schema="object"
    level=3 schema[:type]=nil, schema class is String
    in when nil
    level=2 property_name=properties, property_schema={"urn"=>{"type"=>"string"}, "title"=>{"type"=>"string"}}
    level=3 schema[:type]=nil, schema class is Hash
    in when nil
    {:children=>[
        { :name=>"type", :children=>false}, 
        { :name=>"properties", :children=>false}, 
        { :name=>"books", 
          :children=>{
            "type"=>"object", 
            "properties"=>{
                "urn"=>{"type"=>"string"}, 
                "title"=>{"type"=>"string"}
            }
          }
        }
      ]
    }
    

    (注意:我已经手动漂亮地打印了结果树)。

    跟踪显示发生了什么:在when 'array' 中,当您编写schema['items'].each 时,您可能想要遍历多个项目。但是没有项目,只有一个哈希。所以schema['items'].each 变成了对键的迭代。然后,您使用没有 'type' 键的模式递归,因此 case schema['type'] 属于 when nil

    请注意,如果 when 'object' 被递归调用而不是 when niltree[:children] = [] 将删除以前的结果,因为您始终使用相同的初始 tree。要堆叠中间结果,您需要在递归调用中提供新变量。

    理解递归的最好方法不是循环到方法的开头,而是想象一连串的调用:

    method_1
       |
       +------> method_2
                   |
                   +------> method_3
    

    如果您将相同的初始参数作为参数传递给递归调用,它会被最后一个返回值擦除。但是如果你传递一个新变量,你可以在累加操作中使用它。

    如果您检查 schema['items'] 确实是一个数组,就像我在我的解决方案中所做的那样,您会看到输入与预期不符:

    $ ruby -w t.rb 
    level=1 schema[:type]="object", schema class is Hash
    in when object for 1 properties :
    1. books
    level=1 property_name=books
    level=2 schema[:type]="array", schema class is Hash
    in when array
    oops ! Array expected
    {:children=>[{:name=>"books", :children=>"oops ! Array expected"}]}
    

    现在我的解决方案。我把化妆品的细节留给你。

    schema = 
        { "type"=>"object",
          "properties"=>{
            "books"=>{
              "type"=>"array",
              "items"=> [ # <----- added [
                { "type"=>"object",
                  "properties" => {
                    "urn"   => { "type"=>"string" },
                    "title" => { "type"=>"string" }
                                  }
                },
                { "type"=>"object",
                  "properties" => {
                    "urn2"   => { "type"=>"string" },
                    "title2" => { "type"=>"string" }
                                  }
                }
                        ] # <----- added ]
            } # end books
          } # end properties
        } # end schema
    
    tree = {"name"=>"200", children: []}
    
    def build_tree(schema, tree, level)
        puts
        puts "level=#{level} schema[:type]=#{schema['type'].inspect}, schema class is #{schema.class}"
        puts "level=#{level} tree=#{tree}"
        case schema['type']
        when 'object'
            puts "in when object for #{schema['properties'].size} properties :"
            i = 0
            schema['properties'].each_key{ | name | puts "#{i+=1}. #{name}" }
            schema['properties'].each do | property_name, property_schema |
                puts "object level=#{level}, property_name=#{property_name}"
                type, sub_tree = build_tree(property_schema, {children: []}, level + 1)
                puts "object level=#{level} after recursion, type=#{type} sub_tree=#{sub_tree}"
                child = { name: property_name + type }
                child[:children] = sub_tree unless sub_tree.empty?
                tree[:children] << child
            end
            puts "object level=#{level} about to return tree=#{tree}"
            tree
        when 'array'
            puts "in when array"
            case schema['items']
            when Array
                puts "in when Array for #{schema['items'].size} items"
                i     = 0
                items = []
                schema['items'].each do | a_hash |
                    puts "item #{i+=1} has #{a_hash.keys.size} keys :"
                    a_hash.keys.each{ | key | puts key }
                    # if the item has "type"=>"object" and "properties"=>{ ... }, then
                    # the whole item must be passed as argument to the next recursion
                    puts "level=#{level} about to recurs for item #{i}"
                    answer = build_tree(a_hash, {children: []}, level + 1)
                    puts "level=#{level} after recurs, answer=#{answer}"
                    items << { "item #{i}" => answer }
                end
                return ' (array)', items
            else
                puts "oops ! Array expected"
                "oops ! Array expected"
            end
        when 'string'
            puts "in when string, schema=#{schema}"
            return ' (string)', []
        else
            puts "in else"
            tree[:name] == schema
        end
    end
    
    build_tree(schema, tree, 1)
    puts 'final result :'
    puts tree
    

    执行:

    $ ruby -w t.rb 
    
    level=1 schema[:type]="object", schema class is Hash
    level=1 tree={"name"=>"200", :children=>[]}
    in when object for 1 properties :
    1. books
    object level=1, property_name=books
    
    level=2 schema[:type]="array", schema class is Hash
    level=2 tree={:children=>[]}
    in when array
    in when Array for 2 items
    item 1 has 2 keys :
    type
    properties
    level=2 about to recurs for item 1
    
    level=3 schema[:type]="object", schema class is Hash
    level=3 tree={:children=>[]}
    in when object for 2 properties :
    1. urn
    2. title
    object level=3, property_name=urn
    
    level=4 schema[:type]="string", schema class is Hash
    level=4 tree={:children=>[]}
    in when string, schema={"type"=>"string"}
    object level=3 after recursion, type= (string) sub_tree=[]
    object level=3, property_name=title
    
    level=4 schema[:type]="string", schema class is Hash
    level=4 tree={:children=>[]}
    in when string, schema={"type"=>"string"}
    object level=3 after recursion, type= (string) sub_tree=[]
    object level=3 about to return tree={:children=>[{:name=>"urn (string)"}, {:name=>"title (string)"}]}
    level=2 after recurs, answer={:children=>[{:name=>"urn (string)"}, {:name=>"title (string)"}]}
    item 2 has 2 keys :
    type
    properties
    level=2 about to recurs for item 2
    
    level=3 schema[:type]="object", schema class is Hash
    level=3 tree={:children=>[]}
    in when object for 2 properties :
    1. urn2
    2. title2
    object level=3, property_name=urn2
    
    level=4 schema[:type]="string", schema class is Hash
    level=4 tree={:children=>[]}
    in when string, schema={"type"=>"string"}
    object level=3 after recursion, type= (string) sub_tree=[]
    object level=3, property_name=title2
    
    level=4 schema[:type]="string", schema class is Hash
    level=4 tree={:children=>[]}
    in when string, schema={"type"=>"string"}
    object level=3 after recursion, type= (string) sub_tree=[]
    object level=3 about to return tree={:children=>[{:name=>"urn2 (string)"}, {:name=>"title2 (string)"}]}
    level=2 after recurs, answer={:children=>[{:name=>"urn2 (string)"}, {:name=>"title2 (string)"}]}
    object level=1 after recursion, type= (array) sub_tree=[{"item 1"=>{:children=>[{:name=>"urn (string)"}, {:name=>"title (string)"}]}}, {"item 2"=>{:children=>[{:name=>"urn2 (string)"}, {:name=>"title2 (string)"}]}}]
    object level=1 about to return tree={"name"=>"200", :children=>[{:name=>"books (array)", :children=>[{"item 1"=>{:children=>[{:name=>"urn (string)"}, {:name=>"title (string)"}]}}, {"item 2"=>{:children=>[{:name=>"urn2 (string)"}, {:name=>"title2 (string)"}]}}]}]}
    final result :
    {"name"=>"200", :children=>[{:name=>"books (array)", :children=>[{"item 1"=>{:children=>[{:name=>"urn (string)"}, {:name=>"title (string)"}]}}, {"item 2"=>{:children=>[{:name=>"urn2 (string)"}, {:name=>"title2 (string)"}]}}]}]}
    

    结果编辑:

    {"name"=>"200", 
     :children=>[
       {
         :name=>"books (array)",
         :children=>[
           {"item 1"=>{
             :children=>[
               {:name=>"urn (string)"}, 
               {:name=>"title (string)"}
             ]
            }
           },
           {"item 2"=>{
             :children=>[
               {:name=>"urn2 (string)"}, 
               {:name=>"title2 (string)"}
             ]
            }
           }
         ]
       }
     ]
    }
    

    【讨论】:

    • 亲爱的伯纳德,非常感谢您提供如此广泛的回答!我已经尝试了一些灵丹妙药并且非常喜欢它,尽管函数式编程似乎需要对处理代码的方式进行重大改变 - 我会给你的解决方案一个旋转,并让你知道它是怎么回事 - 再次感谢你详细说明您如何处理此类问题,我将来一定会应用您的策略!
    • 我注意到的一件事(尽管不幸的是我当时无法访问我的工作计算机)是当键被标记为数组时,您将“项目”设置为数组 - 这个有点奇怪,但是以 swagger 格式定义响应的方式实际上并没有使用这个数组结构——“items”下显示的哈希是响应返回的数组的一部分的项目的架构——但我确实希望我应该能够修改功能以适应这种情况!无论如何,再次感谢伯纳德。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-13
    • 1970-01-01
    • 2017-07-06
    • 2010-12-11
    • 1970-01-01
    相关资源
    最近更新 更多