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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +130 -0
- data/Rakefile +37 -0
- data/lib/rails/decorators.rb +14 -0
- data/lib/rails/decorators/active_support.rb +52 -0
- data/lib/rails/decorators/application.rb +17 -0
- data/lib/rails/decorators/decorator.rb +116 -0
- data/lib/rails/decorators/engine.rb +18 -0
- data/lib/rails/decorators/invalid_decorator.rb +5 -0
- data/lib/rails/decorators/object.rb +3 -0
- data/lib/rails/decorators/version.rb +5 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|
data/Rakefile
ADDED
@@ -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
|
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: []
|