Skip to content

Commit

Permalink
When attempting to access array values via the json resource:
Browse files Browse the repository at this point in the history
```
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>
  • Loading branch information
adamleff committed Mar 14, 2017
1 parent 546486f commit 8dea5bb
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 7 deletions.
19 changes: 18 additions & 1 deletion lib/resources/csv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,36 @@ class CsvConfig < JsonConfig
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
Expand Down
9 changes: 5 additions & 4 deletions lib/utils/object_traversal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ def extract_value(keys, value)
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
Expand Down
1 change: 1 addition & 0 deletions test/unit/mock/files/example.csv
Original file line number Diff line number Diff line change
@@ -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."
Expand Down
8 changes: 6 additions & 2 deletions test/unit/resources/csv_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,23 @@
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
_(resource.params[0]).must_be_kind_of Hash
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
80 changes: 80 additions & 0 deletions test/unit/utils/object_traversal_test.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 8dea5bb

Please sign in to comment.