Ruby中的递归哈希转换函数

3

我有以下swagger(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格式中的哈希,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代码的好心人能帮助我!

3个回答

1

递归!尝试使用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 nil,则tree[: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)"}
         ]
        }
       }
     ]
   }
 ]
}

亲爱的Bernard,非常感谢您如此详尽的回答!我已经尝试过一点Elixir,并且非常喜欢它,虽然函数式编程似乎需要改变人们处理代码的方式 - 我将尝试您的解决方案,并告诉您结果 - 再次感谢您详细说明了如何处理这些问题,我一定会在未来应用您的策略! - Dizzy Pazzy
我注意到的一件事(不幸的是那时我没有办法使用我的工作电脑)是你将“items”标记为数组,但实际上在swagger格式中定义响应时并不使用这种数组结构。在“items”下显示的哈希是响应返回的数组中将包含的项目的模式,这有点奇怪,但我希望我能够修改函数以适应这种情况!无论如何,再次感谢Bernard。 - Dizzy Pazzy

1
@BernardK提出的解决方案非常长,但我无法使其正常工作。这是我更加谦逊的解决方案。我将其封装在一个类中,以便可以正确测试它。
你的代码有一个问题,在几个地方,你返回了tree[:name] == schema,它评估为false。我认为你的意思是分配tree[:name] = schema,然后返回tree
像@BernardK一样,我假设类型为“array”的模式将具有作为其值的事物数组。如果不是这样工作的,请提供一个示例,其中“array”不仅仅是围绕“object”的另一层。
希望在这个答案和其他答案之间,你能够得到对你有用的东西。
# 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

Here is the spec file:

# /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,感谢您的详细回答 - 我没有访问包含数组响应的其他swagger定义,但我将确保在明天第一时间使其可用 - 再次感谢您抽出时间查看我的问题并为其编写规范! - Dizzy Pazzy

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)"}
      ]
    }
  ]
}

Don't take it as brilliant code. I write Ruby code every earthquake of magnitude 12. The purpose was to explain what didn't work in your code and draw attention on using new variables (now an empty Hash) in the recursive call. There are plenty of cases that should be tested and raise an error.
The right way is BDD as did @moveson : first write RSpec tests for all cases, especially edge cases, then write the code. I know it gives the feeling to be too slow, but in the long term it pays and replaces the debugging and printing of traces.
More on tests
This code is brittle : for example if a type key is not associated with a properties key, it will fail at schema['properties'].each : undefined method 'each' for nil:NilClass. A spec like 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]...

如果内部键与外部数据的类型不同,则无法找到元素。在创建哈希时选择一个类型(例如符号symbol),并将访问变量系统地转换为该类型。
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]...

嗨Bernard,非常感谢你更新了你的答案 - 当我写问题时应该更清楚;从Google告诉我的来看,12级地震并不经常发生,所以我更感谢你抽出时间重新审视我的问题。BDD确实可能是正确的方法,我只能责怪自己的懒惰没有更严格地采用这种方法。正如你所写的,好处大于坏处。我已经尝试使用你的第一个答案来解决它,但到目前为止我仍然感到智力上被压倒了 - 再次感谢你的认真! - Dizzy Pazzy
@DizzyPazzy编辑了when 'array' ... when Hash中的注释,并在末尾添加了一个更多关于测试的说明的注释。 - BernardK

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接