Skip to content

SimpleDelegator on steroids: automatic delegation, decorator class inference, etc.

License

Notifications You must be signed in to change notification settings

Alexander-Senko/magic-decorator

Repository files navigation

🪄 Magic Decorator

GitHub Actions Workflow Status Code Climate maintainability Code Climate coverage

A bit of history: this gem was inspired by digging deeper into Draper with an eye on a refactoring.

It implements a general decorator logic. It’s not meant to be a presenter.

Installation

Install the gem and add to the application's Gemfile by executing:

$ bundle add magic-decorator

If bundler is not being used to manage dependencies, install the gem by executing:

$ gem install magic-decorator

Usage

Magic::Decorator::Base is a basic decorator class to be inherited by any other decorator. It further inherits from SimpleDelegator and is straightforward like that.

class PersonDecorator < Magic::Decorator::Base
  def name = "#{first_name} #{last_name}"
end

Person = Struct.new :first_name, :last_name do
  include Magic::Decoratable
end

person = Person.new('John', 'Smith').decorate
person.name # => "John Smith"

Magic::Decoratable

This module adds three methods to decorate an object. Decorator class is being inferred automatically. When no decorator is found,

  • #decorate returns nil,
  • #decorate! raises Magic::Lookup::Error,
  • #decorated returns the original object.

One can test for the object is actually decorated with #decorated?.

'with no decorator for String'.decorated
    .decorated? # => false
['with a decorator for Array'].decorated
    .decorated? # => true

Extending decorator logic

When extending Magic::Decoratable, one may override #decorator_base to be used for lookup.

class Special::Decorator < Magic::Decorator::Base
  def self.name_for object_class
    "Special::#{object_class}Decorator"
  end
end

module Special::Decoratable
  include Magic::Decoratable

  private

  def decorator_base = Special::Decorator
end

class Special::Model
  include Special::Decoratable
end

Special::Model.new.decorate # looks for Special::Decorator descendants

🪄 Magic

Decoratable scope

Magic::Decoratable is mixed into Object by default. It means that effectively any object is magically decoratable.

One can use Magic::Decoratable.classes to see all the decoratable classes.

Decoration expansion

For almost any method called on a decorated object, both its result and yielded arguments get decorated.

'with no decorator for String'.decorated.chars
    .decorated? # => false
['with a decorator for Array'].decorated.map(&:chars).first.grep(/\S/).group_by(&:upcase).transform_values(&:size).sort_by(&:last).reverse.first(5).map(&:first)
    .decorated? # => true

Undecorated methods

Some methods aren’t meant to be decorated though:

  • deconstruct & deconstruct_keys for pattern matching,
  • converting methods: those starting with to_,
  • system methods: those starting with _.

undecorated modifier

Magic::Decorator::Base.undecorated can be used to exclude methods from being decorated automagically.

class MyDecorator < Magic::Decorator::Base
  undecorated %i[to_s inspect]
  undecorated :raw_method
  undecorated :m1, :m2
end

Decorator class inference

Decorators provide automatic class inference for any object based on its class name powered by Magic Lookup.

For example, MyNamespace::MyModel.new.decorate looks for MyNamespace::MyModelDecorator first. When missing, it further looks for decorators for its ancestor classes, up to ObjectDecorator.

Default decorators

EnumerableDecorator

It automagically decorates all its decoratable items.

[1, [2], { 3 => 4 }, '5'].decorated
    .map &:decorated? # => [false, true, true, false]

{ 1 => 2, [3] => [4] }.decorated.keys
    .map &:decorated? # => [false, true]
{ 1 => 2, [3] => [4] }.decorated.values
    .map &:decorated? # => [false, true]

{ 1 => 2, [3] => [4] }.decorated[1]
    .decorated? # => false
{ 1 => 2, [3] => [4] }.decorated[[3]]
    .decorated? # => true
Side effects for decorated collections
  • enables splat operator: *decorated ,
  • enables double-splat operator: **decorated,
  • enumerating methods yield decorated items.

Overriding the magic

When one needs more complicated behavior than the default one or feels like explicit is better than implicit.

Decorator class inference

One may override #decorator for any decoratable class, to be used instead of Magic Lookup.

  • That could be as straightforward as a constant:

     class Guest
       private
     
       def decorator = UserDecorator
     end
     
     guest.decorate # => instance of UserDecorator
  • Or, that could be virtually any logic:

     class User
       private
     
       def decorator = admin? ? AdminDecorator : super
     end
     
     user.decorate  # => instance of UserDecorator
     admin.decorate # => instance of AdminDecorator

Testing decorators

Testing a decorator is much like testing any other class.

To test whether an object is decorated one can use #decorated? method.

Note

A decorated object equals the original one (object.decorated == object). Thus, any existing tests shouldn’t break when the objects being tested get decorated.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/Alexander-Senko/magic-decorator. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Magic Decorator project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

About

SimpleDelegator on steroids: automatic delegation, decorator class inference, etc.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks