Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/alexander-senko/magic-decorator

SimpleDelegator on steroids: automatic delegation, decorator class inference, etc.
https://github.com/alexander-senko/magic-decorator

decorators ruby

Last synced: 3 months ago
JSON representation

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

Awesome Lists containing this project

README

        

# 🪄 Magic Decorator

![GitHub Actions Workflow Status](
https://img.shields.io/github/actions/workflow/status/Alexander-Senko/magic-decorator/ci.yml
)
![Code Climate maintainability](
https://img.shields.io/codeclimate/maintainability-percentage/Alexander-Senko/magic-decorator
)
![Code Climate coverage](
https://img.shields.io/codeclimate/coverage/Alexander-Senko/magic-decorator
)

A bit of history:
this gem was inspired by digging deeper into [Draper](https://github.com/drapergem/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`](
https://docs.ruby-lang.org/en/master/SimpleDelegator.html
) and is straightforward like that.

```ruby
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?`.

```ruby
'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.

```ruby
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 `yield`ed arguments get decorated.

```ruby
'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.

```ruby
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](
https://github.com/Alexander-Senko/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.

```ruby
[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_](
https://peps.python.org/pep-0020/#the-zen-of-python
).

### 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:

```ruby
class Guest
private

def decorator = UserDecorator
end

guest.decorate # => instance of UserDecorator
```

- Or, that could be virtually any logic:

```ruby
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](https://github.com/Alexander-Senko/magic-decorator/blob/main/CODE_OF_CONDUCT.md).

## License

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

## 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](https://github.com/Alexander-Senko/magic-decorator/blob/main/CODE_OF_CONDUCT.md).