rails-decorators 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d2b058e294fde7539190070d9b3136d605cae188
4
+ data.tar.gz: 68cb945a3eaf62cb046404b26fa55d500e5fcdbc
5
+ SHA512:
6
+ metadata.gz: cce41bd1af784df612dfb8c1f45236b4331e29d67b5cf4c012126f925c842674de2cb797e52282185037e5ab06785ece88c8e957cfc6ce12b5aaa7155121b0d6
7
+ data.tar.gz: 5604cad44131f137b6baa2c96964237da3be1d310089a292bd962268100728ca46d282d4ea101cf6917eed78e2adcf8b54d646d0c7da017cfef702d6e572f6f4
@@ -0,0 +1,20 @@
1
+ Copyright 2017 Ben Crouse
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,130 @@
1
+ # Rails::Decorators
2
+ Using [Rails engines](http://guides.rubyonrails.org/engines.html) extensively, we needed a way to customize behavior for implementations of the platform. Rails provides a great mechanism for views and assets, but doesn't offer any functionality to handle controller/model/lib overriding/customizing. The suggested mechanism in the documentation is `class_eval` or ActiveSupport::Concern. Our preferred method for this is module prepending, which is cleaner than `class_eval` in that it avoids alias method chaining and becomes part of the ancestor chain.
3
+
4
+ We also want junior developers to be able to jump in and be productive ASAP - not necessarily requiring they learn the techniques of `class_eval` to fix a bug or get a simple task done. Yes, we educate and promote this as the developer grows, but we want the barriers to contribution as low as possible.
5
+
6
+ We like the [`ActiveSupport::Concern`](http://api.rubyonrails.org/classes/ActiveSupport/Concern.html) API, so this library mimics the API of `ActiveSupport::Concern` and layers on top some niceties for the specific task of customizing a Rails engine. Also, we don't like having to store decorated behavior in a separate directory (like https://github.com/parndt/decorators). Storing decorations in the same place as new classes makes it much easier to see the overall picture of how the engine has been customized/extended in one shot.
7
+
8
+ ## Usage
9
+ Say we have a class in an engine like so:
10
+ ```ruby
11
+ # in ecommerce/app/models/ecommerce/product.rb
12
+ module Ecommerce
13
+ class Product < ApplicationRecord
14
+ def price
15
+ skus.first.price
16
+ end
17
+ end
18
+ end
19
+ ```
20
+
21
+ `Rails::Decorators` allows customizing this class like so:
22
+ ```ruby
23
+ # in Rails.root/app/models/ecommerce/product.decorator
24
+ module Ecommerce
25
+ decorate Product do
26
+ decorated do
27
+ # Class methods may be called here.
28
+ attr_writer :on_sale
29
+ end
30
+
31
+ class_methods do
32
+ # Methods defined here extend/decorate static methods on the
33
+ # decorated class.
34
+ def sorts
35
+ super + ['discount_rate']
36
+ end
37
+ end
38
+
39
+ # Methods defined here extend/decorate instance methods on the
40
+ # decorated class.
41
+ def price
42
+ result = super
43
+ result *= discount_rate if on_sale?
44
+ result
45
+ end
46
+ end
47
+ end
48
+ ```
49
+
50
+ `Rails::Decorators` achieves this in a manner similar to `ActiveSupport::Concern` - it dynamically creates a module out of the block passed, and then prepends that into the original class. The above is equivalent to (note that this would need to be required using `require_dependency` as well):
51
+ ```ruby
52
+ # in Rails.root/app/models/ecommerce/product_decorator.rb
53
+ module Ecommerce
54
+ class Product
55
+ module Decorator
56
+ module ClassMethods
57
+ # Methods defined here extend/decorate static methods on the
58
+ # decorated class.
59
+ def sorts
60
+ super + ['discount_rate']
61
+ end
62
+ end
63
+
64
+ # Methods defined here extend/decorate instance methods on the
65
+ # decorated class.
66
+ def price
67
+ result = super
68
+ result *= discount_rate if on_sale?
69
+ result
70
+ end
71
+ end
72
+ end
73
+
74
+ Product.send(:prepend, Product::Decorator)
75
+ Product.singleton_class.send(:prepend, Product::Decorator::ClassMethods)
76
+ Product.class_eval do
77
+ # Class methods may be called here.
78
+ attr_writer :on_sale
79
+ end
80
+ end
81
+ ```
82
+
83
+ You can also decorate more than one class at a time:
84
+ ```ruby
85
+ # in Rails.root/app/models/ecommerce/navigable.decorator
86
+ module Ecommerce
87
+ decorate Product, Category, Page do
88
+ def generate_slug
89
+ # my custom logic here
90
+ end
91
+ end
92
+ end
93
+ ```
94
+
95
+ Other engines may want to namespace their customizations so as not to collide with further customizations:
96
+ ```ruby
97
+ # in ecommerce_blog/app/models/ecommerce/product.decorator
98
+ module Ecommerce
99
+ decorate Product, with: 'blog' do
100
+ def blog_entries
101
+ BlogEntry.for_product(id)
102
+ end
103
+ end
104
+ end
105
+ ```
106
+
107
+ It is strongly suggested you update your editor of choice to use Ruby syntax highlighting on \*.decorator files.
108
+
109
+ ## Installation
110
+ Add this line to your application's Gemfile:
111
+
112
+ ```ruby
113
+ gem 'rails-decorators'
114
+ ```
115
+
116
+ And then execute:
117
+ ```bash
118
+ $ bundle
119
+ ```
120
+
121
+ Or install it yourself as:
122
+ ```bash
123
+ $ gem install rails-decorators
124
+ ```
125
+
126
+ ## Contributing
127
+ Contribution directions go here.
128
+
129
+ ## License
130
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,37 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Rails::Decorators'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+ load 'rails/tasks/statistics.rake'
22
+
23
+
24
+
25
+ require 'bundler/gem_tasks'
26
+
27
+ require 'rake/testtask'
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'lib'
31
+ t.libs << 'test'
32
+ t.pattern = 'test/**/*_test.rb'
33
+ t.verbose = false
34
+ end
35
+
36
+
37
+ task default: :test
@@ -0,0 +1,14 @@
1
+ module Rails
2
+ module Decorators
3
+ mattr_accessor :extension
4
+ self.extension = :decorator
5
+ end
6
+ end
7
+
8
+ require 'rails/decorators/engine'
9
+ require 'rails/decorators/version'
10
+ require 'rails/decorators/application'
11
+ require 'rails/decorators/active_support'
12
+ require 'rails/decorators/decorator'
13
+ require 'rails/decorators/invalid_decorator'
14
+ require 'rails/decorators/object'
@@ -0,0 +1,52 @@
1
+ module ActiveSupport
2
+ module Dependencies
3
+ module Loadable
4
+ private
5
+
6
+ # Monkey patched because Ruby's `require` doesn't load files that don't
7
+ # end in `.rb`, so we have to use `load` for decorators.
8
+ #
9
+ # The difference in semantics between `require` and `load` won't really
10
+ # matter since Rails handles this in development and code doesn't get
11
+ # reloaded in other environments.
12
+ #
13
+ # This module is mixed in to Object.
14
+ #
15
+ def require(file)
16
+ file_string = file.to_s
17
+
18
+ if file_string.end_with?(".#{Rails::Decorators.extension}")
19
+ load(file_string)
20
+ else
21
+ super
22
+ end
23
+ end
24
+ end
25
+
26
+ # Monkey patched because Rails appends a `.rb` to all files it tries to
27
+ # load with `require_dependency`. It does this for a good reason: both the
28
+ # oob `require` and oob `load` provided by Ruby will accept a path ending
29
+ # with `.rb`. But in our case, we want to ensure that decorators are loaded
30
+ # with `load` since `require` doesn't accept files that don't end with
31
+ # `.rb`.
32
+ #
33
+ def self.load_file(path, const_paths = loadable_constants_for_path(path))
34
+ path = path.gsub(/\.rb\z/, '') if path.end_with?(".#{Rails::Decorators.extension}.rb")
35
+
36
+ #
37
+ # Below this copy/pasted from Rails :(
38
+ #
39
+ const_paths = [const_paths].compact unless const_paths.is_a? Array
40
+ parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || ::Object }
41
+
42
+ result = nil
43
+ newly_defined_paths = new_constants_in(*parent_paths) do
44
+ result = Kernel.load path
45
+ end
46
+
47
+ autoloaded_constants.concat newly_defined_paths unless load_once_path?(path)
48
+ autoloaded_constants.uniq!
49
+ result
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,17 @@
1
+ module Rails
2
+ class Application < Engine
3
+ module Decorators
4
+ def watchable_args
5
+ result = super
6
+
7
+ result.last.keys.each do |path|
8
+ result.last[path] << Rails::Decorators.extension
9
+ end
10
+
11
+ result
12
+ end
13
+ end
14
+
15
+ prepend Decorators
16
+ end
17
+ end
@@ -0,0 +1,116 @@
1
+ module Rails
2
+ module Decorators
3
+ module Decorator
4
+ def self.loader(roots)
5
+ Proc.new do
6
+ roots.each do |root|
7
+ decorators = Dir.glob("#{root}/app/**/*.#{Rails::Decorators.extension}")
8
+ decorators.sort!
9
+ decorators.each { |d| require_dependency(d) }
10
+ end
11
+ end
12
+ end
13
+
14
+ def self.decorate(*targets, &module_definition)
15
+ options = targets.extract_options!
16
+
17
+ targets.each do |target|
18
+ unless target.is_a?(Class)
19
+ raise(
20
+ InvalidDecorator,
21
+ <<-eos.strip_heredoc
22
+
23
+ Problem:
24
+ You cannot decorate a Module
25
+ Summary:
26
+ Decoration only works with classes. Decorating modules requires
27
+ managing load order, a problem that is very complicated and
28
+ beyond the scope of this system.
29
+ Resolution:
30
+ Decorate multiple classes that include the module like so:
31
+ `decorate Catalog::Product, Content::Page do`
32
+ eos
33
+ )
34
+ end
35
+
36
+ if target.name.to_s.end_with?('Helper')
37
+ raise(
38
+ InvalidDecorator,
39
+ <<-eos.strip_heredoc
40
+
41
+ Problem:
42
+ Rails::Decorators doesn't work with helpers.
43
+ Summary:
44
+ Rails does some magic with helpers which in certain cases
45
+ causes decoration to not work.
46
+ Resolution:
47
+ Create a new helper and in a `to_prepare` block, use
48
+ ActionPack's `helper` method to include the helper, e.g.
49
+ `MyEngine::ApplicationController.helper(MyEngine::BlogsHelper)`
50
+ eos
51
+ )
52
+ end
53
+
54
+ decorator_name = "#{options[:with].to_s.camelize}#{target.to_s.demodulize}Decorator"
55
+
56
+ if target.const_defined?(decorator_name)
57
+ # We are probably reloading in Rails development env if this happens
58
+ next if !Rails.application.config.cache_classes
59
+
60
+ raise(
61
+ InvalidDecorator,
62
+ <<-eos.strip_heredoc
63
+
64
+ Problem:
65
+ #{decorator_name} is already defined in #{target.name}.
66
+ Summary:
67
+ When decorating a class, Rails::Decorators dynamically defines
68
+ a module for prepending the decorations passed in the block. In
69
+ this case, the name for the decoration module is already defined
70
+ in the namespace, so decorating would redefine the constant.
71
+ Resolution:
72
+ Please specify a unique `with` option when decorating #{target.name}.
73
+ eos
74
+ )
75
+ end
76
+
77
+ mod = Module.new do
78
+ extend Rails::Decorators::Decorator
79
+ module_eval(&module_definition)
80
+ end
81
+
82
+ target.const_set(decorator_name, mod)
83
+ mod.decorates(target)
84
+ end
85
+ end
86
+
87
+ def prepend_features(base)
88
+ super
89
+
90
+ if const_defined?(:ClassMethodsDecorator)
91
+ base
92
+ .singleton_class
93
+ .send(:prepend, const_get(:ClassMethodsDecorator))
94
+ end
95
+
96
+ if instance_variable_defined?(:@_decorated_block)
97
+ base.class_eval(&@_decorated_block)
98
+ end
99
+ end
100
+
101
+ def decorated(&block)
102
+ instance_variable_set(:@_decorated_block, block)
103
+ end
104
+
105
+ def class_methods(&class_methods_module_definition)
106
+ mod = const_set(:ClassMethodsDecorator, Module.new)
107
+ mod.module_eval(&class_methods_module_definition)
108
+ end
109
+ alias_method :module_methods, :class_methods
110
+
111
+ def decorates(klass)
112
+ klass.send(:prepend, self)
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,18 @@
1
+ module Rails
2
+ module Decorators
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace Rails::Decorators
5
+
6
+ config.before_initialize do |app|
7
+ engines = ObjectSpace
8
+ .each_object(Class)
9
+ .select { |klass| klass < ::Rails::Engine }
10
+ .select { |klass| klass.name.present? }
11
+ .select { |klass| klass != ::Rails::Application }
12
+
13
+ loader = Decorator.loader(engines.map(&:root) + [Rails.root])
14
+ app.config.to_prepare(&loader)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ module Rails
2
+ module Decorators
3
+ class InvalidDecorator < RuntimeError; end
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ class Object
2
+ delegate :decorate, to: Rails::Decorators::Decorator
3
+ end
@@ -0,0 +1,5 @@
1
+ module Rails
2
+ module Decorators
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-decorators
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ben Crouse
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-04-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 5.0.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.0.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email:
43
+ - bencrouse@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - MIT-LICENSE
49
+ - README.md
50
+ - Rakefile
51
+ - lib/rails/decorators.rb
52
+ - lib/rails/decorators/active_support.rb
53
+ - lib/rails/decorators/application.rb
54
+ - lib/rails/decorators/decorator.rb
55
+ - lib/rails/decorators/engine.rb
56
+ - lib/rails/decorators/invalid_decorator.rb
57
+ - lib/rails/decorators/object.rb
58
+ - lib/rails/decorators/version.rb
59
+ homepage: https://github.com/weblinc/rails-decorators
60
+ licenses:
61
+ - MIT
62
+ metadata: {}
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 2.5.2
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Rails::Decorators provides a clean, familiar API for decorating the behavior
83
+ of a Rails engine.
84
+ test_files: []