No commit activity in last 3 years
No release in over 3 years
Useful when extending functionality with Rails engines.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 2.0

Runtime

>= 3.2.8
 Project Readme

ActiveSupport Decorators

Build Status

The decorator pattern is particularly useful when extending constants in rails engines or vice versa. To implement the decorator pattern, you need to load the decorator after the original file has been loaded. When you reference a class in a Rails application, ActiveSupport will only load the first file it finds that matches the class name. This means that you need to manually load the additional (decorator) file or eager load all of them on application startup. This is a tiny gem that provides you with a simple way to tell ActiveSupport to load your decorator files when needed.

Installation

Add it to your Gemfile and run bundle install:

gem 'activesupport-decorators', '~> 2.1'

Usage

Example 1 - Application extends engine (or any other) class.

Your engine defines a model called Pet (in my_engine/app/models/pet.rb):

class Pet < ActiveRecord::Base
end

Your application now wants to adds the concept of pet owners. You extend the Pet model in the application with the following model decorator (in app/models/pet_decorator.rb). Note that you could use 'Pet.class_eval do' instead of 'class Pet' if you want.

class Pet
  belongs_to :owner
end

Now tell ActiveSupportDecorators where to look for decorators, similar to setting Rails autoload paths. This will load a decorator file if it matches the original file's name/path and ends with '_decorator.rb'. In other words when the engine's app/pet.rb is loaded, it will load the main applications app/pet_decorator.rb.

ActiveSupportDecorators.paths << File.join(Rails.application.root, 'app/**')

Note that '**' is added to the end of the path because this is the pattern that Rails autoloads your app folder with. It means that every folder inside app is regarded as a autoload path instead of app/ itself, so that you can call your model 'Pet' and not 'Models::Pet'.

Example 2 - Engine extends application (or any other) class.

Similar to the example above except the initializer is placed in the engine instead of the main application. The example below will cause any matching decorator file in my_engine/app to load when a file is loaded from app/.

module MyEngine
  module Rails
    class Engine < ::Rails::Engine
      initializer :set_decorator_paths, :before => :load_environment_hook do |app|
        ActiveSupportDecorators.paths << File.join(config.root, 'app/**')
      end
    end
  end
end

Troubleshooting

  1. Enable debug output to see which decorators are loaded:
ActiveSupportDecorators.debug = true
  1. There is a standard ActiveSupport method available to see all load paths configured in your application. The file you are trying to decorate should be on one of these:
ActiveSupport::Dependencies.autoload_paths
  1. The relative path of the file you are trying to decorate in (2) should match the relative path of your decorator file to your configured decorator paths:
ActiveSupportDecorators.expanded_paths

Comparison to other gems

Other gems work by simply telling Rails to eager load all your decorators on application startup as seen [here] (https://github.com/atd/rails_engine_decorators/blob/master/lib/rails_engine_decorators/engine/configuration.rb) and here. They expect your decorators to use 'MyClass.class_eval do' to extend the original class as this is what triggers the original class to be loaded. Disadvantages of this approach include:

  • if you decorate two classes and one uses decorated functionality of the other, you have to make sure that it is not used during class loading since the other class might not be decorated yet.
  • development mode is a bit slower since eager loading decorators usually has a cascade effect on the application. This is more noticeable when using JRuby as it will be a compile action instead of class load action.
  • using 'MyClass.class_eval do' instead of 'class MyClass' means you can not define constants.

This gem works by hooking into ActiveSupport, which means that decorators are loaded as required instead of at application startup. You can use 'class MyClass' and expect that other classes are already decorated, since when you reference other classes they will be decorated on the fly when ActiveSupport loads them.