Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add class decorator support #237

Merged
merged 11 commits into from
Feb 24, 2017
Merged

Add class decorator support #237

merged 11 commits into from
Feb 24, 2017

Conversation

himdel
Copy link
Contributor

@himdel himdel commented Jan 25, 2017

This makes it possible to run decorate not only on model instances, but also directly on the classes.

It also replaces Draper with a simpler solution.

Differences from Drapper:

  • there is only that decorate method, no attempt at implementing decorator_class? or any other Draper methods was made
  • that decorate method is also available on classes now
  • delegate_all is done automatically, but only for decorated instances, not much sense in using it for the decorated class
  • decorate returns nil when no decorator is found, instead of throwing

Usage:

VmServer.decorate will return VmOrTemplateDecorator
vm_instance.decorate will return an instance of VmOrTemplateDecorator which delegates to the original vm_instance object

adding

def self.fonticon
  "foobar"
end

to that decorator (note the self) will cause VmServer.decorate.fonticon to return "foobar".

Closes ManageIQ/manageiq#10313

Done in preparation for ManageIQ/manageiq#13199, cc @epwinchell

TODO::


Note that this does monkeypatch ApplicationRecord - adding a class and instance decorate method (and a @_decorator field to cache it once resolved).

To be able to decorate non-AR models, we'll need to extend and include the relevant mixin manually.

@himdel
Copy link
Contributor Author

himdel commented Jan 25, 2017

@hayesr, @martinpovolny you were active in the original discussion about class decorators => ping :)

@himdel
Copy link
Contributor Author

himdel commented Jan 25, 2017

Aand @skateman :)

@himdel
Copy link
Contributor Author

himdel commented Jan 25, 2017

So..

config/initializers/ is maybe not the best solution, but works (provided there is that < ActiveRecord::Base, otherwise it fails with NoMethodError trying to call connection).

Putting this in app/models/ causes Draper's collection decorator take precedence, which doesn't work :(.

So.. I guess we can either keep it here, or remove draper and reimplement the decorate method here as well.

EDIT: actually, putting it in (ui-classic) app/models means it never gets loaded

@himdel himdel added the wip label Jan 25, 2017
@himdel himdel changed the title Add class decorator support [WIP] Add class decorator support Jan 25, 2017
Copy link
Member

@chrisarcand chrisarcand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good idea, but I think we should do some changes as to the implementation.

def method_missing(*args)
@_decorated_instance.send(*args)
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would strongly suggest using an actual delegator rather than crossing your fingers with method_missing.

Are you familiar with some of Ruby's core Delegator classes? I would use SimpleDelegator here.

class MiqDecorator < SimpleDelegator

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 on using the SimpleDelegator

def decorate
self.class.decorator_for(self.class).new(self)
end
end
Copy link
Member

@chrisarcand chrisarcand Jan 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Follow this after addressing comment here)

Patching something that doesn't belong to you is a bad idea, especially re-opening a class in our lovely project 😄

Instead, you can do away with this entirely and create these methods in MiqDecorator itself. MiqDecorator can take in instances and klasses and return the proper decorator (which it owns) instance or class for you.

class MiqDecorator < SimpleDelegator
  self << class
    def decorator_for(klass)
       "#{klass.name}Decorator".safe_constantize
    end

    def decorate(instance)
      decorator = decorator_for(instance.class)
      decorator.new(instance)
    end
  end
end

Disclaimer: I just wrote this pseudo code up quick, a few details are plainly missing.

Copy link
Contributor

@hayesr hayesr Jan 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍
Yay for naming conventions in decorator_for

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Patching something that doesn't belong to you is a bad idea, especially re-opening a class in our lovely project 😄

I would take this a bit further and state that as you move to the REST API, you can't patch platform classes, so doing so here might make it harder to move forward. However, using the decoration pattern above, it is clear which methods are owned by the UI and not the platform, and those can be converted to code that eventually decorates the result of the REST API.

@@ -959,7 +959,7 @@ def listicon_item(view, id = nil)
# Return the icon classname for the list view icon of a db,id pair
# this always supersedes listicon_image if not nil
def listicon_icon(item)
item.decorate.try(:fonticon) if item.decorator_class?
item.decorate.try(:fonticon)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With my other comments addressed, this now becomes a non-patched call, entirely dependent on the UI's own code, with MiqDecorator.decorate(item). You of course can feel free to create a top level controller helper like decorate_model that does the same thing without MiqDecorator explicitly.

Copy link
Member

@kbrock kbrock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good. just 2 style ideas that can be ignored.

I like chris's idea of using a delegator

@@ -963,7 +963,7 @@

context "when item is not CIM or decorated" do
before(:each) do
allow(item).to receive(:decorator_class?) { false }
allow(item).to receive(:decorate) { nil }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could you use and_return?

allow(item).to receive(:decorate).and_return(nil)

class ApplicationRecord < ActiveRecord::Base
class << self
def decorate
@_decorator || (@_decorator = decorator_for(self))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another nit. I think this is more clear with ||=:

@_decorator ||= decorator_for(self)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(But you won't need this code given proper delegation, anyway)

@chrisarcand
Copy link
Member

Your comment about initializers here should be addressed/fixed given my feedback.

@hayesr
Copy link
Contributor

hayesr commented Jan 25, 2017

I think it might be possible to extend ApplicationRecord with an initializer block in engine.rb, but I think I like @chrisarcand's idea better. It feels a lot more object-oriented, and I bet we'll get some happy testing benefits as well.

@Fryguy
Copy link
Member

Fryguy commented Jan 25, 2017

Putting this in app/models/ causes Draper's collection decorator take precedence, which doesn't work :(.

So.. I guess we can either keep it here, or remove draper and reimplement the decorate method here as well.

It sounds like you want to remove Draper, so in that case, I would say remove draper.

@chrisarcand
Copy link
Member

Indeed, Draper should be replaced given where we are now.

Let me be clear that your implementation is understandable given that you were basically implementing how Draper does it. But Draper, with the UI split having been done, now doesn't make any sense for us at all because of how it patches things in much the same way. The UI doesn't really own it's models that it's trying to decorate anymore.

@miq-bot
Copy link
Member

miq-bot commented Jan 26, 2017

This pull request is not mergeable. Please rebase and repush.

@martinpovolny
Copy link
Member

But Draper, with the UI split having been done, now doesn't make any sense for us at all because of how it patches things in much the same way. The UI doesn't really own it's models that it's trying to decorate anymore.

Looking forward that's true. But for now it's not that much imporant. We need to be able to decorate to be able to do cleanups and move forward.

As @Fryguy wrote, we might be decorating stuff returned from the API in the future. But for now we need the decorators for both instances and classes of the models.

@martinpovolny martinpovolny self-assigned this Jan 27, 2017
@miq-bot
Copy link
Member

miq-bot commented Feb 17, 2017

This pull request is not mergeable. Please rebase and repush.

This makes it possible to run `decorate` not only on model instances, but also directly on the classes.

Differences from Drapper:
  * there is only that `decorate` method, no attempt at implementing `decorator_class?` or any other Draper methods was made
  * `delegate_all` works out of the box (meaning `Host.decorate.foo` calls `Host.foo` when the decorator does not implement that method, and does have the `delegate_all` call)
  * `decorate` returns nil when no decorator is found, instead of throwing

Usage:

`VmServer.decorate` will return `VmOrTemplateDecorator`

adding

```
def self.fonticon
  "foobar"
end
```

to that decorator (note the `self`) will cause `VmServer.decorate.fonticon` to return `"foobar"`.
pretty much just expects an instance passed to the constructor, so it can delegate everything to it
…qDecorator.for

so that MiqDecorator.for(Klass) returns KlassDecorator, if it exists
now that decorate doesn't throw but returns nil instead
@himdel
Copy link
Contributor Author

himdel commented Feb 23, 2017

So... I've rebased and addressed most of the concerns, including moving the class resolution logic to MiqDecorator, rebased, etc.

But... do we really want to write MiqDecorate.decorate_instance(instance) and MiqDecorate.decorate_class(VmOrTemplate) instead of instance.decorate and VmOrTemplate.decorate? To me, that sounds like the exact opposite of right :). (Also, harder to cache :).)

@miq-bot
Copy link
Member

miq-bot commented Feb 24, 2017

Checked commits https://github.com/himdel/manageiq-ui-classic/compare/de3561070a93f4dd69f140bcc29a8e75805728bc~...5c1fec047e408741352f64d5fbdb0f164bbaacce with ruby 2.2.6, rubocop 0.47.1, and haml-lint 0.20.0
48 files checked, 3 offenses detected

app/decorators/miq_event_definition_decorator.rb

spec/lib/miq_decorator_spec.rb

@himdel himdel changed the title [WIP] Add class decorator support Add class decorator support Feb 24, 2017
@himdel
Copy link
Contributor Author

himdel commented Feb 24, 2017

Tested this does not break the API, so we do not depend on ManageIQ/manageiq#14040.
Tested that removing Draper can be done after, so we do not depend on ManageIQ/manageiq#14044 either.

@miq-bot miq-bot removed the wip label Feb 24, 2017
@himdel
Copy link
Contributor Author

himdel commented Feb 24, 2017

... doing a resume of a gitter discussion - ☝️ February 24, 2017 3:18 PM

We will use .decorate for now, to keep the code same, and not have to depend on the API changes,
but as step 2, we will stop patching ApplicationRecord and do MiqDecorator.for(instance) and MiqDecorator.for(klass) instead.

@chrisarcand
Copy link
Member

👍 Please note for anyone having an issue with the MiqDecorator.for(object) (which I think is great, personally...) You can use whatever helper method you want as long as it's within the scope of the UI project and doesn't directly edit ApplicationRecord or any other core class that doesn't belong to the UI (as we are doing above, simply to keep the .decorate syntax working). This is important for continued decoupling of the core and UI projects, making our lives betterer. Thanks. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Decorators + icons by class, not instance
7 participants