Fix ObjectTraverser when accessing array values

When attempting to access array values via the `json` resource:

```
describe json('/tmp/test.json') do
      its(['array',0]) { should eq "zero" }
end
```

... the resulting data would be an array of the size of the original array
with all the values replaced with nils:

```
     expected: "zero"
          got: [nil, nil, nil]
```

This was due to a bug in the ObjectTraverser mixin that mapped array values
back through `extract_value` rather than properly handling the passed-in
key(s). This worked fine for the specific data format created by the `csv`
resource but did not work `json` or any other resource that subclassed the
`JsonConfig` resource.

This change fixes the logic when dealing with an array when it's encountered,
and fixes up the `csv` resource with its own `value` method.

This change also adds tests for ObjectTraverser.

Signed-off-by: Adam Leff <adam@leff.co>
This commit is contained in:
Adam Leff 2017-03-14 11:33:54 -05:00 committed by Christoph Hartmann
parent 3d551f977f
commit 4f2b66302d
5 changed files with 110 additions and 7 deletions

View file

@ -15,19 +15,36 @@ module Inspec::Resources
end
"
# override file load and parse hash from csv
# override the parse method from JsonConfig
# Assuming a header row of name,col1,col2, it will output an array of hashes like so:
# [
# { 'name' => 'row1', 'col1' => 'value1', 'col2' => 'value2' },
# { 'name' => 'row2', 'col1' => 'value3', 'col2' => 'value4' }
# ]
def parse(content)
require 'csv'
# convert empty field to nil
CSV::Converters[:blank_to_nil] = lambda do |field|
field && field.empty? ? nil : field
end
# implicit conversion of values
csv = CSV.new(content, headers: true, converters: [:all, :blank_to_nil])
# convert to hash
csv.to_a.map(&:to_hash)
end
# override the value method from JsonConfig
# The format of the CSV hash as created by #parse is very different
# than what the YAML, JSON, and INI resources create, so using the
# #value method from JsonConfig (which uses ObjectTraverser.extract_value)
# doesn't make sense here.
def value(key)
@params.map { |x| x[key.first.to_s] }.compact
end
def to_s
"Csv #{@path}"
end

View file

@ -6,11 +6,12 @@ module ObjectTraverser
key = keys.shift
return nil if key.nil? || value.nil?
# if value is an array, iterate over each child
if value.is_a?(Array)
value = value.map { |i|
extract_value([key], i)
}
value = if key.is_a?(Fixnum)
value[key]
elsif value.respond_to?(key.to_sym)
value.send(key.to_sym)
end
else
value = value[key.to_s].nil? ? nil : value[key.to_s]
end

View file

@ -1,3 +1,4 @@
name,version,license,title,description
addressable,2.3.6,Apache 2.0,URI Implementation,"Addressable is a replacement for the URI implementation that is part of
Ruby's standard library. It more closely conforms to the relevant RFCs and
adds support for IRIs and URI templates."

1 addressable name 2.3.6 version Apache 2.0 license URI Implementation title Addressable is a replacement for the URI implementation that is part of Ruby's standard library. It more closely conforms to the relevant RFCs and adds support for IRIs and URI templates. description
1 name version license title description
2 addressable addressable 2.3.6 2.3.6 Apache 2.0 Apache 2.0 URI Implementation URI Implementation Addressable is a replacement for the URI implementation that is part of Ruby's standard library. It more closely conforms to the relevant RFCs and adds support for IRIs and URI templates. Addressable is a replacement for the URI implementation that is part of Ruby's standard library. It more closely conforms to the relevant RFCs and adds support for IRIs and URI templates.
3 ast ast 2.0.0 2.0.0 MIT MIT A library for working with Abstract Syntax Trees. A library for working with Abstract Syntax Trees. A library for working with Abstract Syntax Trees. A library for working with Abstract Syntax Trees.
4 astrolabe astrolabe 1.3.0 1.3.0 MIT MIT An object-oriented AST extension for Parser An object-oriented AST extension for Parser An object-oriented AST extension for Parser An object-oriented AST extension for Parser

View file

@ -17,7 +17,7 @@ describe 'Inspec::Resources::CSV' do
end
it 'gets all value lines' do
_(resource.params.length).must_equal 3
_(resource.params.length).must_equal 4
end
it 'captures a hashmap of entries of a line' do
@ -25,11 +25,15 @@ describe 'Inspec::Resources::CSV' do
end
it 'gets params by header fields' do
_(resource.params[0]['addressable']).must_equal 'ast'
_(resource.params[0]['name']).must_equal 'addressable'
end
it 'retrieves nil if a param is missing' do
_(resource.params[0]['missing']).must_be_nil
end
it 'returns an array of values by column name' do
_(resource.value(['name'])).must_equal([ 'addressable', 'ast', 'astrolabe', 'berkshelf' ])
end
end
end

View file

@ -0,0 +1,80 @@
# encoding: utf-8
# author: Adam Leff
require 'helper'
class Tester
include ObjectTraverser
end
describe ObjectTraverser do
let(:subject) { Tester.new }
let(:sample_data) do
{
'string1' => 'value1',
'string2' => 'value2',
'number1' => 2468,
'hash1' => { 'key1' => 'value1' },
'hash2' => {
'hash1string1' => 'value3',
'hash1number1' => 123,
'hash1subhash' => { 'key1' => 1, 'key2' => 2 },
},
'array1' => %w(word1 word2 word3),
'array2' => [
123,
456,
{ 'array1hashkey1' => 1, 'array1hashkey2' => 2 },
]
}
end
it 'returns values from the top-level' do
subject.extract_value(['string1'], sample_data).must_equal('value1')
subject.extract_value(['string2'], sample_data).must_equal('value2')
subject.extract_value(['number1'], sample_data).must_equal(2468)
end
it 'returns a full hash from the top-level' do
subject.extract_value(['hash1'], sample_data).must_equal({ 'key1' => 'value1' })
end
it 'returns values from a hash' do
subject.extract_value(['hash2', 'hash1string1'], sample_data).must_equal('value3')
subject.extract_value(['hash2', 'hash1number1'], sample_data).must_equal(123)
end
it 'returns values from a nested hash' do
subject.extract_value(['hash2', 'hash1subhash', 'key1'], sample_data).must_equal(1)
subject.extract_value(['hash2', 'hash1subhash', 'key2'], sample_data).must_equal(2)
end
it 'returns a full array from the top level' do
subject.extract_value(['array1'], sample_data).must_equal(%w(word1 word2 word3))
end
it 'returns values from the array using index numbers' do
subject.extract_value(['array1', 0], sample_data).must_equal('word1')
subject.extract_value(['array1', 1], sample_data).must_equal('word2')
subject.extract_value(['array1', 2], sample_data).must_equal('word3')
end
it 'returns values from the array using methods' do
subject.extract_value(['array1', 'first'], sample_data).must_equal('word1')
subject.extract_value(['array1', 'last'], sample_data).must_equal('word3')
end
it 'returns nil when fetching from an array when it does not match a method' do
subject.extract_value(['array1', 'not_a_valid_method'], sample_data).must_be_nil
end
it 'returns values from a nested hash within an array, accessing the array using numbers' do
subject.extract_value(['array2', 2, 'array1hashkey1'], sample_data).must_equal(1)
subject.extract_value(['array2', 2, 'array1hashkey2'], sample_data).must_equal(2)
end
it 'returns values from a nested hash within an array, accessing the array using methods' do
subject.extract_value(['array2', 'last', 'array1hashkey1'], sample_data).must_equal(1)
subject.extract_value(['array2', 'last', 'array1hashkey2'], sample_data).must_equal(2)
end
end