Skip to content

Commit

Permalink
expand the interface filter to include checking for ancestors
Browse files Browse the repository at this point in the history
  • Loading branch information
AaronLasseigne committed Jan 10, 2021
1 parent 4e6edb2 commit 3361f01
Show file tree
Hide file tree
Showing 7 changed files with 503 additions and 26 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
- [#392][] - Integer parsing now defaults the base to 10. ([how to upgrade](#integer-parsing-base-now-10))
- The `inputs` method now returns an `ActiveInteraction::Input` instead of a
hash. The `ActiveInteraction::Input` still responds to all hash methods.
- The `interface` filter will now look for an ancestor of the value passed
based on the name of the interface or the value passed in the `from` option.

## Added

Expand Down
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,45 @@ hash :stuff,

### Interface

Interface filters allow you to specify that an object must respond to a certain
set of methods. This allows you to do duck typing with interactions.
Interface filters allow you to specify an interface that the passed value must
meet in order to pass. The name of the interface is used to look for a constant
inside the ancestor listing for the passed value. This allows for a variety of
checks depending on what's passed. Class instances are checked for an included
module or an inherited ancestor class. Classes are checked for an extended
module or an inherited ancestor class. Modules are checked for an extended
module.

``` rb
class InterfaceInteraction < ActiveInteraction::Base
interface :exception

def execute
exception
end
end

InterfaceInteraction.run!(exception: Exception)
# ActiveInteraction::InvalidInteractionError: Exception is not a valid interface
InterfaceInteraction.run!(exception: NameError) # a subclass of Exception
# => NameError
```

You can use `:from` to specify a class or module. This would be the equivalent
of what's above.

```rb
class InterfaceInteraction < ActiveInteraction::Base
interface :error,
from: Exception

def execute
error
end
end
```

You can also create an anonymous interface on the fly by passing the `methods`
option.

``` rb
class InterfaceInteraction < ActiveInteraction::Base
Expand Down
5 changes: 5 additions & 0 deletions lib/active_interaction/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ module ActiveInteraction # rubocop:disable Style/Documentation
# @return [Class]
InvalidClassError = Class.new(Error)

# Raised if an ancestor name is invalid.
#
# @return [Class]
InvalidAncestorError = Class.new(Error)

# Raised if a converter is invalid.
#
# @return [Class]
Expand Down
8 changes: 5 additions & 3 deletions lib/active_interaction/filters/file_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Base
end

# @private
class FileFilter < InterfaceFilter
class FileFilter < Filter
register :file

def database_column_type
Expand All @@ -24,8 +24,10 @@ def database_column_type

private

def methods
[:rewind]
def matches?(object)
object.respond_to?(:rewind)
rescue NoMethodError
false
end
end
end
57 changes: 51 additions & 6 deletions lib/active_interaction/filters/interface_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ module ActiveInteraction
class Base
# @!method self.interface(*attributes, options = {})
# Creates accessors for the attributes and ensures that values passed to
# the attributes implement an interface.
# the attributes implement an interface. An interface can be based on a
# set of methods or the existance of a class or module in the ancestors
# of the passed value.
#
# @!macro filter_method_params
# @option options [Array<String,Symbol>] :methods ([]) the methods that
# @option options [Constant, String, Symbol] :from (use the attribute
# name) The class or module representing the interface to check for.
# @option options [Array<String, Symbol>] :methods ([]) the methods that
# objects conforming to this interface should respond to
#
# @example
# interface :anything
# interface :concern
# @example
# interface :person,
# from: Manageable
# @example
# interface :serializer,
# methods: %i[dump load]
Expand All @@ -21,14 +28,52 @@ class Base
class InterfaceFilter < Filter
register :interface

def initialize(name, options = {}, &block)
if options.key?(:methods) && options.key?(:from)
raise InvalidFilterError,
'method and from options cannot both be passed'
end

super
end

private

def from
const_name = options.fetch(:from, name).to_s.camelize
Object.const_get(const_name)
rescue NameError
raise InvalidAncestorError,
"constant #{const_name.inspect} does not exist"
end

def matches?(object)
methods.all? { |method| object.respond_to?(method) }
return matches_methods?(object) if options.key?(:methods)

const = from
if checking_class_inheritance?(object, const)
class_inherits_from?(object, const)
else
singleton_ancestor?(object, const)
end
rescue NoMethodError
false
end

def matches_methods?(object)
options.fetch(:methods, []).all? { |method| object.respond_to?(method) }
end

def checking_class_inheritance?(object, from)
object.is_a?(Class) && from.is_a?(Class)
end

def class_inherits_from?(klass, inherits_from)
klass != inherits_from && klass.ancestors.include?(inherits_from)
end

def methods
options.fetch(:methods, [])
def singleton_ancestor?(object, from)
object.class != from && object.singleton_class.ancestors.include?(from)
end
end
end
Loading

0 comments on commit 3361f01

Please sign in to comment.