diff --git a/CHANGELOG.md b/CHANGELOG.md index cbdd3f70..fd3ae4d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Added - Support for Ruby 3.4. + - Added per-filter option validation. Each filter type now validates its own specific options and raises `ArgumentError` when passed unrecognized options. ## Fixed diff --git a/lib/active_interaction/filter.rb b/lib/active_interaction/filter.rb index 0e49bb57..f0dbd921 100644 --- a/lib/active_interaction/filter.rb +++ b/lib/active_interaction/filter.rb @@ -51,6 +51,13 @@ def factory(slug) CLASSES.fetch(slug) { raise MissingFilterError, slug.inspect } end + # Returns the list of allowed options for this filter type. + # + # @return [Array] + def allowed_options + %i[desc default] + end + private # @param slug [Symbol] @@ -66,6 +73,7 @@ def register(slug) # # @option options [Object] :default Fallback value to use when given `nil`. def initialize(name, options = {}, &block) + validate_options!(options) @name = name @options = options.dup @filters = {} @@ -229,6 +237,12 @@ def describe(value) "(Object doesn't support #inspect)" end + def validate_options!(options) + if (invalid_options = options.keys - self.class.allowed_options).any? + raise ArgumentError, "invalid options: #{invalid_options.join(', ')}" + end + end + def raw_default(context) value = options.fetch(:default) return value unless value.is_a?(Proc) diff --git a/lib/active_interaction/filters/abstract_date_time_filter.rb b/lib/active_interaction/filters/abstract_date_time_filter.rb index 5a678d58..20550018 100644 --- a/lib/active_interaction/filters/abstract_date_time_filter.rb +++ b/lib/active_interaction/filters/abstract_date_time_filter.rb @@ -8,6 +8,10 @@ module ActiveInteraction # # @private class AbstractDateTimeFilter < Filter + def self.allowed_options + super + %i[format] + end + def database_column_type self.class.slug end diff --git a/lib/active_interaction/filters/array_filter.rb b/lib/active_interaction/filters/array_filter.rb index 3c0d08f1..5a26c6b4 100644 --- a/lib/active_interaction/filters/array_filter.rb +++ b/lib/active_interaction/filters/array_filter.rb @@ -38,6 +38,10 @@ class ArrayFilter < Filter register :array + def self.allowed_options + super + %i[index_errors] + end + def process(value, context) input = super diff --git a/lib/active_interaction/filters/decimal_filter.rb b/lib/active_interaction/filters/decimal_filter.rb index a3f41ac9..bef2bf4e 100644 --- a/lib/active_interaction/filters/decimal_filter.rb +++ b/lib/active_interaction/filters/decimal_filter.rb @@ -19,6 +19,10 @@ class Base # rubocop:disable Lint/EmptyClass class DecimalFilter < AbstractNumericFilter register :decimal + def self.allowed_options + super + %i[digits] + end + private def digits diff --git a/lib/active_interaction/filters/hash_filter.rb b/lib/active_interaction/filters/hash_filter.rb index 5819ff6c..d59a24a5 100644 --- a/lib/active_interaction/filters/hash_filter.rb +++ b/lib/active_interaction/filters/hash_filter.rb @@ -25,6 +25,10 @@ class HashFilter < Filter register :hash + def self.allowed_options + super + %i[strip] + end + def process(value, context) # rubocop:disable Metrics/AbcSize input = super diff --git a/lib/active_interaction/filters/integer_filter.rb b/lib/active_interaction/filters/integer_filter.rb index 6f145097..4b7aabda 100644 --- a/lib/active_interaction/filters/integer_filter.rb +++ b/lib/active_interaction/filters/integer_filter.rb @@ -20,6 +20,10 @@ class Base # rubocop:disable Lint/EmptyClass class IntegerFilter < AbstractNumericFilter register :integer + def self.allowed_options + super + %i[base] + end + private def base diff --git a/lib/active_interaction/filters/interface_filter.rb b/lib/active_interaction/filters/interface_filter.rb index f8e9c3f3..dbfbbd77 100644 --- a/lib/active_interaction/filters/interface_filter.rb +++ b/lib/active_interaction/filters/interface_filter.rb @@ -28,6 +28,10 @@ class Base # rubocop:disable Lint/EmptyClass class InterfaceFilter < Filter register :interface + def self.allowed_options + super + %i[from methods] + end + def initialize(name, options = {}, &block) if options.key?(:methods) && options.key?(:from) raise InvalidFilterError, diff --git a/lib/active_interaction/filters/object_filter.rb b/lib/active_interaction/filters/object_filter.rb index cc6cbe7d..8cf04e56 100644 --- a/lib/active_interaction/filters/object_filter.rb +++ b/lib/active_interaction/filters/object_filter.rb @@ -28,6 +28,10 @@ class Base # rubocop:disable Lint/EmptyClass class ObjectFilter < Filter register :object + def self.allowed_options + super + %i[class converter] + end + private def klass diff --git a/lib/active_interaction/filters/record_filter.rb b/lib/active_interaction/filters/record_filter.rb index 35abe813..271ba202 100644 --- a/lib/active_interaction/filters/record_filter.rb +++ b/lib/active_interaction/filters/record_filter.rb @@ -30,6 +30,10 @@ class Base # rubocop:disable Lint/EmptyClass class RecordFilter < Filter register :record + def self.allowed_options + super + %i[class finder] + end + private def klass diff --git a/lib/active_interaction/filters/string_filter.rb b/lib/active_interaction/filters/string_filter.rb index 49a6209d..cc426cf2 100644 --- a/lib/active_interaction/filters/string_filter.rb +++ b/lib/active_interaction/filters/string_filter.rb @@ -20,6 +20,10 @@ class Base # rubocop:disable Lint/EmptyClass class StringFilter < Filter register :string + def self.allowed_options + super + %i[strip] + end + private def strip? diff --git a/spec/active_interaction/base_spec.rb b/spec/active_interaction/base_spec.rb index ab5b2e1a..e1343cea 100644 --- a/spec/active_interaction/base_spec.rb +++ b/spec/active_interaction/base_spec.rb @@ -140,6 +140,42 @@ def execute end.to raise_error NoMethodError end + it 'raises an error for a non-existent kwarg' do + expect do + Class.new(TestInteraction) do + string :beverage, type: :sparkling_wine, bubbles: true + end + end.to raise_error ArgumentError, /invalid options: type, bubbles/ + end + + it 'allows extending allowed_options for custom filters' do + Class.new(ActiveInteraction::Filter) do + register :custom + + def self.allowed_options + super + %i[custom_option] + end + + private + + def matches?(value) + value.is_a?(String) + end + end + + extended_class = Class.new(TestInteraction) do + custom :test_field, custom_option: true + end + + expect { extended_class.new }.not_to raise_error + end + + it 'filters validate their own options' do + expect(ActiveInteraction::StringFilter.allowed_options).to include(:desc, :default, :strip) + expect(ActiveInteraction::IntegerFilter.allowed_options).to include(:desc, :default, :base) + expect(ActiveInteraction::Filter.allowed_options).to eq(%i[desc default]) + end + it do expect do Class.new(TestInteraction) do diff --git a/spec/active_interaction/errors_spec.rb b/spec/active_interaction/errors_spec.rb index 74f602df..bb5b5b7f 100644 --- a/spec/active_interaction/errors_spec.rb +++ b/spec/active_interaction/errors_spec.rb @@ -11,8 +11,8 @@ let(:klass) do Class.new(ActiveInteraction::Base) do - string :attribute, defualt: nil - array :array, defualt: nil + string :attribute, default: nil + array :array, default: nil def self.name @name ||= SecureRandom.hex