Recursion ! Try Elixir :-)
To trace a recursive method, I write plenty of puts and add a level number.
As I don't have Rails, I have removed the Rails stuff. With this slight modification, your input (where the array of books is not an array !) and your code :
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
the result is what you have obtained :
$ 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"}
}
}
}
]
}
(Note : I have manually pretty printed the resulting tree).
The trace shows what's going on : in when 'array' when you write schema['items'].each you probably want to iterate over several items. But there are no items, there is a single hash. So schema['items'].each becomes iterating over the keys. Then you recurs with a schema which has no 'type' key, hence case schema['type'] falls into when nil.
Note that if when 'object'had been recursively called instead of when nil, tree[:children] = [] would have erased previous results, because you use always the same initial tree. To stack intermediate results, you need to provide new variables in the recursive calls.
The best way to understand recursion is not to loop to the beginning of the method, but to imagine a cascade of calls :
method_1
|
+------> method_2
|
+------> method_3
If you pass the same initial parameter as argument to the recursive call, it is erased by the last returned value. But if you pass a new variable, you can use it in an accumulation operation.
If you had checked that schema['items']is really an array, as I do in my solution, you would have seen that the input does not correspond to the expectation :
$ 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"}]}
Now my solution. I leave cosmetic details up to you.
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
Execution :
$ 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)"}]}}]}]}
Result edited :
{"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)"}
]
}
}
]
}
]
}