resubject 0.0.1 → 0.0.2

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.
data/Gemfile CHANGED
@@ -1,4 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ gem 'rails', '~> 3.2.10'
4
+
3
5
  # Specify your gem's dependencies in resubject.gemspec
4
6
  gemspec
data/IDEAS.md ADDED
@@ -0,0 +1,51 @@
1
+ Present a object with a symbol
2
+
3
+ present user, context: :public
4
+ # => present user, PublicUserPresenter
5
+
6
+ present user, :public, :crazy
7
+ # => present user, PublicPresenter, CrazyPresenter
8
+
9
+ # best way to handle this?
10
+ present user, :public, :user
11
+ # => present user, PublicPresenter, UserPresenter
12
+
13
+ Helper class methods
14
+
15
+ class UserPresenter < Resubject::Presenter
16
+ # formats to default (to_s), only if present
17
+ # object.created_at
18
+ timestamp :created_at
19
+
20
+ # formats to long (to_s(:long))
21
+ timestamp :created_at, format: :long
22
+
23
+ # multiple
24
+ timestamps :created_at, :updated_at
25
+
26
+ autolink!
27
+ # create object.link
28
+
29
+ # currency format
30
+ currency :price
31
+
32
+ # change options
33
+ currency :price, format: 'whatever'
34
+ end
35
+
36
+ Test helpers
37
+
38
+ describe UserPresenter do
39
+ it 'has currency' do
40
+ user = stub price: 200.0
41
+ presented(user).currency.should == '$200.00'
42
+ end
43
+
44
+ # or
45
+
46
+ subject { UserPresenter.new(stub(price: 200.0)) }
47
+
48
+ it 'has currency' do
49
+ presented.currency.should == '$200'
50
+ end
51
+ end
data/README.md CHANGED
@@ -10,13 +10,17 @@ Uber simple presenters using Ruby's SimpleDelegator.
10
10
  Add this line to your application's Gemfile:
11
11
 
12
12
  ```ruby
13
- gem 'resubject', '~> 0.0.1'
13
+ gem 'resubject', '~> 0.0.2'
14
14
  ```
15
15
 
16
16
  And then execute:
17
17
 
18
18
  $ bundle
19
19
 
20
+ ## Documentation
21
+
22
+ Checkout the documentation in [rdoc.info/resubject](http://rdoc.info/github/felipeelias/resubject/master/frames)
23
+
20
24
  ## Usage
21
25
 
22
26
  Resubject works on top of `SimpleDelegator` which simply delegates every method call to the delegated object. Example:
@@ -37,7 +41,7 @@ Then use the delegator:
37
41
  ```ruby
38
42
  box = Box.new('Awkward Package', ['platypus', 'sloth', 'anteater'])
39
43
 
40
- presentable = BoxPresenter.new(box, nil)
44
+ presentable = BoxPresenter.new(box)
41
45
  presentable.contents
42
46
  # => platypus, sloth, anteater
43
47
  ```
@@ -84,6 +88,56 @@ Or if you prefer, you can use the `#present` method directly into your views
84
88
  <%= present(@box).contents %>
85
89
  ```
86
90
 
91
+ ## Helpers
92
+
93
+ You can define presentable attributes:
94
+
95
+ ```ruby
96
+ class PostPresenter < Resubject::Presenter
97
+ presents :title
98
+ presents :comments # or => presents :comments, CommentPresenter
99
+ end
100
+ ```
101
+
102
+ Then the attributes will return an instance of those presenters:
103
+
104
+ ```ruby
105
+ post.title
106
+ # => <TitlePresenter>
107
+
108
+ post.comments
109
+ # => [<CommentPresenter>, <CommentPresenter>, <CommentPresenter>]
110
+ ```
111
+
112
+ Or if you wish, you can use the `present` method inside your class
113
+
114
+ ```ruby
115
+ class PostPresenter < Resubject::Presenter
116
+ def comments
117
+ present(to_model.comments, SomePresenter)
118
+ end
119
+ end
120
+ ```
121
+
122
+ ### Helpers on Rails
123
+
124
+ `Resubject` can generate some rails helpers for your attributes:
125
+
126
+ ```ruby
127
+ class ProductPresenter < Resubject::Presenter
128
+ currency :price, precision: 2
129
+ end
130
+ ```
131
+
132
+ Will generate:
133
+
134
+ ```ruby
135
+ product.price
136
+ # => $10.00
137
+ ```
138
+
139
+ Check out [the extensions file](https://github.com/felipeelias/resubject/blob/master/lib/resubject/rails/extensions.rb) for other attribute helpers.
140
+
87
141
  ## Maintainers
88
142
 
89
143
  - Felipe Elias Philipp - [coderwall.com/felipeelias](http://coderwall.com/felipeelias)
@@ -0,0 +1,65 @@
1
+ module Resubject
2
+ module Builder
3
+ class InvalidPresenterArgument < StandardError #:nodoc:
4
+ end
5
+
6
+ # Presents a object or a collection of objects
7
+ #
8
+ # Examples:
9
+ #
10
+ # Builder.present box, template
11
+ # => <BoxPresenter>
12
+ #
13
+ # # Using a custom presenter
14
+ # Builder.present box, template, CustomPresenter
15
+ # => <CustomPresenter>
16
+ #
17
+ # # Using multiple presenters
18
+ # Builder.present box, template, OnePresenter, CustomPresenter
19
+ # => <OnePresenter<CustomPresenter>>
20
+ #
21
+ # # Using a collection
22
+ # Builder.present [box, box], template
23
+ # => [<BoxPresenter>, <BoxPresenter>]
24
+ #
25
+ def self.present(objects, template, *presenters)
26
+ if objects.respond_to?(:each)
27
+ Builder.present_all(objects, template, *presenters)
28
+ else
29
+ Builder.present_one(objects, template, *presenters)
30
+ end
31
+ end
32
+
33
+ # Presents a single object (see .present)
34
+ #
35
+ # Example:
36
+ #
37
+ # Builder.present box, template
38
+ # => <BoxPresenter>
39
+ #
40
+ def self.present_one(object, template, *presenters)
41
+ presenters = [Naming.presenter_for(object)] unless presenters.any?
42
+
43
+ unless presenters.all? { |p| p.is_a?(Class) && p.ancestors.include?(Resubject::Presenter) }
44
+ raise InvalidPresenterArgument.new("Expected a presenter in #{presenters.inspect}")
45
+ end
46
+
47
+ presenters.inject(object) do |presented, klass|
48
+ klass.new(presented, template)
49
+ end
50
+ end
51
+
52
+ # Presents a collection of objects (see .present)
53
+ #
54
+ # Example:
55
+ #
56
+ # Builder.present [box, box], template
57
+ # => [<BoxPresenter>, <BoxPresenter>]
58
+ #
59
+ def self.present_all(objects, template, *presenters)
60
+ objects.map do |o|
61
+ Builder.present_one(o, template, *presenters)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,20 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+
3
+ module Resubject
4
+ module Naming
5
+ def self.presenter_for(presentable)
6
+ klass = case presentable
7
+ when Symbol
8
+ presentable.to_s
9
+ when String
10
+ presentable
11
+ else
12
+ presentable.class.to_s
13
+ end
14
+
15
+ presenter = "#{klass.camelize}Presenter"
16
+
17
+ Object.const_get presenter
18
+ end
19
+ end
20
+ end
@@ -2,36 +2,55 @@ require 'delegate'
2
2
 
3
3
  module Resubject
4
4
  class Presenter < SimpleDelegator
5
- attr_reader :template
6
- alias :h :template
5
+ attr_reader :context
6
+ alias_method :template, :context
7
7
 
8
- def initialize(model, template)
9
- @template = template
8
+ def initialize(model, context = nil)
9
+ @context = context
10
10
  super(model)
11
11
  end
12
12
 
13
- def to_model
14
- __getobj__
13
+ alias_method :to_model, :__getobj__
14
+
15
+ # Builds a collection of presenters given an array of objects
16
+ #
17
+ # Example:
18
+ #
19
+ # boxes = [box1, box2, box3]
20
+ # BoxPresenter.all boxes
21
+ # # => [<BoxPresenter>, <BoxPresenter>, <BoxPresenter>]
22
+ #
23
+ def self.all(collection, context = nil)
24
+ collection.map { |c| new(c, context) }
15
25
  end
16
26
 
17
- def self.all(collection, template)
18
- collection.map { |object| new(object, template) }
27
+ # Creates a presenter from object or collection of objects
28
+ # See Builder.present for more details
29
+ # Example:
30
+ #
31
+ # present box
32
+ # => <BoxPresenter>
33
+ #
34
+ def present(objects, *presenters)
35
+ Builder.present objects, context, *presenters
19
36
  end
20
37
 
21
- private
22
-
23
- def self.inject_presenter_for(*options)
24
- options.each do |resource|
25
- presenter_name = "#{resource}_presenter"
26
- attr_writer presenter_name
27
-
28
- define_method presenter_name do
29
- instance_variable_get("@#{presenter_name}") || presenter_name.camelize.constantize
30
- end
31
-
32
- define_method "present_#{resource}" do |object|
33
- send(presenter_name).new(object, @template)
34
- end
38
+ # Generates a instance method with the attribute presented
39
+ #
40
+ # Example:
41
+ #
42
+ # class BoxPresenter < Resubject::Presenter
43
+ # presents :name
44
+ # # or
45
+ # # presents :name, CustomPresenter
46
+ # end
47
+ #
48
+ # BoxPresenter.new(box).name
49
+ # => <NamePresenter>
50
+ #
51
+ def self.presents(attribute, *presenters)
52
+ define_method attribute do
53
+ present to_model.send(attribute), *presenters
35
54
  end
36
55
  end
37
56
  end
@@ -0,0 +1,10 @@
1
+ module Resubject
2
+ class Engine < ::Rails::Engine #:nodoc:
3
+ initializer "resubject.helpers" do
4
+ ActiveSupport.on_load(:action_controller) do
5
+ require 'resubject/rails/helpers'
6
+ include Resubject::Helpers
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,30 @@
1
+ module Resubject
2
+ module Rails #:nodoc:
3
+ module Extensions
4
+ # Generates an attribute using `number_to_currency` helper from rails
5
+ #
6
+ # Examples:
7
+ #
8
+ # class ProductPresenter < Resubject::Presenter
9
+ # currency :price
10
+ # end
11
+ #
12
+ # # Will create a `price` attribute using `number_to_currency`
13
+ #
14
+ # product.price
15
+ # # => '$10.00'
16
+ #
17
+ # Options:
18
+ #
19
+ # currency :price, precision: 3 # => '$123.456'
20
+ # currency :price, locale: :fr # => '123,51 €'
21
+ #
22
+ # See ActionView::Helpers::NumberHelper#number_to_currency[http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_to_currency] for other options
23
+ def currency(attribute, options = {})
24
+ define_method attribute do
25
+ template.number_to_currency to_model.send(attribute), options
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,27 +1,17 @@
1
1
  module Resubject
2
2
  module Helpers
3
- def self.included(base)
3
+ def self.included(base) #:nodoc:
4
4
  base.send(:helper_method, :present)
5
- base.send(:private, :present_object)
6
5
  end
7
6
 
8
- def present(object, *presenter_classes)
9
- presenter = if object.respond_to?(:each)
10
- object.map { |o| present_object(o, presenter_classes) }
11
- else
12
- present_object(object, presenter_classes)
13
- end
7
+ def present(objects, *presenters)
8
+ presenters = Builder.present(objects, view_context, *presenters)
14
9
 
15
- presenter.tap do |p|
10
+ presenters.tap do |p|
16
11
  if block_given?
17
12
  yield p
18
13
  end
19
14
  end
20
15
  end
21
-
22
- def present_object(object, presenter_classes)
23
- presenter_classes = ["#{object.class}Presenter".constantize] if presenter_classes.empty?
24
- presenter_classes.inject(object) { |o, klass| klass.new(o, view_context) }
25
- end
26
16
  end
27
17
  end
@@ -1,11 +1,34 @@
1
- require 'resubject/rails/helpers'
1
+ require 'resubject/rails/engine'
2
+ require 'resubject/rails/extensions'
2
3
 
3
4
  module Resubject
4
- class Engine < ::Rails::Engine
5
- initializer "resubject.helpers" do
6
- ActiveSupport.on_load(:action_controller) do
7
- include Resubject::Helpers
8
- end
5
+ class Presenter
6
+ extend Resubject::Rails::Extensions
7
+
8
+ private
9
+
10
+ def translate(*args, &block)
11
+ context.t(*args, &block)
12
+ end
13
+
14
+ alias_method :t, :translate
15
+
16
+ def localize(*args, &block)
17
+ context.l(*args, &block)
18
+ end
19
+
20
+ alias_method :l, :localize
21
+
22
+ def routes
23
+ ::Rails.application.routes.url_helpers
24
+ end
25
+
26
+ alias_method :r, :routes
27
+
28
+ def helpers
29
+ context
9
30
  end
31
+
32
+ alias_method :h, :helpers
10
33
  end
11
34
  end
@@ -1,3 +1,3 @@
1
1
  module Resubject
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/resubject.rb CHANGED
@@ -1,3 +1,5 @@
1
1
  require 'resubject/version'
2
+ require 'resubject/naming'
2
3
  require 'resubject/presenter'
4
+ require 'resubject/builder'
3
5
  require 'resubject/rails' if defined? Rails
data/resubject.gemspec CHANGED
@@ -18,6 +18,8 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
+ gem.add_dependency 'activesupport', '~> 3.2'
22
+
21
23
  gem.add_development_dependency "rake"
22
24
  gem.add_development_dependency "rspec", "~> 2.12.0"
23
25
  end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ describe Resubject::Builder do
4
+ let(:template) { stub }
5
+
6
+ before do
7
+ stub_const 'Box', Class.new
8
+ stub_const 'BoxPresenter', Class.new(Resubject::Presenter)
9
+ stub_const 'OtherBoxPresenter', Class.new(Resubject::Presenter)
10
+ end
11
+
12
+ describe '.present_one' do
13
+ it 'presents the object with related class' do
14
+ presented = Resubject::Builder.present_one Box.new, template
15
+ expect(presented).to be_a BoxPresenter
16
+ end
17
+
18
+ it 'presents the object with custom class' do
19
+ presented = Resubject::Builder.present_one Box.new, template, OtherBoxPresenter
20
+ expect(presented).to be_a OtherBoxPresenter
21
+ end
22
+
23
+ it 'presents the object with multiple classes' do
24
+ OtherBoxPresenter.should_receive(:new).twice
25
+ presenters = [OtherBoxPresenter, OtherBoxPresenter]
26
+ presented = Resubject::Builder.present_one Box.new, template, *presenters
27
+ end
28
+
29
+ it 'raises an error if custom presenter is not a presenter' do
30
+ expect do
31
+ Resubject::Builder.present_one Box.new, template, *[nil, Class.new]
32
+ end.to raise_error(Resubject::Builder::InvalidPresenterArgument)
33
+ end
34
+ end
35
+
36
+ describe '.present_all' do
37
+ it 'presents multiple objects' do
38
+ box = Box.new
39
+ presented = Resubject::Builder.present_all [box, box], template
40
+
41
+ expect(presented.map(&:class)).to eq [BoxPresenter, BoxPresenter]
42
+ end
43
+
44
+ it 'presents multiple objects with custom presenters' do
45
+ box = Box.new
46
+ presented = Resubject::Builder.present_all [box, box], template, OtherBoxPresenter
47
+
48
+ expect(presented.map(&:class)).to eq [OtherBoxPresenter, OtherBoxPresenter]
49
+ end
50
+ end
51
+
52
+ describe '.present' do
53
+ it 'presents single object' do
54
+ presented = Resubject::Builder.present Box.new, template
55
+ expect(presented).to be_a BoxPresenter
56
+ end
57
+
58
+ it 'presents multiple objects' do
59
+ box = Box.new
60
+ presented = Resubject::Builder.present [box, box], template
61
+
62
+ expect(presented.map(&:class)).to eq [BoxPresenter, BoxPresenter]
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Resubject::Naming do
4
+ before do
5
+ stub_const 'Box', Class.new
6
+ stub_const 'BoxPresenter', Class.new
7
+ end
8
+
9
+ it 'gets the presenter class from an object' do
10
+ presenter = Resubject::Naming.presenter_for Box.new
11
+
12
+ expect(presenter).to eq BoxPresenter
13
+ end
14
+
15
+ it 'gets the presenter class from a symbol' do
16
+ presenter = Resubject::Naming.presenter_for :box
17
+
18
+ expect(presenter).to eq BoxPresenter
19
+ end
20
+
21
+ it 'gets the presenter class from a string' do
22
+ presenter = Resubject::Naming.presenter_for 'box'
23
+
24
+ expect(presenter).to eq BoxPresenter
25
+ end
26
+ end
@@ -2,12 +2,62 @@ require 'spec_helper'
2
2
 
3
3
  describe Resubject::Presenter do
4
4
  let(:object) { mock :object }
5
- let(:template) { mock :template }
6
5
 
7
- subject { Resubject::Presenter.new(object, template) }
6
+ subject { Resubject::Presenter.new(object) }
8
7
 
9
8
  it 'delegate methods to object' do
10
9
  object.should_receive(:pretty)
11
10
  subject.pretty
12
11
  end
12
+
13
+ describe :to_model do
14
+ it 'returns the delegated object' do
15
+ expect(subject.to_model).to eq object
16
+ end
17
+ end
18
+
19
+ describe '.all' do
20
+ it 'creates various instances of the presenter' do
21
+ presented = Resubject::Presenter.all([object, object])
22
+
23
+ expect(presented.map(&:class)).to eq [Resubject::Presenter,
24
+ Resubject::Presenter]
25
+ end
26
+ end
27
+
28
+ describe :present do
29
+ it 'creates a presenter' do
30
+ stub_const 'Box', Class.new
31
+ stub_const 'BoxPresenter', Class.new(Resubject::Presenter)
32
+
33
+ presented = subject.present Box.new
34
+ expect(presented).to be_a BoxPresenter
35
+ end
36
+ end
37
+
38
+ describe '.presents' do
39
+ let :presenter do
40
+ Class.new(Resubject::Presenter) do
41
+ presents :item
42
+ presents :other_item, ItemPresenter
43
+ end
44
+ end
45
+
46
+ before do
47
+ stub_const 'Item', Class.new
48
+ stub_const 'ItemPresenter', Class.new(Resubject::Presenter)
49
+ end
50
+
51
+ it 'generates a method preseting the attribute' do
52
+ box = stub :box, :item => Item.new
53
+
54
+ expect(presenter.new(box).item).to be_a ItemPresenter
55
+ end
56
+
57
+ it 'presents the attributes with custom presenter' do
58
+ box = stub :box, :other_item => stub
59
+
60
+ expect(presenter.new(box).other_item).to be_a ItemPresenter
61
+ end
62
+ end
13
63
  end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+ require 'rails/all'
3
+ require 'resubject/rails'
4
+
5
+ describe Resubject::Presenter, 'extensions' do
6
+ let(:model) { mock :model }
7
+
8
+ let(:presenter) do
9
+ Class.new(Resubject::Presenter) do
10
+ currency :price
11
+ end
12
+ end
13
+
14
+ subject do
15
+ presenter.new(model, ActionView::Base.new)
16
+ end
17
+
18
+ describe '.currency' do
19
+ it 'returns currency format' do
20
+ model.should_receive(:price).and_return(10.0)
21
+ expect(subject.price).to eq "$10.00"
22
+ end
23
+ end
24
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resubject
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,8 +10,24 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-12-19 00:00:00.000000000 Z
13
+ date: 2013-01-06 00:00:00.000000000 Z
14
14
  dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ type: :runtime
17
+ version_requirements: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: '3.2'
23
+ name: activesupport
24
+ prerelease: false
25
+ requirement: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ version: '3.2'
15
31
  - !ruby/object:Gem::Dependency
16
32
  type: :development
17
33
  version_requirements: !ruby/object:Gem::Requirement
@@ -56,16 +72,24 @@ files:
56
72
  - .rspec
57
73
  - .travis.yml
58
74
  - Gemfile
75
+ - IDEAS.md
59
76
  - LICENSE.txt
60
77
  - README.md
61
78
  - Rakefile
62
79
  - lib/resubject.rb
80
+ - lib/resubject/builder.rb
81
+ - lib/resubject/naming.rb
63
82
  - lib/resubject/presenter.rb
64
83
  - lib/resubject/rails.rb
84
+ - lib/resubject/rails/engine.rb
85
+ - lib/resubject/rails/extensions.rb
65
86
  - lib/resubject/rails/helpers.rb
66
87
  - lib/resubject/version.rb
67
88
  - resubject.gemspec
89
+ - spec/resubject/builder_spec.rb
90
+ - spec/resubject/naming_spec.rb
68
91
  - spec/resubject/presenter_spec.rb
92
+ - spec/resubject/rails/extensions_spec.rb
69
93
  - spec/spec_helper.rb
70
94
  homepage: https://github.com/felipeelias/resubject
71
95
  licenses:
@@ -82,7 +106,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
82
106
  segments:
83
107
  - 0
84
108
  version: '0'
85
- hash: 3361348896197597575
109
+ hash: 134183276947229850
86
110
  required_rubygems_version: !ruby/object:Gem::Requirement
87
111
  none: false
88
112
  requirements:
@@ -91,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
115
  segments:
92
116
  - 0
93
117
  version: '0'
94
- hash: 3361348896197597575
118
+ hash: 134183276947229850
95
119
  requirements: []
96
120
  rubyforge_project:
97
121
  rubygems_version: 1.8.24
@@ -99,5 +123,8 @@ signing_key:
99
123
  specification_version: 3
100
124
  summary: Uber simple presenters using Ruby's SimpleDelegator
101
125
  test_files:
126
+ - spec/resubject/builder_spec.rb
127
+ - spec/resubject/naming_spec.rb
102
128
  - spec/resubject/presenter_spec.rb
129
+ - spec/resubject/rails/extensions_spec.rb
103
130
  - spec/spec_helper.rb