I have the following swagger (openAPI) definition of a response schema:
h = { "type"=>"object",
"properties"=>{
"books"=>{
"type"=>"array",
"items"=>{
"type"=>"object",
"properties"=>{
"urn" =>{ "type"=>"string" },
"title"=>{ "type"=>"string" }
}
}
}
}
}
And would like to transform this into the following format, so as to be able to display this response as a tree:
{ "name"=>"200",
"children"=> [
{
"name"=>"books (array)",
"children"=> [
{"name"=>"urn (string)" },
{"name"=>"title (string)" }
]
}
]
}
In the swagger schema format, a node can either be an object (with properties), or an array of items, which are themselves objects. Here is the function I have written: the schema parameter is the hash in the swagger format shown above, and the tree variable contains {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
Unfortunately I think I'm making an error somewhere, as this returns the following Hash:
{ :name=>"200",
:children=>[
{ :name=>"type", :children=>false },
{ :name=>"properties", :children=>false },
{ :name=>"books",
:children=>{
"type"=>"object",
"properties"=>{
"urn"=>{"type"=>"string"},
"title"=>{"type"=>"string"}
}
}
}
]
}
I must be missing a step in the recursion or passing the tree in the wrong way, but I'm afraid I don't have enough brainpower to figure it out :) maybe a kind soul with a gift for writing beautiful ruby code will give me a hand!
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 :
the result is what you have obtained :
(Note : I have manually pretty printed the resulting tree).
The trace shows what's going on : in
when 'array'
when you writeschema['items'].each
you probably want to iterate over several items. But there are no items, there is a single hash. Soschema['items'].each
becomes iterating over the keys. Then you recurs with a schema which has no'type'
key, hencecase schema['type']
falls intowhen nil
.Note that if
when 'object'
had been recursively called instead ofwhen nil
,tree[:children] = []
would have erased previous results, because you use always the same initialtree
. 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 :
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 :Now my solution. I leave cosmetic details up to you.
Execution :
Result edited :
So array of items is not an array of items but an array of properties of the sub-schema. This is the new solution to take this fact into account :
Result edited (tested with ruby 2.3.3p222) :
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 likecontext 'when a type object has no properties' do let(:schema) { {"type" => "object", "xyz" => ...
would help adding code to check pre-conditions. I'm also lazy using RSpec for small scripts, but for serious development, I make the effort because I have recognized the benefits. The time spent in debugging is lost forever, the time invested in specs gives security in case of changes and a nice legible indented report about what the code does or does not. I recommend the brand new Rspec 3 book.
One more word on accessing hashes : if you have a mix of strings and symbols, it's a source of problems.
will not find the element if the internal keys are not of the same type as the external data. Choose a type, for example symbol, when you create the hash, and systematically convert access variables to symbol :
or all to strings :
The solution suggested by @BernardK is impressive in its length, but I could not get it to work. Here's my more humble solution. I wrapped it in a class so I could test it properly.
One problem with your code is that in a couple of places, you are returning
tree[:name] == schema
, which evaluates tofalse
. I think you meant to assigntree[:name] = schema
and then returntree
.Like @BernardK, I assumed that a schema of type 'array' would have, as its value, an array of things. If that's not how it works, then could you please provide an example where 'array' is something more than an additional layer around an 'object'?
Hopefully between this answer and the other, you can make something out of it that works for you.
Here is the spec file: