diff --git a/lib/resources/xinetd.rb b/lib/resources/xinetd.rb index b9b3f3adc..401e3a8fb 100644 --- a/lib/resources/xinetd.rb +++ b/lib/resources/xinetd.rb @@ -23,7 +23,6 @@ module Inspec::Resources def initialize(conf_path = '/etc/xinetd.conf') @conf_path = conf_path - @filters = '' @contents = {} end @@ -35,28 +34,15 @@ module Inspec::Resources @params ||= read_params end - extend Inspec::Filter - add_filter 'service' - add_filter 'id' - add_filter 'socket_type' - add_filter 'type' - add_filter 'wait' - - def disabled? - where({ 'disable' => 'no' }).services.empty? - end - - def enabled? - where({ 'disable' => 'yes' }).services.empty? - end - - def where(conditions = {}) - fields, filters = Inspec::Filter.where(service_lines, conditions) - res = clone - res.instance_variable_set(:@filters, @filters + filters) - res.instance_variable_set(:@services, fields) - res - end + filter = FilterTable.create(self, :service_lines) + filter.add_delegator(:where) + .add(:services, field: 'service') + .add(:ids, field: 'id') + .add(:socket_types, field: 'socket_type') + .add(:types, field: 'type') + .add(:wait, field: 'wait') + .add(:disabled?) { |x| x.where('disable' => 'no').services.empty? } + .add(:enabled?) { |x| x.where('disable' => 'yes').services.empty? } private @@ -97,13 +83,5 @@ module Inspec::Resources def service_lines @services ||= params['services'].values.flatten.map(&:params) end - - def get_fields(*fields) - res = service_lines.map do |line| - fields.map { |f| line[f] } - end.flatten - return res unless fields == ['service'] - res.uniq - end end end diff --git a/lib/utils/filter.rb b/lib/utils/filter.rb index 78fdb680f..d60a3fa58 100644 --- a/lib/utils/filter.rb +++ b/lib/utils/filter.rb @@ -1,34 +1,45 @@ # encoding: utf-8 # author: Dominik Richter +# author: Stephan Renatus # author: Christoph Hartmann -module Inspec - module Filter - module Show; end +module FilterTable + module Show; end - def add_filter(field_name) - fail "Called add_filter in resource #{self} with field name nil!" if field_name.nil? - method_name = field_name.to_s - # methods will alwas target plurals; this is why the suffix 's' is mandatory - method_name += 's' unless method_name.end_with? 's' - - define_method method_name.to_sym do |condition = Show| - return get_fields(field_name) if condition == Show - where({ field_name => condition }) - end + class Table + attr_reader :params + def initialize(resource, params, filters) + @resource = resource + @params = params + @filters = filters end - def self.where(table, conditions = {}) + def where(conditions) return self if !conditions.is_a?(Hash) || conditions.empty? filters = '' + table = @params conditions.each do |field, condition| filters += " #{field} = #{condition.inspect}" table = filter_lines(table, field, condition) end - [table, filters] + self.class.new(@resource, table, @filters + filters) end - def self.filter_lines(table, field, condition) + def get_fields(*fields) + @params.map do |line| + fields.map { |f| line[f] } + end.flatten + end + + def to_s + @resource.to_s + @filters + end + + alias inspect to_s + + private + + def filter_lines(table, field, condition) table.find_all do |line| next unless line.key?(field) case line[field] @@ -40,4 +51,48 @@ module Inspec end end end + + class Factory + def initialize(resource, accessor) + @resource = resource + @accessor = accessor + @table = Class.new(Table) + end + + def table(instance) + table.new(self, instance.method(table_accessor).call, '') + end + + def add_delegator(method_name) + if method_name.nil? + throw RuntimeError, "Called filter.add_delegator for resource #{@resource} with method name nil!" + end + table_accessor = @accessor + table = @table + @resource.send(:define_method, method_name.to_sym) do |*args| + filter = table.new(self, method(table_accessor).call, '') + filter.method(method_name.to_sym).call(*args) + end + self + end + + def add(method_name, opts = {}, &block) + if method_name.nil? + throw RuntimeError, "Called filter.add for resource #{@resource} with method name nil!" + end + + field_name = opts[:field] || method_name + @table.send(:define_method, method_name.to_sym) do |condition = Show| + return block.call(self) unless block.nil? # rubocop:disable Performance/RedundantBlockCall + return where(nil).get_fields(field_name) if condition == Show + where({ field_name => condition }) + end + + add_delegator(method_name) + end + end + + def self.create(resource, accessor) + Factory.new(resource, accessor) + end end diff --git a/test/unit/resources/xinetd_test.rb b/test/unit/resources/xinetd_test.rb index 8afea9d15..e17a82adc 100644 --- a/test/unit/resources/xinetd_test.rb +++ b/test/unit/resources/xinetd_test.rb @@ -17,7 +17,7 @@ describe 'Inspec::Resources::XinetdConf' do describe 'with services from child configs' do it 'has one service name' do - _(resource.services).must_equal %w{chargen} + _(resource.services.uniq).must_equal %w{chargen} end it 'has multiple service definitions' do @@ -30,7 +30,7 @@ describe 'Inspec::Resources::XinetdConf' do it 'can chain filters' do one = resource.services('chargen').socket_types('dgram') - _(one.params['services'].length).must_equal 1 + _(one.services.length).must_equal 1 _(one.ids).must_equal %w{chargen-dgram} end diff --git a/test/unit/utils/filter_table_test.rb b/test/unit/utils/filter_table_test.rb new file mode 100644 index 000000000..4454a657a --- /dev/null +++ b/test/unit/utils/filter_table_test.rb @@ -0,0 +1,102 @@ +# encoding: utf-8 +# author: Dominik Richter +# author: Stephan Renatus +# author: Christoph Hartmann + +require 'helper' + +describe FilterTable do + let (:data) {[ + { foo: 3, bar: true, baz: 'yay', num: nil }, + { foo: 2, bar: false, baz: 'noo', num: 1 }, + { foo: 2, bar: false, baz: 'whatever', num: 2 }, + ]} + + let (:resource) { + Class.new do + attr_reader :data + def initialize(data) + @data = data + end + end + } + + let (:factory) { FilterTable.create(resource, :data) } + let (:instance) { resource.new(data) } + + it 'has a create utility which creates a filter factory' do + factory.must_be_kind_of FilterTable::Factory + end + + describe 'when calling add_delegator' do + it 'is chainable' do + factory.add_delegator(:sth).must_equal factory + end + + it 'wont add nil' do + proc { factory.add_delegator(nil) }.must_throw RuntimeError + end + + it 'can expose the where method' do + factory.add_delegator(:where) + _(instance.respond_to?(:where)).must_equal true + instance.where({ baz: 'yay' }).params.must_equal [data[0]] + end + + it 'will delegate even non-existing methods' do + factory.add_delegator(:not_here) + _(instance.respond_to?(:not_here)).must_equal true + end + end + + describe 'when calling add' do + it 'is chainable' do + factory.add(:sth).must_equal factory + end + + it 'wont add nil' do + proc { factory.add(nil) }.must_throw RuntimeError + end + + it 'can expose a data column' do + factory.add(:baz) + instance.baz(123).must_be_kind_of(FilterTable::Table) + end + end + + describe 'with the number field' do + before { factory.add(:num) } + + it 'filter by nil' do + instance.num(nil).params.must_equal [data[0]] + end + + it 'filter by existing numbers' do + instance.num(1).params.must_equal [data[1]] + end + + it 'filter by missing number' do + instance.num(-1).params.must_equal [] + end + end + + describe 'with the string field' do + before { factory.add(:baz) } + + it 'filter by existing strings' do + instance.baz('yay').params.must_equal [data[0]] + end + + it 'filter by missing string' do + instance.baz('num').params.must_equal [] + end + + it 'filter by existing regex' do + instance.baz(/A/i).params.must_equal [data[0], data[2]] + end + + it 'filter by missing regex' do + instance.baz(/zzz/).params.must_equal [] + end + end +end