diff --git a/lib/rackstash/filter.rb b/lib/rackstash/filter.rb index 7098af8..f871e16 100644 --- a/lib/rackstash/filter.rb +++ b/lib/rackstash/filter.rb @@ -58,21 +58,51 @@ module Rackstash # we then create a filter object as before. When giving an object which # responds to `call` already (e.g. a `Proc`, we return it unchanged, # ignoring any additional passed `args`. + # @param only_if [#call, nil] An optional condition defining whether the + # filter should be applied, usually given as a `Proc` object. Before + # evaluating the newly created filter object, we first call the given + # proc with the event as its argument. The filter is applied only if the + # proc returns a truethy value. + # @param not_if [#call, nil] An optional condition defining whether the + # filter should not be applied, usually given as a `Proc` object. Before + # evaluating the newly created filter object, we first call the given + # proc with the event as its argument. The filter is not applied if the + # proc returns a truethy value. # @param args [Array] an optional list of arguments which is passed to the # initializer for the new filter object. + # @param kwargs [Hash] an optional list of keyword arguments which are + # passed to the initializer for the new filter object. # @raise [TypeError] if we can not create a new filter object from the # given `filter_spec`, usually because it is an unsupported type # @raise [KeyError] if we could not find a filter class in the registry # for the specified class name # @return [Object] a new filter object - def build(filter_spec, *args, &block) + def build(filter_spec, *args, only_if: nil, not_if: nil, **kwargs, &block) case filter_spec when ->(filter) { filter.respond_to?(:call) } filter_spec else - registry[filter_spec].new(*args, &block) + args << kwargs unless kwargs.empty? + + filter = registry[filter_spec].new(*args, &block) + conditional_filter(filter, only_if: only_if, not_if: not_if) end end + + private + + def conditional_filter(filter, only_if: nil, not_if: nil) + return filter if only_if.nil? && not_if.nil? + + conditional = Module.new do + define_method(:call) do |event| + return event if only_if && !only_if.call(event) + return event if not_if && not_if.call(event) + super(event) + end + end + filter.extend(conditional) + end end end end diff --git a/spec/rackstash/filter_spec.rb b/spec/rackstash/filter_spec.rb index de53c1b..a11557d 100644 --- a/spec/rackstash/filter_spec.rb +++ b/spec/rackstash/filter_spec.rb @@ -56,6 +56,83 @@ describe Rackstash::Filter do expect(described_class.build(filter, :ignored, 42)).to equal filter end + context 'with conditionals' do + let(:event) { Object.new } + + it 'applies the only_if conditional for new filters' do + only_if = -> {} + filter = described_class.build(filter_name, only_if: only_if) + + expect(only_if).to receive(:call).and_return false + expect { filter.call({}) }.not_to raise_error + end + + it 'applies the not_if conditional for new filters' do + not_if = -> {} + filter = described_class.build(filter_name, not_if: not_if) + + expect(not_if).to receive(:call).and_return true + expect(filter.call(event)).to equal event + end + + it 'applies both conditionals for new filters' do + only_if = -> {} + not_if = -> {} + + filter = described_class.build(filter_name, only_if: only_if, not_if: not_if) + + expect(only_if).to receive(:call).and_return true + expect(not_if).to receive(:call).and_return false + expect(filter.call(event)).to eql 'filtered' + end + + it 'keeps the class hierarchy unchanged' do + filter = described_class.build(filter_name, only_if: ->(event){ false }) + + expect(filter).to be_instance_of(filter_class) + end + + it 'ignores the conditional for existing filters' do + filter = filter_class.new + only_if = -> {} + + expect(described_class.build(filter, only_if: only_if)) + .to equal filter + + expect(only_if).not_to receive(:call) + expect(described_class.build(filter, only_if: only_if).call(event)) + .to eql 'filtered' + end + + it 'passes keyword arguments to the initializer' do + filter_class.class_eval do + def initialize(mandatory:) + @mandatory = mandatory + end + + attr_reader :mandatory + end + + filter = described_class.build(filter_name, only_if: ->{}, mandatory: 'foo') + expect(filter.mandatory).to eql 'foo' + end + end + + context 'without conditionals' do + it 'passes keyword arguments to the initializer' do + filter_class.class_eval do + def initialize(mandatory:) + @mandatory = mandatory + end + + attr_reader :mandatory + end + + filter = described_class.build(filter_name, mandatory: 'foo') + expect(filter.mandatory).to eql 'foo' + end + end + it 'raises a TypeError with invalid spec types' do expect { described_class.build(123) } .to raise_error(TypeError, '123 can not be used to describe filters')