diff --git a/lib/resources/csv.rb b/lib/resources/csv.rb index 7e7d6ec5c..d4416adf1 100644 --- a/lib/resources/csv.rb +++ b/lib/resources/csv.rb @@ -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 diff --git a/lib/utils/object_traversal.rb b/lib/utils/object_traversal.rb index 5984c6f32..b10574c93 100644 --- a/lib/utils/object_traversal.rb +++ b/lib/utils/object_traversal.rb @@ -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 diff --git a/test/unit/mock/files/example.csv b/test/unit/mock/files/example.csv index 91874603a..387f97971 100644 --- a/test/unit/mock/files/example.csv +++ b/test/unit/mock/files/example.csv @@ -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." diff --git a/test/unit/resources/csv_test.rb b/test/unit/resources/csv_test.rb index 9baffc790..20fe00d9e 100644 --- a/test/unit/resources/csv_test.rb +++ b/test/unit/resources/csv_test.rb @@ -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 diff --git a/test/unit/utils/object_traversal_test.rb b/test/unit/utils/object_traversal_test.rb new file mode 100644 index 000000000..00558e287 --- /dev/null +++ b/test/unit/utils/object_traversal_test.rb @@ -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