draper 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+ gem 'actionpack', "~> 3.0.9", :require => 'action_view'
3
+ gemspec
@@ -0,0 +1,5 @@
1
+ guard 'rspec', :version => 2, :notification => false do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,169 @@
1
+ Draper
2
+ ================
3
+
4
+ This gem makes it easy to apply the decorator pattern to the models in a Rails application.
5
+
6
+ ## Why use decorators?
7
+
8
+ Helpers, as they're commonly used, are a bit odd. In both Ruby and Rails we approach everything from an Object-Oriented perspective, then with helpers we get procedural.
9
+
10
+ The job of a helper is to take in data or a data object and output presentation-ready results. We can do that job in an OO fashion with a decorator.
11
+
12
+ In general, a decorator wraps an object with presentation-related accessor methods. For instance, if you had an `Article` object, then a decorator might add instance methods like `.formatted_published_at` or `.formatted_title` that output actual HTML.
13
+
14
+ ## How is it implemented?
15
+
16
+ To implement the pattern in Rails we can:
17
+
18
+ 1. Write a wrapper class with the decoration methods
19
+ 2. Wrap the data object
20
+ 3. Utilize those methods within our view layer
21
+
22
+ ## How do you utilize this gem in your application?
23
+
24
+ Here are the steps to utilizing this gem:
25
+
26
+ Add the dependency to your `Gemfile`:
27
+
28
+ ```
29
+ gem "draper"
30
+ ```
31
+
32
+ Run bundle:
33
+
34
+ ```
35
+ bundle
36
+ ```
37
+
38
+ Create a decorator for your model (ex: `Article`)
39
+
40
+ ```
41
+ rails generate draper:model Article
42
+ ```
43
+
44
+ Open the decorator model (ex: `app/decorators/article_decorator.rb`)
45
+
46
+ Add your new formatting methods as normal instance or class methods. You have access to the Rails helpers from the following classes:
47
+
48
+ ```
49
+ ActionView::Helpers::TagHelper
50
+ ActionView::Helpers::UrlHelper
51
+ ActionView::Helpers::TextHelper
52
+ ```
53
+
54
+ Use the new methods in your views like any other model method (ex: `@article.formatted_published_at`)
55
+
56
+ ## An Interface with Allows/Denies
57
+
58
+ A proper interface defines a contract between two objects. One purpose of the decorator pattern is to define an interface between your data model and the view template.
59
+
60
+ You are provided class methods `allows` and `denies` to control exactly which of the subject's methods are available. By default, *all* of the subject's methods can be accessed.
61
+
62
+ For example, say you want to prevent access to the `:title` method. You'd use `denies` like this:
63
+
64
+ ```ruby
65
+ class ArticleDecorator < Draper::Base
66
+ denies :title
67
+ end
68
+ ```
69
+
70
+ `denies` uses a blacklist approach. Note that, as of the current version, denying `:title` does not affect related methods like `:title=`, `:title?`, etc.
71
+
72
+ A better idea is a whitelist approach using `allows`:
73
+
74
+ ```ruby
75
+ class ArticleDecorator < Draper::Base
76
+ allows :title, :body, :author
77
+ end
78
+ ```
79
+
80
+ Now only those methods and any defined in the decorator class itself can be accessed directly.
81
+
82
+ ## Possible Decoration Methods
83
+
84
+ Here are some ideas of what you might do in decorator methods:
85
+
86
+ * Implement output formatting for `to_csv`, `to_json`, or `to_xml`
87
+ * Format dates and times using `strftime`
88
+ * Implement a commonly used representation of the data object like a `.name` method that combines `first_name` and `last_name` attributes
89
+
90
+ ## Example Using a Decorator
91
+
92
+ Say I have a publishing system with `Article` resources. My designer decides that whenever we print the `published_at` timestamp, it should be constructed like this:
93
+
94
+ ```html
95
+ <span class='published_at'>
96
+ <span class='date'>Monday, May 6</span>
97
+ <span class='time'>8:52AM</span>
98
+ </span>
99
+ ```
100
+
101
+ Could we build that using a partial? Yes. A helper? Uh-huh. But the point of the decorator is to encapsulate logic just like we would a method in our models. Here's how to implement it.
102
+
103
+ First, follow the steps above to add the dependency, update your bundle, then run the `rails generate decorator:setup` to prepare your app.
104
+
105
+ Since we're talking about the `Article` model we'll create an `ArticleDecorator` class. You could do it by hand, but use the provided generator:
106
+
107
+ ```
108
+ rails generate draper:model Article
109
+ ```
110
+
111
+ Now open up the created `app/decorators/article_decorator.rb` and you'll find an `ArticleDecorator` class. Add this method:
112
+
113
+ ```ruby
114
+ def formatted_published_at
115
+ date = content_tag(:span, published_at.strftime("%A, %B %e").squeeze(" "), :class => 'date')
116
+ time = content_tag(:span, published_at.strftime("%l:%M%p").delete(" "), :class => 'time')
117
+ content_tag :span, date + time, :class => 'published_at'
118
+ end
119
+ ```
120
+
121
+ *ASIDE*: Unfortunately, due to the current implementation of `content_tag`, you can't use the style of sending the content is as a block or you'll get an error about `undefined method 'output_buffer='`. Passing in the content as the second argument, as above, works fine.
122
+
123
+ Then you need to perform the wrapping in your controller. Here's the simplest method:
124
+
125
+ ```ruby
126
+ class ArticlesController < ApplicationController
127
+ def show
128
+ @article = ArticleDecorator.new( Article.find params[:id] )
129
+ end
130
+ end
131
+ ```
132
+
133
+ Then within your views you can utilize both the normal data methods and your new presentation methods:
134
+
135
+ ```ruby
136
+ <%= @article.formatted_published_at %>
137
+ ```
138
+
139
+ Ta-da! Object-oriented data formatting for your view layer. Below is the complete decorator with extra comments removed:
140
+
141
+ ```ruby
142
+ class ArticleDecorator < Draper::Base
143
+ def formatted_published_at
144
+ date = content_tag(:span, published_at.strftime("%A, %B %e").squeeze(" "), :class => 'date')
145
+ time = content_tag(:span, published_at.strftime("%l:%M%p"), :class => 'time').delete(" ")
146
+ content_tag :span, date + time, :class => 'created_at'
147
+ end
148
+ end
149
+ ```
150
+
151
+ ## Issues / Pending
152
+
153
+ * Test coverage for generators
154
+ * Ability to decorate multiple objects at once, ex: `ArticleDecorator.decorate(Article.all)`
155
+ * Revise readme to better explain interface pattern
156
+ * Build sample Rails application
157
+ * Consider: `ArticleDecorator.new(1)` does the equivalent of `ArticleDecorator.new(Article.find(1))`
158
+
159
+ ## License
160
+
161
+ (The MIT License)
162
+
163
+ Copyright © 2011 Jeff Casimir
164
+
165
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
166
+
167
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
168
+
169
+ THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "draper/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "draper"
7
+ s.version = Draper::VERSION
8
+ s.authors = ["Jeff Casimir"]
9
+ s.email = ["jeff@casimircreative.com"]
10
+ s.homepage = "http://github.com/jcasimir/draper"
11
+ s.summary = "Decorator pattern implmentation for Rails."
12
+ s.description = "Draper reimagines the role of helpers in the view layer of a Rails application, allowing an object-oriented approach rather than procedural."
13
+
14
+ s.rubyforge_project = "draper"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "rake", "0.8.7"
22
+ s.add_development_dependency "rspec", "~> 2.0.1"
23
+ s.add_development_dependency "activesupport", "~> 3.0.9"
24
+ s.add_development_dependency "actionpack", "~> 3.0.9"
25
+ s.add_development_dependency "ruby-debug19"
26
+ s.add_development_dependency "guard"
27
+ s.add_development_dependency "guard-rspec"
28
+ s.add_development_dependency "rb-fsevent"
29
+ end
@@ -0,0 +1,2 @@
1
+ require "draper/version"
2
+ require 'draper/base'
@@ -0,0 +1,46 @@
1
+ module Draper
2
+ class Base
3
+ include ActionView::Helpers::TagHelper
4
+ include ActionView::Helpers::UrlHelper
5
+ include ActionView::Helpers::TextHelper
6
+
7
+ require 'active_support/core_ext/class/attribute'
8
+ class_attribute :denied, :allowed
9
+ attr_accessor :source
10
+
11
+ DEFAULT_DENIED = Object.new.methods << :method_missing
12
+ self.denied = DEFAULT_DENIED
13
+
14
+ def self.denies(*input_denied)
15
+ raise ArgumentError, "Specify at least one method (as a symbol) to exclude when using denies" if input_denied.empty?
16
+ raise ArgumentError, "Use either 'allows' or 'denies', but not both." if self.allowed?
17
+ self.denied += input_denied
18
+ end
19
+
20
+ def self.allows(*input_allows)
21
+ raise ArgumentError, "Specify at least one method (as a symbol) to allow when using allows" if input_allows.empty?
22
+ #raise ArgumentError, "Use either 'allows' or 'denies', but not both." unless (self.denies == DEFAULT_EXCLUSIONS)
23
+ self.allowed = input_allows
24
+ end
25
+
26
+ def initialize(subject)
27
+ self.source = subject
28
+ build_methods
29
+ end
30
+
31
+ private
32
+ def select_methods
33
+ self.allowed || (source.public_methods - denied)
34
+ end
35
+
36
+ def build_methods
37
+ select_methods.each do |method|
38
+ (class << self; self; end).class_eval do
39
+ define_method method do |*args|
40
+ source.send method, *args
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ module Draper
2
+ VERSION = "0.3.1"
3
+ end
@@ -0,0 +1,7 @@
1
+ Description:
2
+ The draper:model generator creates a decorator model in /app/decorators.
3
+
4
+ Examples:
5
+ rails generate draper:model Article
6
+
7
+ file: app/decorators/article_decorator.rb
@@ -0,0 +1,10 @@
1
+ module Draper
2
+ class ModelGenerator < Rails::Generators::NamedBase
3
+ source_root File.expand_path('../templates', __FILE__)
4
+
5
+ def build_model
6
+ empty_directory "app/decorators"
7
+ template 'model.rb', "app/decorators/#{singular_name}_decorator.rb"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ class <%= singular_name.camelize %>Decorator < Draper::Base
2
+
3
+ # Rails Helpers
4
+ # Rails helpers like content_tag, link_to, and pluralize are already
5
+ # available to you. If you need access to other helpers, include them
6
+ # like this:
7
+ # include ActionView::Helpers::TextHelper
8
+ # Or pull in the whole kitchen sink:
9
+ # include ActionView::Helpers
10
+
11
+ # Wrapper Methods
12
+ # Control access to the wrapped subject's methods using one of the following:
13
+ #
14
+ # To allow _only_ the listed methods:
15
+ # allows :method1, :method2
16
+ #
17
+ # To allow everything _except_ the listed methods:
18
+ # denies :method1, :method2
19
+
20
+ # Presentation Methods
21
+ # Define presentation-related instance methods. Ex:
22
+ # def formatted_created_at
23
+ # content_tag :span, created_at.strftime("%A")
24
+ # end
25
+ end
@@ -0,0 +1,132 @@
1
+ require 'spec_helper'
2
+ require 'draper'
3
+
4
+ describe Draper::Base do
5
+ subject{ Draper::Base.new(source) }
6
+ let(:source){ "Sample String" }
7
+
8
+ it "should return the wrapped object when asked for source" do
9
+ subject.source.should == source
10
+ end
11
+
12
+ it "echos the methods of the wrapped class" do
13
+ source.methods.each do |method|
14
+ subject.should respond_to(method)
15
+ end
16
+ end
17
+
18
+ it "should not copy the .class, .inspect, or other existing methods" do
19
+ source.class.should_not == subject.class
20
+ source.inspect.should_not == subject.inspect
21
+ source.to_s.should_not == subject.to_s
22
+ end
23
+
24
+ describe "a sample usage with denies" do
25
+ before(:all) do
26
+ class DecoratorWithDenies < Draper::Base
27
+ denies :upcase
28
+
29
+ def sample_content
30
+ content_tag :span, "Hello, World!"
31
+ end
32
+
33
+ def sample_link
34
+ link_to "Hello", "/World"
35
+ end
36
+
37
+ def sample_truncate
38
+ ActionView::Helpers::TextHelper.truncate("Once upon a time", :length => 7)
39
+ end
40
+ end
41
+ end
42
+
43
+ let(:subject_with_denies){ DecoratorWithDenies.new(source) }
44
+
45
+ it "should not echo methods specified with denies" do
46
+ subject_with_denies.should_not respond_to(:upcase)
47
+ end
48
+
49
+ it "should not clobber other decorators' methods" do
50
+ subject.should respond_to(:upcase)
51
+ end
52
+
53
+ it "should be able to use the content_tag helper" do
54
+ subject_with_denies.sample_content.to_s.should == "<span>Hello, World!</span>"
55
+ end
56
+
57
+ it "should be able to use the link_to helper" do
58
+ subject_with_denies.sample_link.should == "<a href=\"/World\">Hello</a>"
59
+ end
60
+
61
+ it "should be able to use the pluralize helper" do
62
+ pending("Figure out odd interaction when the wrapped source object already has the text_helper methods (ie: a String)")
63
+ subject_with_denies.sample_truncate.should == "Once..."
64
+ end
65
+
66
+ it "should nullify method_missing to prevent AR from being cute" do
67
+ pending("How to test this without AR? Ugh.")
68
+ end
69
+ end
70
+
71
+ describe "a sample usage with allows" do
72
+ before(:all) do
73
+ class DecoratorWithAllows < Draper::Base
74
+ allows :upcase
75
+ end
76
+ end
77
+
78
+ let(:subject_with_allows){ DecoratorWithAllows.new(source) }
79
+
80
+ it "should echo the allowed method" do
81
+ subject_with_allows.should respond_to(:upcase)
82
+ end
83
+
84
+ it "should echo _only_ the allowed method" do
85
+ subject_with_allows.should_not respond_to(:downcase)
86
+ end
87
+ end
88
+
89
+ describe "invalid usages of allows and denies" do
90
+ let(:blank_allows){
91
+ class DecoratorWithInvalidAllows < Draper::Base
92
+ allows
93
+ end
94
+ }
95
+
96
+ let(:blank_denies){
97
+ class DecoratorWithInvalidDenies < Draper::Base
98
+ denies
99
+ end
100
+ }
101
+
102
+ let(:using_allows_then_denies){
103
+ class DecoratorWithInvalidMixing < Draper::Base
104
+ allows :upcase
105
+ denies :downcase
106
+ end
107
+ }
108
+
109
+ let(:using_denies_then_allows){
110
+ class DecoratorWithInvalidMixing < Draper::Base
111
+ denies :downcase
112
+ allows :upcase
113
+ end
114
+ }
115
+
116
+ it "should raise an exception for a blank allows" do
117
+ expect {blank_allows}.should raise_error(ArgumentError)
118
+ end
119
+
120
+ it "should raise an exception for a blank denies" do
121
+ expect {blank_denies}.should raise_error(ArgumentError)
122
+ end
123
+
124
+ it "should raise an exception for calling allows then denies" do
125
+ expect {using_allows_then_denies}.should raise_error(ArgumentError)
126
+ end
127
+
128
+ it "should raise an exception for calling denies then allows" do
129
+ expect {using_denies_then_allows}.should raise_error(ArgumentError)
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.require
4
+ require 'active_support'
5
+ require 'action_view'
6
+ require 'bundler/setup'
7
+ require 'draper'
8
+
9
+ Dir["spec/support/**/*.rb"].each do |file|
10
+ require "./" + file
11
+ end
12
+
13
+ RSpec.configure do |config|
14
+
15
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: draper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jeff Casimir
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-06-30 00:00:00.000000000 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rake
17
+ requirement: &2154360120 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - =
21
+ - !ruby/object:Gem::Version
22
+ version: 0.8.7
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: *2154360120
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ requirement: &2154359620 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 2.0.1
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: *2154359620
37
+ - !ruby/object:Gem::Dependency
38
+ name: activesupport
39
+ requirement: &2154359160 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ version: 3.0.9
45
+ type: :development
46
+ prerelease: false
47
+ version_requirements: *2154359160
48
+ - !ruby/object:Gem::Dependency
49
+ name: actionpack
50
+ requirement: &2154358700 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: 3.0.9
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: *2154358700
59
+ - !ruby/object:Gem::Dependency
60
+ name: ruby-debug19
61
+ requirement: &2154358320 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: *2154358320
70
+ - !ruby/object:Gem::Dependency
71
+ name: guard
72
+ requirement: &2154357860 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: *2154357860
81
+ - !ruby/object:Gem::Dependency
82
+ name: guard-rspec
83
+ requirement: &2154381220 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: *2154381220
92
+ - !ruby/object:Gem::Dependency
93
+ name: rb-fsevent
94
+ requirement: &2154380800 !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ type: :development
101
+ prerelease: false
102
+ version_requirements: *2154380800
103
+ description: Draper reimagines the role of helpers in the view layer of a Rails application,
104
+ allowing an object-oriented approach rather than procedural.
105
+ email:
106
+ - jeff@casimircreative.com
107
+ executables: []
108
+ extensions: []
109
+ extra_rdoc_files: []
110
+ files:
111
+ - .gitignore
112
+ - Gemfile
113
+ - Guardfile
114
+ - Rakefile
115
+ - Readme.markdown
116
+ - draper.gemspec
117
+ - lib/draper.rb
118
+ - lib/draper/base.rb
119
+ - lib/draper/version.rb
120
+ - lib/generators/draper/model/USAGE
121
+ - lib/generators/draper/model/model_generator.rb
122
+ - lib/generators/draper/model/templates/model.rb
123
+ - spec/base_spec.rb
124
+ - spec/spec_helper.rb
125
+ has_rdoc: true
126
+ homepage: http://github.com/jcasimir/draper
127
+ licenses: []
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ none: false
134
+ requirements:
135
+ - - ! '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ segments:
139
+ - 0
140
+ hash: 50059517492085377
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ none: false
143
+ requirements:
144
+ - - ! '>='
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ segments:
148
+ - 0
149
+ hash: 50059517492085377
150
+ requirements: []
151
+ rubyforge_project: draper
152
+ rubygems_version: 1.6.2
153
+ signing_key:
154
+ specification_version: 3
155
+ summary: Decorator pattern implmentation for Rails.
156
+ test_files:
157
+ - spec/base_spec.rb
158
+ - spec/spec_helper.rb