objectify 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -37,10 +37,8 @@ Objectify has two primary components:
37
37
 
38
38
  ```ruby
39
39
  class UnauthenticatedResponder
40
- # yes, at some point we probably need a better interface
41
- # for handling responses, but this'll do for now.
42
- def call(controller, renderer)
43
- renderer.redirect_to controller.login_url
40
+ def call(format, routes)
41
+ format.any { redirect_to routes.login_url }
44
42
  end
45
43
  end
46
44
  ```
@@ -73,14 +71,14 @@ Objectify has two primary components:
73
71
  ```ruby
74
72
  class PicturesCreateResponder
75
73
  # service_result is exactly what it sounds like
76
- def call(service_result, controller, renderer)
74
+ def call(service_result, format)
77
75
  if service_result.persisted?
78
- renderer.redirect_to service_result
76
+ format.any { redirect_to service_result }
79
77
  else
80
- # this is the only way that you can pass data to the view layer
81
- # and you can only pass one thing. Hint: use a presenter.
82
- renderer.data(service_result)
83
- renderer.render :template => "pictures/edit.html.erb"
78
+ # the service_result is always the only thing passed to the view
79
+ # (hint: use a presenter)
80
+ # you can access it with the `objectify_data` helper.
81
+ format.any { render :template => "pictures/new.html.erb" }
84
82
  end
85
83
  end
86
84
  end
@@ -229,6 +227,61 @@ describe "CurrentUserResolver" do
229
227
  end
230
228
  ```
231
229
 
230
+ ### Decorators
231
+
232
+ Decorators are a great way to create truly modular and composable software. Here's a great example.
233
+
234
+ In objectify\_auth, there's a SessionsCreateService that you can use as the basis for the creat action in your /sessions resource. By default, it does very little:
235
+
236
+ ```ruby
237
+ class SessionsCreateService
238
+ def initialize(authenticator, session_creator)
239
+ @authenticator = authenticator
240
+ @session_creator = session_creator
241
+ end
242
+
243
+ def call(params, session)
244
+ @authenticator.call(params[:email], params[:password]).tap do |user|
245
+ @session_creator.call(session) if user
246
+ end
247
+ end
248
+ end
249
+ ```
250
+
251
+ Let's say we wanted to add remember token issuance. We could rewrite the entire SessionsCreationService or extend (with inheritance) it to do that, but then we'd have to retest the whole unit again. A decorator allows us to avoid that:
252
+
253
+ ```ruby
254
+ class SessionsCreateServiceWithRememberToken
255
+ def initialize(sessions_create_service, remember_token_generator)
256
+ @sessions_create_service = sessions_create_service
257
+ @remember_token_generator = remember_token_generator
258
+ end
259
+
260
+ def call(params, session, cookies)
261
+ @sessions_create_service.call(params, session).tap do |user|
262
+ if user
263
+ token = @remember_token_generator.call(user)
264
+ cookies[:remember_token] = { ... }
265
+ end
266
+ end
267
+ end
268
+ end
269
+ ```
270
+
271
+ This makes for a very simple and easy to test extension to our SessionsCreateService. We can tell objectify to use this decorator like this:
272
+
273
+ ```ruby
274
+ # config/routes.rb
275
+ objectify.decorators :sessions_create_service => :sessions_create_service_with_remember_token
276
+ ```
277
+
278
+ If we wanted to specify additional decorators, it'd look like this:
279
+
280
+ ```ruby
281
+ # config/routes.rb
282
+ objectify.decorators :sessions_create_service => [:sessions_create_service_with_remember_token, :sessions_create_service_with_captcha_verification]
283
+ ```
284
+
232
285
  ## Views
233
286
 
234
287
  Objectify has two major impacts on your views.
@@ -6,9 +6,10 @@ module Objectify
6
6
  class Action
7
7
  attr_reader :resource_name, :name, :route, :policies, :service, :responder
8
8
 
9
- def initialize(resource_name, name, options, default_policies,
9
+ def initialize(routing_opts,
10
+ resource_name, name, options, default_policies,
10
11
  route_factory = Route)
11
- @route = route_factory.new(resource_name, name)
12
+ @route = route_factory.new(routing_opts)
12
13
  @resource_name = resource_name
13
14
  @name = name
14
15
  @policies = default_policies.merge(options, options[name])
@@ -57,7 +57,7 @@ module Objectify
57
57
 
58
58
  def legacy_action(route)
59
59
  actions[route] ||
60
- @action_factory.new(route.resource, route.action, {}, policies)
60
+ @action_factory.new(route.opts, route.opts[:controller], route.opts[:action], {}, policies)
61
61
  end
62
62
 
63
63
  def injector
@@ -86,6 +86,12 @@ module Objectify
86
86
  end
87
87
  end
88
88
 
89
+ def append_decorators(opts)
90
+ opts.each do |k,v|
91
+ injectables.add_decorators(k, v)
92
+ end
93
+ end
94
+
89
95
  def instantiator
90
96
  @instantiator ||= Instantiator.new(injector)
91
97
  end
@@ -6,6 +6,7 @@ module Objectify
6
6
 
7
7
  def initialize(config = {})
8
8
  @config = config
9
+ @decorators = {}
9
10
  end
10
11
 
11
12
  def add_resolver(name, value)
@@ -24,6 +25,14 @@ module Objectify
24
25
  @config[name] || context[name] || [:unknown, name]
25
26
  end
26
27
 
28
+ def add_decorators(name, value)
29
+ @decorators[name] = value
30
+ end
31
+
32
+ def decorators(name)
33
+ [*@decorators[name]].compact
34
+ end
35
+
27
36
  def context
28
37
  @context && @context.config || {}
29
38
  end
@@ -8,6 +8,7 @@ module Objectify
8
8
 
9
9
  def initialize(config)
10
10
  @config = config
11
+ @decoration_context = {}
11
12
  end
12
13
 
13
14
  def call(object, method)
@@ -15,7 +16,7 @@ module Objectify
15
16
  instrument("inject.objectify", payload) do |payload|
16
17
  method_obj = method_object(object, method)
17
18
  injectables = method_obj.parameters.map do |reqd, name|
18
- @config.get(name) if reqd == :req
19
+ @decoration_context[name] || @config.get(name) if reqd == :req
19
20
  end.compact
20
21
  arguments = injectables.map do |type, value|
21
22
  if type == :unknown
@@ -39,7 +40,19 @@ module Objectify
39
40
  payload[:injectables] = injectables
40
41
  payload[:arguments] = arguments
41
42
 
42
- object.send(method, *arguments)
43
+ result = object.send(method, *arguments)
44
+ if method == :new
45
+ base_name = object.name.underscore.to_sym
46
+ add_decoration_context(base_name, result)
47
+ result = @config.decorators(base_name).inject(result) do |memo, decorator|
48
+ call(decorator.to_s.classify.constantize, :new).tap do |decorated|
49
+ add_decoration_context(base_name, decorated)
50
+ end
51
+ end
52
+ clear_decoration_context
53
+ end
54
+
55
+ result
43
56
  end
44
57
  end
45
58
 
@@ -63,5 +76,13 @@ module Objectify
63
76
 
64
77
  raise ArgumentError, "Can't figure out how to inject #{value}."
65
78
  end
79
+
80
+ def add_decoration_context(name, object)
81
+ @decoration_context[name] = [:value, object]
82
+ end
83
+
84
+ def clear_decoration_context
85
+ @decoration_context.clear
86
+ end
66
87
  end
67
88
  end
@@ -3,6 +3,7 @@ require "objectify/executor"
3
3
  require "objectify/policy_chain_executor"
4
4
  require "objectify/instrumentation"
5
5
  require "objectify/rails/renderer"
6
+ require "objectify/rails/routes"
6
7
 
7
8
  module Objectify
8
9
  module Rails
@@ -20,6 +21,12 @@ module Objectify
20
21
  objectify.injector
21
22
  end
22
23
 
24
+ def objectify_routes
25
+ @objectify_routes ||= Routes.new.tap do |routes|
26
+ routes.default_url_options = url_options
27
+ end
28
+ end
29
+
23
30
  def request_injectables_context
24
31
  klass = Objectify::Config::Injectables
25
32
  @request_injectables_context ||= klass.new.tap do |injectables_context|
@@ -31,8 +38,14 @@ module Objectify
31
38
  injectables_context.add_value(:response, response)
32
39
  injectables_context.add_value(:flash, flash)
33
40
  injectables_context.add_value(:renderer, Renderer.new(self))
41
+ injectables_context.add_value(:format, objectify_response_collector)
42
+ injectables_context.add_value(:routes, objectify_routes)
34
43
  end
35
44
  end
45
+
46
+ def objectify_response_collector
47
+ @objectify_response_collector ||= ActionController::MimeResponds::Collector.new { default_response }
48
+ end
36
49
 
37
50
  def objectify_executor
38
51
  objectify.executor
@@ -43,8 +56,9 @@ module Objectify
43
56
  end
44
57
 
45
58
  def objectify_route
46
- resource_name = params[:objectify] ? params[:objectify][:resource] : params[:controller]
47
- @objectify_route ||= Objectify::Route.new(resource_name.to_sym, params[:action].to_sym)
59
+ routing_options = params[:objectify] ? params[:objectify] : {:controller => params[:controller].to_sym, :action => params[:action].to_sym}
60
+ routing_options.merge!(:action => params[:action]) if routing_options.delete(:append_action)
61
+ @objectify_route ||= Objectify::Route.new(routing_options)
48
62
  end
49
63
 
50
64
  def action
@@ -57,6 +71,7 @@ module Objectify
57
71
 
58
72
  def execute_policy_chain
59
73
  policy_chain_executor.call(action)
74
+ objectify_respond_if_response
60
75
  end
61
76
 
62
77
  def objectify_around_filter
@@ -66,10 +81,19 @@ module Objectify
66
81
  end
67
82
 
68
83
  def execute_objectify_action
69
- service_result = objectify_executor.call(action.service, :service)
70
- request_injectables_context.add_value(:service_result, service_result)
84
+ @objectify_service_result = objectify_executor.call(action.service, :service)
85
+ request_injectables_context.add_value(:service_result, @objectify_service_result)
71
86
 
72
87
  objectify_executor.call(action.responder, :responder)
88
+ objectify_respond_if_response
89
+ end
90
+
91
+ def objectify_respond_if_response
92
+ if format = request.negotiate_mime(objectify_response_collector.order)
93
+ self.content_type ||= format.to_s
94
+ lookup_context.freeze_formats([format.to_sym])
95
+ instance_eval &objectify_response_collector.response_for(format)
96
+ end
73
97
  end
74
98
  end
75
99
 
@@ -81,11 +105,24 @@ module Objectify
81
105
  klass.helper_method(:objectify_executor) if klass.respond_to?(:helper_method)
82
106
  end
83
107
 
84
- def method_missing(name, *args, &block)
108
+ def action_missing(name, *args, &block)
85
109
  instrument("start_processing.objectify", :route => objectify_route)
86
110
 
87
111
  execute_objectify_action
88
112
  end
113
+
114
+ def controller_path
115
+ action.resource_name
116
+ end
117
+
118
+ def action_name
119
+ action.name
120
+ end
121
+
122
+ private
123
+ def view_assigns
124
+ {:_objectify_data => @objectify_service_result}
125
+ end
89
126
  end
90
127
 
91
128
  module ControllerBehaviour
@@ -96,16 +133,30 @@ module Objectify
96
133
  klass.helper_method(:objectify_executor) if klass.respond_to?(:helper_method)
97
134
  end
98
135
 
99
- def method_missing(name, *args, &block)
136
+ def action_missing(name, *args, &block)
100
137
  instrument("start_processing.objectify", :route => objectify_route)
101
138
 
102
139
  if execute_policy_chain
103
140
  execute_objectify_action
104
141
  end
105
142
  end
143
+
144
+ def controller_path
145
+ action.resource_name
146
+ end
147
+
148
+ def action_name
149
+ action.name
150
+ end
151
+
152
+ private
153
+ def view_assigns
154
+ {:_objectify_data => @objectify_service_result}
155
+ end
106
156
  end
107
157
 
108
158
  class ObjectifyController < ActionController::Base
159
+ around_filter :objectify_around_filter
109
160
  include ControllerBehaviour
110
161
  end
111
162
  end
@@ -0,0 +1,7 @@
1
+ module Objectify
2
+ module Rails
3
+ class Routes
4
+ include ::Rails.application.routes.url_helpers
5
+ end
6
+ end
7
+ end
@@ -6,7 +6,7 @@ module Objectify
6
6
  module Routing
7
7
  RESOURCE_ACTIONS = [:index, :show, :new, :create,
8
8
  :edit, :update, :destroy].freeze
9
- OBJECTIFY_OPTIONS = [:policies].freeze
9
+ OBJECTIFY_OPTIONS = [:policies, :service, :skip_policies].freeze
10
10
 
11
11
  class ObjectifyMapper
12
12
  def initialize(rails_mapper,
@@ -24,12 +24,31 @@ module Objectify
24
24
  rails_options = options.merge(:controller => controller)
25
25
 
26
26
  args.each do |resource_name|
27
- merged_defaults = objectify_defaults(resource_name, rails_options)
27
+ objectify_defaults = {:resource => resource_name}
28
+ merged_defaults = merge_defaults(objectify_defaults.merge(:append_action => true),
29
+ rails_options)
28
30
  @rails_mapper.resources(resource_name, merged_defaults)
29
- RESOURCE_ACTIONS.each { |action_name| append_action(resource_name, action_name, objectify_options) }
31
+ RESOURCE_ACTIONS.each do |action_name|
32
+ append_action(objectify_defaults.merge(:action => action_name),
33
+ resource_name,
34
+ action_name,
35
+ objectify_options)
36
+ end
30
37
  end
31
38
  end
32
39
 
40
+ def match(options)
41
+ from,to = options.detect { |k,v| k.is_a?(String) }
42
+ resource,action = to.split("#").map(&:to_sym)
43
+ controller = @application.objectify.objectify_controller
44
+ objectify_options = extract_objectify_options(options)
45
+ objectify_defaults = {"path" => from.dup}
46
+ rails_options = merge_defaults(objectify_defaults, options)
47
+ @rails_mapper.match rails_options.merge(from => "#{controller}#action")
48
+
49
+ append_action(objectify_defaults, resource, action, objectify_options)
50
+ end
51
+
33
52
  def defaults(options)
34
53
  @application.objectify.append_defaults(options)
35
54
  end
@@ -50,8 +69,16 @@ module Objectify
50
69
  @application.objectify.append_values(options)
51
70
  end
52
71
 
72
+ def decorators(options)
73
+ @application.objectify.append_decorators(options)
74
+ end
75
+
53
76
  def legacy_action(controller, actions, options)
54
- [*actions].each { |action_name| append_action(controller, action_name, options) }
77
+ [*actions].each do |action_name|
78
+ routing_opts = {:controller => controller,
79
+ :action => action_name}
80
+ append_action(routing_opts, controller, action_name, options)
81
+ end
55
82
  end
56
83
 
57
84
  private
@@ -61,14 +88,15 @@ module Objectify
61
88
  end.compact.flatten]
62
89
  end
63
90
 
64
- def objectify_defaults(resource_name, rails_options)
65
- defaults = {:objectify => {:resource => resource_name}}
91
+ def merge_defaults(objectify_defaults, rails_options)
92
+ defaults = {:objectify => objectify_defaults}
66
93
  defaults = (rails_options[:defaults] || {}).merge(defaults)
67
94
  defaults = rails_options.merge(:defaults => defaults)
68
95
  end
69
96
 
70
- def append_action(resource_name, action_name, options)
71
- action = @action_factory.new(resource_name,
97
+ def append_action(routing_opts, resource_name, action_name, options)
98
+ action = @action_factory.new(routing_opts,
99
+ resource_name,
72
100
  action_name,
73
101
  options,
74
102
  @application.objectify.policies)
@@ -1,4 +1,4 @@
1
1
  module Objectify
2
- class Route < Struct.new(:resource, :action)
2
+ class Route < Struct.new(:opts)
3
3
  end
4
4
  end
@@ -1,3 +1,3 @@
1
1
  module Objectify
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -8,6 +8,8 @@ describe "Objectify::Config::Action" do
8
8
 
9
9
  @route = stub("Route")
10
10
  @route_factory = stub("RouteFactory", :new => @route)
11
+
12
+ @routing_opts = {:some => :route}
11
13
  end
12
14
 
13
15
  context "when everything is specified" do
@@ -23,7 +25,8 @@ describe "Objectify::Config::Action" do
23
25
  :index => @index_options
24
26
  }
25
27
 
26
- @action = Objectify::Config::Action.new(:pictures,
28
+ @action = Objectify::Config::Action.new(@routing_opts,
29
+ :pictures,
27
30
  :index,
28
31
  @options,
29
32
  @default_policies,
@@ -31,7 +34,7 @@ describe "Objectify::Config::Action" do
31
34
  end
32
35
 
33
36
  it "creates and stores a route from the route factory" do
34
- @route_factory.should have_received(:new).with(:pictures, :index)
37
+ @route_factory.should have_received(:new).with(@routing_opts)
35
38
  @action.route.should == @route
36
39
  end
37
40
 
@@ -61,7 +64,8 @@ describe "Objectify::Config::Action" do
61
64
  context "when nothing is specified" do
62
65
  before do
63
66
  @options = {}
64
- @action = Objectify::Config::Action.new(:pictures,
67
+ @action = Objectify::Config::Action.new(@routing_opts,
68
+ :pictures,
65
69
  :index,
66
70
  @options,
67
71
  @default_policies,
@@ -76,7 +76,8 @@ describe "Objectify::Config::Context" do
76
76
  @action_factory = stub("ActionFactory", :new => @action)
77
77
  @policies = stub("Policies")
78
78
  @policies_factory = stub("PoliciesFactory", :new => @policies)
79
- @route = stub("Route", :resource => :controller, :action => :action)
79
+ @route_opts = {:controller => :controller, :action => :action}
80
+ @route = stub("Route", :opts => @route_opts)
80
81
 
81
82
  @context = Objectify::Config::Context.new(@policies_factory, @action_factory)
82
83
  end
@@ -88,7 +89,9 @@ describe "Objectify::Config::Context" do
88
89
 
89
90
  it "creates a new action with the controller and action name and its policies" do
90
91
  @action_factory.should have_received(:new).
91
- with(:controller, :action, {}, @policies)
92
+ with(@route_opts,
93
+ :controller,
94
+ :action, {}, @policies)
92
95
  end
93
96
  end
94
97
 
@@ -37,4 +37,12 @@ describe "Objectify::Config::Injectables" do
37
37
  @injectables.context = @context
38
38
  @injectables.get(:controller).should == [:value, :something]
39
39
  end
40
+
41
+ it "accepts decorators" do
42
+ @injectables.add_decorators(:base, :decorator1)
43
+ @injectables.decorators(:base).should == [:decorator1]
44
+ @injectables.add_decorators(:base, [:decorator1, :decorator2])
45
+ @injectables.decorators(:base).should == [:decorator1, :decorator2]
46
+ @injectables.decorators(:nonexistent).should == []
47
+ end
40
48
  end
@@ -40,7 +40,7 @@ describe "Objectify::Injector" do
40
40
  end
41
41
 
42
42
  before do
43
- @config = stub("Config", :get => nil)
43
+ @config = stub("Config", :get => nil, :decorators => [])
44
44
  @injector = Objectify::Injector.new(@config)
45
45
  end
46
46
 
@@ -99,4 +99,40 @@ describe "Objectify::Injector" do
99
99
  @injector.call(obj, :requires_params).call.should == :SOMETHING
100
100
  end
101
101
  end
102
+
103
+ class ToBeDecorated
104
+ end
105
+
106
+ class Decorator1
107
+ attr_reader :to_be_decorated
108
+
109
+ def initialize(to_be_decorated)
110
+ @to_be_decorated = to_be_decorated
111
+ end
112
+ end
113
+
114
+ class Decorator2
115
+ attr_reader :to_be_decorated
116
+
117
+ def initialize(to_be_decorated)
118
+ @to_be_decorated = to_be_decorated
119
+ end
120
+ end
121
+
122
+ context "decorating an object" do
123
+ before do
124
+ decorators = [:decorator1, :decorator2]
125
+ @config.stubs(:decorators).with(:to_be_decorated).returns(decorators)
126
+ @result = @injector.call(ToBeDecorated, :new)
127
+ end
128
+
129
+ it "decorates left to right" do
130
+ @result.should be_instance_of(Decorator2)
131
+ @result.to_be_decorated.should be_instance_of(Decorator1)
132
+ @result.to_be_decorated.to_be_decorated.should be_instance_of(ToBeDecorated)
133
+
134
+ @config.stubs(:decorators).with(:to_be_decorated).returns([])
135
+ @injector.call(ToBeDecorated, :new).should be_instance_of(ToBeDecorated)
136
+ end
137
+ end
102
138
  end
@@ -11,6 +11,7 @@ describe "Objectify::Rails::Routing::ObjectifyMapper" do
11
11
  :append_implementations => nil,
12
12
  :append_resolvers => nil,
13
13
  :append_values => nil,
14
+ :append_decorators => nil,
14
15
  :objectify_controller => "some_controller")
15
16
  @rails_mapper = stub("RailsMapper", :resources => nil,
16
17
  :match => nil)
@@ -25,6 +26,9 @@ describe "Objectify::Rails::Routing::ObjectifyMapper" do
25
26
 
26
27
  context "adding a resource" do
27
28
  before do
29
+ @rails_routing_info = {:resource => :pictures,
30
+ :append_action => true}
31
+ @objectify_routing_info = {:resource => :pictures}
28
32
  @mapper.resources(:pictures, :policies => :some_policy,
29
33
  :create => {
30
34
  :policies => :blocked_user,
@@ -34,7 +38,7 @@ describe "Objectify::Rails::Routing::ObjectifyMapper" do
34
38
 
35
39
  it "correctly adds the resource to the rails mapper" do
36
40
  opts = { :controller => @objectify.objectify_controller,
37
- :defaults => {:objectify => {:resource => :pictures}}}
41
+ :defaults => {:objectify => @rails_routing_info}}
38
42
  @rails_mapper.should have_received(:resources).
39
43
  with(:pictures, opts)
40
44
  end
@@ -48,8 +52,13 @@ describe "Objectify::Rails::Routing::ObjectifyMapper" do
48
52
  :policies => :some_policy
49
53
  }
50
54
  Objectify::Rails::Routing::RESOURCE_ACTIONS.each do |action|
55
+ routing_info = @objectify_routing_info.merge(:action => action)
51
56
  @action_factory.should have_received(:new).
52
- with(:pictures, action, opts, @policies)
57
+ with(routing_info,
58
+ :pictures,
59
+ action,
60
+ opts,
61
+ @policies)
53
62
  end
54
63
  end
55
64
 
@@ -61,6 +70,9 @@ describe "Objectify::Rails::Routing::ObjectifyMapper" do
61
70
 
62
71
  context "adding a resource with its own defaults" do
63
72
  before do
73
+ @rails_routing_info = {:resource => :pictures,
74
+ :append_action => true}
75
+ @objectify_routing_info = {:resource => :pictures}
64
76
  @mapper.resources(:pictures, :policies => :some_policy,
65
77
  :create => {
66
78
  :policies => :blocked_user,
@@ -71,7 +83,7 @@ describe "Objectify::Rails::Routing::ObjectifyMapper" do
71
83
 
72
84
  it "correctly adds the resource to the rails mapper" do
73
85
  opts = { :controller => @objectify.objectify_controller,
74
- :defaults => {:objectify => {:resource => :pictures},
86
+ :defaults => {:objectify => @rails_routing_info,
75
87
  :some => :bullshit}}
76
88
  @rails_mapper.should have_received(:resources).
77
89
  with(:pictures, opts)
@@ -86,8 +98,10 @@ describe "Objectify::Rails::Routing::ObjectifyMapper" do
86
98
  :policies => :some_policy
87
99
  }
88
100
  Objectify::Rails::Routing::RESOURCE_ACTIONS.each do |action|
101
+ routing_info = @objectify_routing_info.merge(:action => action)
89
102
  @action_factory.should have_received(:new).
90
- with(:pictures, action, opts, @policies)
103
+ with(routing_info,
104
+ :pictures, action, opts, @policies)
91
105
  end
92
106
  end
93
107
 
@@ -97,6 +111,58 @@ describe "Objectify::Rails::Routing::ObjectifyMapper" do
97
111
  end
98
112
  end
99
113
 
114
+ context "#match" do
115
+ before do
116
+ @routing_info = {"path" => "/pictures"}
117
+ @opts = { "/pictures" => "pictures#create" }
118
+ @mapper.match @opts
119
+ end
120
+
121
+ it "correctly adds the resource to the rails mapper" do
122
+ opts = { "/pictures" => "#{@objectify.objectify_controller}#action",
123
+ :defaults => {:objectify => @routing_info}}
124
+ @rails_mapper.should have_received(:match).with(opts)
125
+ end
126
+
127
+ it "creates an action" do
128
+ @action_factory.should have_received(:new).
129
+ with(@routing_info, :pictures, :create, {}, @policies)
130
+ end
131
+
132
+ it "appends each of the actions to the objectify object" do
133
+ @objectify.should have_received(:append_action).
134
+ with(@action).times(1)
135
+ end
136
+ end
137
+
138
+ context "#match with overrides" do
139
+ before do
140
+ @routing_info = {"path" => "/pictures"}
141
+ @opts = { "/pictures" => "pictures#create" }
142
+ @objectify_opts = {:service => :pics, :skip_policies => :an}
143
+ @opts.merge!(@objectify_opts)
144
+
145
+ @mapper.match @opts
146
+ end
147
+
148
+ it "correctly adds the resource to the rails mapper" do
149
+ opts = { "/pictures" => "#{@objectify.objectify_controller}#action",
150
+ :defaults => {:objectify => @routing_info}}
151
+ @rails_mapper.should have_received(:match).with(opts)
152
+ end
153
+
154
+ it "creates an action" do
155
+ @action_factory.should have_received(:new).
156
+ with(@routing_info, :pictures, :create,
157
+ @objectify_opts, @policies)
158
+ end
159
+
160
+ it "appends each of the actions to the objectify object" do
161
+ @objectify.should have_received(:append_action).
162
+ with(@action).times(1)
163
+ end
164
+ end
165
+
100
166
  context "adding a resource with an alternative objectify_controller" do
101
167
  before do
102
168
  @objectify.stubs(:objectify_controller).returns("asdfbsdf")
@@ -110,7 +176,8 @@ describe "Objectify::Rails::Routing::ObjectifyMapper" do
110
176
 
111
177
  it "correctly adds the resource to the rails mapper" do
112
178
  opts = { :controller => @objectify.objectify_controller,
113
- :defaults => {:objectify => {:resource => :pictures},
179
+ :defaults => {:objectify => {:resource => :pictures,
180
+ :append_action => true},
114
181
  :some => :bullshit}}
115
182
  @rails_mapper.should have_received(:resources).
116
183
  with(:pictures, opts)
@@ -161,6 +228,13 @@ describe "Objectify::Rails::Routing::ObjectifyMapper" do
161
228
  @objectify.should have_received(:append_values).
162
229
  with(@opts)
163
230
  end
231
+
232
+ it "hands decorators to the context" do
233
+ @opts = { :base => :decorator }
234
+ @mapper.decorators @opts
235
+ @objectify.should have_received(:append_decorators).
236
+ with(@opts)
237
+ end
164
238
  end
165
239
 
166
240
  context "adding a legacy action" do
@@ -170,11 +244,20 @@ describe "Objectify::Rails::Routing::ObjectifyMapper" do
170
244
  end
171
245
 
172
246
  it "creates actions for each of the specified actions" do
247
+ routing_opts = {:controller => :controller}
173
248
  @action_factory.should have_received(:new).
174
- with(:controller, :action1, @opts, @policies)
249
+ with(routing_opts.merge(:action => :action1),
250
+ :controller,
251
+ :action1,
252
+ @opts,
253
+ @policies)
175
254
 
176
255
  @action_factory.should have_received(:new).
177
- with(:controller, :action2, @opts, @policies)
256
+ with(routing_opts.merge(:action => :action2),
257
+ :controller,
258
+ :action2,
259
+ @opts,
260
+ @policies)
178
261
  end
179
262
 
180
263
  it "passes the actions to the objectify objects" do
@@ -4,8 +4,8 @@ require "objectify/route"
4
4
  describe "Objectify::Route" do
5
5
  context "two routes with the same path" do
6
6
  it "have the same #hash value" do
7
- Objectify::Route.new(:pictures, :index).should ==
8
- Objectify::Route.new(:pictures, :index)
7
+ Objectify::Route.new(:controller => :pictures, :action => :index).should ==
8
+ Objectify::Route.new(:controller => :pictures, :action => :index)
9
9
  end
10
10
  end
11
11
  end
@@ -0,0 +1,156 @@
1
+ # responders
2
+
3
+ Currently responders collaborate with other objects to actually initiate the response. They're by far the weakest abstraction in objectify. This document contains some thoughts on how to fix that.
4
+
5
+ ## Value objects approach
6
+
7
+ Bryan Helmkamp's proposal on the mailing list was to have responders return value objects that describe the response, and then objectify could use those descriptors to actually fire the response using rails' APIs.
8
+
9
+ An example:
10
+
11
+ ```ruby
12
+ class SessionNewResponder
13
+ def call
14
+ Objectify::Render::Template.new(:name => "new.html.erb")
15
+ end
16
+ end
17
+ ```
18
+
19
+ There are two problems with this. First of all, typing out the whole namespace is cumbersome. Though, that can be solved with one `include`.
20
+
21
+ ```ruby
22
+ class SessionNewResponder
23
+ include Objectify::Render
24
+
25
+ def call
26
+ Template.new(:name => "new.html.erb")
27
+ end
28
+ end
29
+ ```
30
+
31
+ The second problem is how to handle multiple formats. One idea is to use the rails block syntax:
32
+
33
+ ```ruby
34
+ class SessionNewResponder
35
+ include Objectify::Render
36
+
37
+ def call(service_result)
38
+ FormattedResponse.new(:data => service_result) do |format|
39
+ format.html
40
+ format.js
41
+ end
42
+ end
43
+ end
44
+ ```
45
+
46
+ Or something like that. I'm not a huge fan of that syntax because it seems difficult to test, although maybe that's solved by making the result of the little DSL very simple.
47
+
48
+ ```ruby
49
+ class SessionNewResponder
50
+ include Objectify::Render
51
+
52
+ def call(service_result)
53
+ FormattedResponse.new(:data => service_result) do |format|
54
+ format.html Template.new("somewhere.html.erb")
55
+ format.js
56
+ end
57
+ end
58
+ end
59
+ ```
60
+
61
+ After writing this, I'm starting to think it's the right approach.
62
+
63
+ ## Rename to "Responses"?
64
+
65
+ One idea I had this weekend, was to rename responders to responses. That way, conceptually, you're defining a type of response. I think that might be easier for people to think about.
66
+
67
+ Also, I think we should use methods to respond to different content types. Can't believe it took me this long to think of this.
68
+
69
+ ```ruby
70
+ class UnauthorizedResponse
71
+ include Objectify::Response
72
+
73
+ respond_with :html, :js
74
+ status 403
75
+
76
+ def js
77
+ :default
78
+ end
79
+
80
+ def any(service_result, renderer)
81
+ renderer.template :name => "unauthorized.html.erb", :data => service_result
82
+ end
83
+ end
84
+ ```
85
+
86
+ ```ruby
87
+ class PicturesCreateSuccessfulResponse
88
+ include Objectify::Response
89
+
90
+ when_policy :create_successful
91
+ respond_with :html, :js, :json
92
+
93
+ def html(service_result, renderer)
94
+ responder.redirect_to service_result
95
+ end
96
+
97
+ def js(service_result, renderer)
98
+ responder.redirect_to "xyz"
99
+ end
100
+ end
101
+
102
+ class PicturesCreateUnsuccessfulResponse
103
+ include Objectify::Response
104
+
105
+ when_not_policy :create_successful
106
+ respond_with :html, :js, :json
107
+
108
+ def html(service_result, renderer)
109
+ responder.redirect_to service_result
110
+ end
111
+
112
+ def js(service_result, renderer)
113
+ responder.redirect_to "xyz"
114
+ end
115
+ end
116
+
117
+ class PicturesCreateResponse < Objectify::Response
118
+ respond do |service_result, format|
119
+ if service_result.persisted?
120
+ format.html { redirect_to service_result }
121
+ format.js { render :template => "pictures/show.json.json_builder" }
122
+ format.json { render :template => "pictures/show.json.json_builder" }
123
+ else
124
+ format.html { render :template => "pictures/new.html.erb" }
125
+ format.js { render :json => { :errors => service_result.errors, :status => :unprocessable_entity } }
126
+ format.json { render :template => "pictures/show.json.json_builder" }
127
+ end
128
+ end
129
+ end
130
+ ```
131
+
132
+ Tests?
133
+
134
+ ```ruby
135
+ describe "PicturesCreateResponse" do
136
+ before do
137
+ @format = Objectify::FakeFormat.new
138
+ @response = PicturesCreateResponse.new
139
+ end
140
+
141
+ context "when create is successful" do
142
+ before do
143
+ @service_result = stub("Result", :persisted? => true)
144
+ @response.respond(@service_result, @format)
145
+ end
146
+
147
+ it "renders a template called 'pictures/new.html.erb' for html" do
148
+ @response.should render_template("pictures/new.html.erb")
149
+ end
150
+ end
151
+ end
152
+ ```
153
+
154
+ ## Other approaches?
155
+
156
+ * Something that can be tested mockist-style, perhaps? I have no idea what this would look like, though, really.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: objectify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-29 00:00:00.000000000 Z
12
+ date: 2012-06-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: &70203413321160 !ruby/object:Gem::Requirement
16
+ requirement: &70262082543760 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 2.4.0
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *70203413321160
24
+ version_requirements: *70262082543760
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: bundler
27
- requirement: &70203413318700 !ruby/object:Gem::Requirement
27
+ requirement: &70262082559640 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 1.0.0
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70203413318700
35
+ version_requirements: *70262082559640
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: jeweler
38
- requirement: &70203413317340 !ruby/object:Gem::Requirement
38
+ requirement: &70262082559180 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 1.6.4
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70203413317340
46
+ version_requirements: *70262082559180
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: bourne
49
- requirement: &70203413316500 !ruby/object:Gem::Requirement
49
+ requirement: &70262082558700 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - =
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '1.0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70203413316500
57
+ version_requirements: *70262082558700
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: mocha
60
- requirement: &70203419213280 !ruby/object:Gem::Requirement
60
+ requirement: &70262082558160 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - =
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: 0.9.8
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70203419213280
68
+ version_requirements: *70262082558160
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rails
71
- requirement: &70203419212740 !ruby/object:Gem::Requirement
71
+ requirement: &70262082557540 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: 3.0.0
77
77
  type: :runtime
78
78
  prerelease: false
79
- version_requirements: *70203419212740
79
+ version_requirements: *70262082557540
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: i18n
82
- requirement: &70203419212340 !ruby/object:Gem::Requirement
82
+ requirement: &70262082557000 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,7 +87,7 @@ dependencies:
87
87
  version: '0'
88
88
  type: :runtime
89
89
  prerelease: false
90
- version_requirements: *70203419212340
90
+ version_requirements: *70262082557000
91
91
  description: Objects on rails.
92
92
  email:
93
93
  - jamesgolick@gmail.com
@@ -118,6 +118,7 @@ files:
118
118
  - lib/objectify/rails/log_subscriber.rb
119
119
  - lib/objectify/rails/railtie.rb
120
120
  - lib/objectify/rails/renderer.rb
121
+ - lib/objectify/rails/routes.rb
121
122
  - lib/objectify/rails/routing.rb
122
123
  - lib/objectify/route.rb
123
124
  - lib/objectify/version.rb
@@ -135,6 +136,7 @@ files:
135
136
  - spec/route_spec.rb
136
137
  - spec/spec_helper.rb
137
138
  - thoughts/injector.md
139
+ - thoughts/responders.md
138
140
  homepage: https://github.com/bitlove/objectify
139
141
  licenses: []
140
142
  post_install_message: