hookercookerman-merb-resource-scope 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. data/LICENSE +20 -0
  2. data/README.textile +297 -0
  3. data/Rakefile +82 -0
  4. data/TODO +3 -0
  5. data/lib/dependencies.rb +1 -0
  6. data/lib/merb-resource-scope.rb +25 -0
  7. data/lib/merb-resource-scope/controller/actions.rb +67 -0
  8. data/lib/merb-resource-scope/controller/helpers.rb +252 -0
  9. data/lib/merb-resource-scope/controller/scoped_resource_mixin.rb +300 -0
  10. data/lib/merb-resource-scope/controller/singleton_actions.rb +25 -0
  11. data/lib/merb-resource-scope/merbtasks.rb +6 -0
  12. data/lib/merb-resource-scope/router/resource_specification.rb +144 -0
  13. data/lib/merb-resource-scope/specification.rb +36 -0
  14. data/spec/app.rb +7 -0
  15. data/spec/controller/_build_enclosing_scope_spec.rb +107 -0
  16. data/spec/controller/scope_resource_mixin_spec.rb +0 -0
  17. data/spec/integration/admin_posts_controller_spec.rb +158 -0
  18. data/spec/integration/campaign_post_comments_controller_spec.rb +158 -0
  19. data/spec/integration/campaign_posts_controller_spec.rb +73 -0
  20. data/spec/integration/campaigns_controller_spec.rb +43 -0
  21. data/spec/integration/images_spec.rb +17 -0
  22. data/spec/integration/myhome_controller_spec.rb +29 -0
  23. data/spec/integration/myhome_posts_comment_controller_spec.rb +44 -0
  24. data/spec/integration/myhome_posts_controller_spec.rb +55 -0
  25. data/spec/integration/profile_images_controller_spec.rb +61 -0
  26. data/spec/integration/tags_controller_spec.rb +34 -0
  27. data/spec/integration/user_posts_controller_spec.rb +71 -0
  28. data/spec/request_with_controller.rb +103 -0
  29. data/spec/spec_helper.rb +33 -0
  30. data/spec/specification_spec.rb +1 -0
  31. metadata +88 -0
@@ -0,0 +1,252 @@
1
+ module MerbResourceScope
2
+ module Controller
3
+ module Helpers
4
+
5
+ class UrlGenerator
6
+ attr_accessor :resources, :name_prefixes, :resources_with_specs, :named_route
7
+
8
+ def initialize(name_prefixes, resources_with_specs, &block)
9
+ @resources = []
10
+ @name_prefixes, @resources_with_specs = name_prefixes, resources_with_specs
11
+ yield self if block_given?
12
+ @named_route = generate_named_route
13
+ end
14
+
15
+ # this is so we can extend out current name_route
16
+ # will be used in congunction of the url helpers
17
+ # enclosing_resource_url{|route| route.add(:comments)}
18
+ #
19
+ # @param args<VarArgs> : this takes 3 parameter really, first one
20
+ #  is a symbol of the named_route you want to add, so lets
21
+ # say we had enclosing_resource_url of /users/1/
22
+ # and we wanted to add tags on to this url we can simple
23
+ # enclosing_resource_url{|route| route.add(:tags)}
24
+ # and this will be /users/1/tags
25
+ # You can also add custom routes as well then as well
26
+ # add(:tags, :pending)
27
+ # Also add a resource as well, add(:tag, @tag)
28
+ # Also do a new this way as well add(:tag, :new)
29
+ # And finally add(:tag, :edit, @tag) Where the @tag
30
+ # has to be the last option
31
+ #
32
+ # @examples
33
+ # enclosing_resource_url{|route| route.add(:comments)}
34
+ # resource_url(current_resource){|route| route.add(:tag, @tag)}
35
+ # enclosing_resource_url{|route| route.add(:post, :edit, @post)}
36
+ #
37
+ #
38
+ def add(*args)
39
+ name_prefixes.flatten!
40
+ name_prefixes << args.shift
41
+ args.each do |arg|
42
+ if arg.is_a?(Symbol)
43
+ name_prefixes.unshift(arg.to_s)
44
+ else
45
+ resources << arg
46
+ end
47
+ end
48
+ self
49
+ end
50
+
51
+ alias_method :with, :add
52
+
53
+ def generate_named_route
54
+ if resources_with_specs
55
+ none_singletons = resources_with_specs.reject{|rs| rs[1].singleton}
56
+ resources << none_singletons.map{|rs| rs[0]}
57
+ end
58
+ generated_named_route = name_prefixes.flatten.compact.join('_').intern
59
+ end
60
+ end
61
+
62
+ # TODO tidy refactor these methods pretty please
63
+ # if you want the current resources url then use this method its the way forward
64
+ # url is generated from building up a named route and then parsing the named
65
+ # route to the url method, so remember this url is buit using named routes!!
66
+ #
67
+ # so if we have a path of /users/1/posts/2
68
+ # the current_resources_url would be /users/1/posts
69
+ # remeber the url generated will be already scoped
70
+ # You can pass a custom route to generate a custom route
71
+ # and the params just like the url method
72
+ #
73
+ # @example
74
+ # current_resources_url(:pending, {:q => "cool"})
75
+ # current_resources_url(:notpending, {:lots => "yes", :lot => "ofparams"})
76
+ # current_resources_url
77
+ #
78
+ #
79
+ # @param<*args> : pass in a list of custom named routes, and param hash
80
+ #
81
+ # @return<String> : the generate url
82
+ def current_resources_url(*args, &block)
83
+ options = extract_options_from_args!(args) || {}
84
+ name_prefixes = []
85
+ name_prefixes << _current_specification.name_prefix
86
+ name_prefixes << Extlib::Inflection.pluralize(_current_specification.resource_name)
87
+ name_prefixes.unshift(args)
88
+ generator = UrlGenerator.new(name_prefixes, _enclosing_resources_with_spec, &block)
89
+ url(generator.named_route, *[generator.resources, options].flatten)
90
+ end
91
+ alias_method :resources_url, :current_resources_url
92
+
93
+
94
+ # generates a new resource url that is automatically scoped where you are
95
+ #
96
+ #
97
+ # lets just say we were at this url users/1/posts
98
+ # by using new_current_resource_url we would basically generate
99
+ # new_user_post named route
100
+ # which inturn would generate /users/1/posts/new
101
+ #
102
+ #
103
+ # @example
104
+ # new_current_resource_url({:awantaparam=> "yesyoucan"})
105
+ # new_current_resource_url
106
+ #
107
+ #
108
+ # @param options<Hash> : Pass in a hash of params
109
+ #
110
+ # @return<String> : the generate url
111
+ def new_current_resource_url(options = {}, &block)
112
+ name_prefixes = []
113
+ name_prefixes << _current_specification.name_prefix
114
+ name_prefixes << _current_specification.resource_name
115
+ name_prefixes.unshift("new")
116
+ generator = UrlGenerator.new(name_prefixes, _enclosing_resources_with_spec, &block)
117
+ url(generator.named_route, *[generator.resources, options].flatten)
118
+ end
119
+ alias_method :new_resource_url, :new_current_resource_url
120
+
121
+
122
+ # generates the current_resource_url cool
123
+ #
124
+ #
125
+ # lets just say we were at this url users/1/posts/1
126
+ # then via to using the current_resource_url we would do
127
+ # current_resource_url(@post) for singleton resources
128
+ # just pass in the resource_name ie. for /myhome/proile_image
129
+ # we would have current_resource_url(:myhome)
130
+ #
131
+ #
132
+ # @example
133
+ # current_resource_url(@post)
134
+ # current_resource_url(@post, :my => "params")
135
+ # current_resource_url(:myhome)
136
+ #
137
+ # @param options<Hash> : Pass in a hash of params
138
+ #
139
+ # @return<String> : the generate url
140
+ def current_resource_url(current_resource, *args, &block)
141
+ name_prefixes = []
142
+ options = extract_options_from_args!(args) || {}
143
+ resource_specs = _resources_with_specs.dup
144
+ name_prefixes << _current_specification.name_prefix
145
+ unless current_resource.is_a?(Symbol) && _current_specification.singleton
146
+ resource_specs << [current_resource, _current_specification]
147
+ name_prefixes << _current_specification.resource_name
148
+ else
149
+ name_prefixes << current_resource
150
+ end
151
+ name_prefixes.unshift(args)
152
+ generator = UrlGenerator.new(name_prefixes, resource_specs, &block)
153
+ url(generator.named_route, *[generator.resources, options].flatten)
154
+ end
155
+ alias_method :resource_url, :current_resource_url
156
+
157
+
158
+
159
+ # generates the enclosing_resource_url cool
160
+ #
161
+ #
162
+ # lets just say we were at this url users/1/posts/1
163
+ # then via to using the current_resource_url we would do
164
+ # current_resource_url(@post) for singleton resources
165
+ # just pass in the resource_name ie. for /myhome/proile_image
166
+ # we would have current_resource_url(:myhome)
167
+ #
168
+ #
169
+ # @example
170
+ # enclosing_resource_url(:edit)
171
+ # enclosing_resource_url(:edit, :my => "param")
172
+ # enclosing_resource_url(:whatever)
173
+ # enclosing_resource_url(:my => "params")
174
+ # enclosing_resource_url(:myhome)
175
+ #
176
+ # @param args<Var> : Pass in custom route, then params basically
177
+ #
178
+ # @return<String> : the generate url
179
+ def enclosing_resource_url(*args, &block)
180
+ if _enclosing_specification
181
+ options = extract_options_from_args!(args) || {}
182
+ name_prefixes = []
183
+ name_prefixes << _enclosing_specification.name_prefix
184
+ name_prefixes << _enclosing_specification.resource_name
185
+ name_prefixes.unshift(args)
186
+ generator = UrlGenerator.new(name_prefixes, _resources_with_specs, &block)
187
+ url(generator.named_route, *[generator.resources, options].flatten)
188
+ end
189
+ end
190
+
191
+
192
+ # generates the enclosing_resources_url cool
193
+ #
194
+ #
195
+ # lets just say we were at this url users/1/posts/1
196
+ # then via to using the enclosing_resources_url
197
+ # we would get the url of /users
198
+ # which inturn would be using :users named_route
199
+ #
200
+ #
201
+ # @example
202
+ # enclosing_resources_url
203
+ # enclosing_resources_url(:pending)
204
+ # enclosing_resources_url(:pending, :my => "param")
205
+ # @param args<Var> : Pass in custom route, then params basically
206
+ #
207
+ # @return<String> : the generate url
208
+ def enclosing_resources_url(*args, &block)
209
+ if _enclosing_specification
210
+ options = extract_options_from_args!(args) || {}
211
+ name_prefixes = []
212
+ name_prefixes << _enclosing_specification.name_prefix
213
+ name_prefixes << Extlib::Inflection.pluralize(_enclosing_specification.resource_name)
214
+ name_prefixes.unshift(args)
215
+ generator = UrlGenerator.new(name_prefixes, _enclosing_resources_with_spec, &block)
216
+ url(generator.named_route, *[generator.resources, options].flatten)
217
+ end
218
+ end
219
+
220
+ # generates the new_enclosing_resource_url cool
221
+ #
222
+ #
223
+ # lets just say we were at this url users/1/posts/1
224
+ # then via to using the new_enclosing_resource_url
225
+ # we would get the url of /users/new
226
+ # which inturn would be using :new_user named_route
227
+ #
228
+ #
229
+ # @example
230
+ # new_enclosing_resource_url(:edit)
231
+ # new_enclosing_resource_url(:edit, :my => "param")
232
+ # new_enclosing_resource_url(:whatever)
233
+ # new_enclosing_resource_url(:my => "params")
234
+ # new_enclosing_resource_url(:myhome)
235
+ #
236
+ # @param args<Var> : Pass in custom route, then params basically
237
+ #
238
+ # @return<String> : the generate url
239
+ def new_enclosing_resource_url(options = {}, &block)
240
+ if _enclosing_specification
241
+ name_prefixes = []
242
+ name_prefixes << _enclosing_specification.name_prefix
243
+ name_prefixes << _enclosing_specification.resource_name
244
+ name_prefixes.unshift("new")
245
+ generator = UrlGenerator.new(name_prefixes, _enclosing_resources_with_spec, &block)
246
+ url(generator.named_route, *[generator.resources, options].flatten)
247
+ end
248
+ end
249
+
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,300 @@
1
+ module MerbResourceScope
2
+ module Controller
3
+
4
+ module ScopedResourceMixin
5
+ def self.included(base)
6
+ base.class_eval do
7
+ attr_accessor :_enclosing_scope, :_enclosing_specifications, :_resources_with_specs, :_enclosing_resources_with_spec, :_current_specification, :_enclosing_specification
8
+ attr_accessor :current_resources, :current_resource, :resource_scope
9
+ extend ScopedResourceMixin::ClassMethods
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def build_resource_scope(options = {})
15
+ include InstanceMethods
16
+ options.only(:build_scope, :singleton, :actions)
17
+ filter_options = options.delete(:build_scope)
18
+ add_filter(_before_filters, :_build_resource_scope, filter_options || {})
19
+
20
+ unless options[:actions] == false
21
+ actions ||= options[:singleton] ? Merb::Plugins.config[:merb_resource_scope][:singleton_actions] : Merb::Plugins.config[:merb_resource_scope][:actions]
22
+ include_actions actions, options.delete(:actions) || {}
23
+ end
24
+ end
25
+
26
+ private
27
+ # we only want to include actions that we want!
28
+ def include_actions(mixin, options = {})
29
+ mixin = mixin.dup
30
+ if only = options[:only]
31
+ only = Array(options[:only])
32
+ mixin.instance_methods.each {|m| mixin.send(:undef_method, m) unless only.include?(m.intern)}
33
+ elsif except = options[:exclude]
34
+ except = Array(options[:exclude])
35
+ mixin.instance_methods.each {|m| mixin.send(:undef_method, m) if except.include?(m.intern)}
36
+ end
37
+ include mixin
38
+ end
39
+ end
40
+
41
+ # Proxy class to provide a consistent API for resource_scope. passing all method calls
42
+ # to the current_scope
43
+ class ResourceScope
44
+ instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class kind_of? respond_to? should should_not instance_variable_set instance_variable_get instance_eval].include?(m) }
45
+ attr_reader :controller
46
+
47
+ def initialize(controller)
48
+ @controller = controller
49
+ end
50
+
51
+ def _enclosing_resource
52
+ @_enclosing_resource ||= controller.enclosing_resource
53
+ end
54
+
55
+ def _current_specification
56
+ @_current_specification ||= controller._current_specification
57
+ end
58
+
59
+ def respond_to?(method)
60
+ super || current_scope.respond_to?(method)
61
+ end
62
+
63
+ def current_scope
64
+ @current_scope ||= _enclosing_resource ? _enclosing_resource.send(_current_specification.association_method) : _current_specification.klass
65
+ end
66
+
67
+ def method_missing(method, *args, &block)
68
+ if current_scope.respond_to?(method)
69
+ current_scope.send(method, *args, &block)
70
+ else
71
+ super
72
+ end
73
+ end
74
+ end
75
+
76
+ module InstanceMethods
77
+
78
+ # the before filter, when this before filter is run; it will build the resource_scope
79
+ def _build_resource_scope
80
+ if specifications = request.resource_specifications
81
+ @_current_specification = specifications.pop
82
+ @_enclosing_specifications = specifications
83
+ @_resources_with_specs = []
84
+
85
+ # if we dont want to load all the enclosing resource and resources
86
+ # we can specify a depth to which build the the scope from
87
+ if scope_depth = @_current_specification.scope_depth
88
+ @_enclosing_specifications.slice!(0..scope_depth-1)
89
+ end
90
+
91
+ _generate_enclosing_scope
92
+ @resource_scope ||= MerbResourceScope::Controller::ScopedResourceMixin::ResourceScope.new(self)
93
+ end
94
+ end
95
+
96
+ def enclosing_resource
97
+ return nil if @_enclosing_specifications.size == 0
98
+ if @_resources_with_specs.size == @_enclosing_specifications.size+1
99
+ @_resources_with_specs.slice(-2)[0]
100
+ else
101
+ @_resources_with_specs.last && @_resources_with_specs.last[0]
102
+ end
103
+ end
104
+
105
+ # When we find the current_resource we want its resource with its specification
106
+ # to be added; essentially so we can know what the correct enclosing resource is
107
+ # but we dont want to add it if its a new_record as its not a resource just yet
108
+ # TODO need looking AT basically!! as create and destroy actions show not hold
109
+ # the resource OR put into the resource as they are essentiall not correct
110
+ # as it should be current_resource_url(@new_resource) Basically YEAH!
111
+
112
+ # if you want to find resources in another way just override this in your
113
+ # controller;
114
+ #
115
+ # @example
116
+ # def find_resources
117
+ # @resource_scope.all :conditions => {:pending => true}
118
+ # end
119
+ #
120
+ # @api overwritable
121
+ def find_resources
122
+ @resource_scope.all
123
+ end
124
+
125
+ # if you want to find a resource in another then this default way
126
+ # then you can override in the controller,
127
+ #
128
+ # @example
129
+ # def find_resource
130
+ # @resource_scope.first :conditions => {:user_id => 1, :pending => false}
131
+ # end
132
+ #
133
+ # @param id<String>|<Integer> the params to use to find a resource could be permalink
134
+ #
135
+ # @api overwritable
136
+ def find_resource(id = params[@_current_specification.permalink])
137
+ if find_method = @_current_specification.find_method
138
+ find_method.is_a?(Proc) ? self.instance_eval(&find_method) : self.send(find_method)
139
+ else
140
+ @_current_specification.singleton ? @resource_scope : @resource_scope.first(:conditions => {@_current_specification.permalink => id})
141
+ end
142
+ end
143
+
144
+ # if you have other requirments on how you would like to create a new resource
145
+ # then you can override this at the controller level, using the @resource_scope
146
+ # Also if you wish to have different attributes set you can just add them as well
147
+ # dont really want to go into it but I cannot use the proxy here as a send then a
148
+ # method call basically stops the dependent attributes from being set! strange but
149
+ # true
150
+ # @example
151
+ # def new_resource
152
+ # @resource_scope.different_new {:whatever => :egg}
153
+ # end
154
+ #
155
+ # @api overwritable
156
+ def new_resource(attributes = (params[@_current_specification.resource_name] || {}))
157
+ if enclosing_resource
158
+ if @_current_specification.singleton
159
+ return @_current_specification.klass.new attributes.merge!(_enclosing_specification.association_method.to_sym => enclosing_resource)
160
+ end
161
+ enclosing_resource.send(@_current_specification.association_method).build attributes
162
+ else
163
+ @_current_specification.klass.new attributes
164
+ end
165
+ end
166
+ #
167
+ # we set the current_resource to have the current specification resource_name method
168
+ def current_resource=(resource)
169
+ instance_variable_set("@#{@_current_specification.resource_name}", resource)
170
+ @current_resource = resource
171
+ end
172
+
173
+ # we set the current_resource to have the current specification resource_name method
174
+ def current_resources=(resources)
175
+ instance_variable_set("@#{@_current_specification.association_method}", resources)
176
+ @current_resources = resources
177
+ end
178
+
179
+ private
180
+ # this is the work horse of how the enclosing_scope is built,
181
+ # it will use all the enclosing enclosing_specifications to work out what the
182
+ # enclosing scope is.
183
+ #
184
+ # On its first run it will determine whether
185
+ # it should just use the singleton method or the find_method of the specification,
186
+ # or just go with the standand User.get(params[:id])
187
+ #
188
+ # Futher iterations will just try to find the resource via method calls
189
+ # and if the key of the specification is in the params then that resource
190
+ # will also be found
191
+ #
192
+ # so for a typical url say users/1/posts/1/comments
193
+ # it would go something like this, setting the enclosing_scope as it goes
194
+ # 1. enclosing_scope = User - via enclosing_specifications.first.klass_name
195
+ # 2. enclosing_scope = User.get(params[:user_id]) - via collect_resource_via_params(@enclosing_scope, specification)
196
+ # 3. enclosing_scope = User.get(params[:user_id]).posts.get(params[:post_id]) - via collect_resource_via_params(@enclosing_scope, specification)
197
+ # 4. thats it! now our enclosing_scope is where we want it
198
+ #
199
+ # This will also handle enclosing depth witch can default to the number of
200
+ # enclosing specification but can get it from current
201
+ # specification and if its been set to say 1 then my inclosing_specifications
202
+ # will only be set to just that COOL
203
+ def _generate_enclosing_scope
204
+ @_enclosing_specifications.each_with_index do |specification, index|
205
+ if specification.singleton || specification.find_method
206
+ @_enclosing_scope = _collect_resource_via_method(@_enclosing_scope, specification)
207
+ else
208
+ @_enclosing_scope = _collect_resource_via_params(@_enclosing_scope, specification)
209
+ end
210
+ end
211
+ end
212
+
213
+ # collect a resource either with the specification.find_method or the association method
214
+ # we also want to store the found resource or resources along with the specification
215
+ # that was used to find them for later use; mainly in route generation
216
+ #
217
+ # @param enclosing_scope<Object>|<Objects> aka ur orm object
218
+ # @param specification<Specification>
219
+ #
220
+ # @return <Object>|<Objects> aka ur orm object
221
+ def _collect_resource_via_method(enclosing_scope, specification)
222
+ if specification.find_method
223
+ return _collect_resource_via_find_method(specification)
224
+ else
225
+ resource_or_resources = enclosing_scope.send(specification.association_method)
226
+ @_resources_with_specs << [resource_or_resources, specification]
227
+ instance_variable_set("@#{specification.association_method}", resource_or_resources)
228
+ end
229
+ resource_or_resources
230
+ end
231
+
232
+
233
+ # collect a resource either with the specification.find_method
234
+ # we also want to store the found resource or resources along with the specification
235
+ # that was used to find them for later use; mainly in route generation and to find
236
+ # enclosing resources or resource
237
+ #
238
+ # @param specification<Specification>
239
+ #
240
+ # @return <Object>|<Objects> aka ur orm object
241
+ def _collect_resource_via_find_method(specification)
242
+ resource_or_resources = if specification.find_method.is_a?(Proc)
243
+ self.instance_eval(&specification.find_method)
244
+ else
245
+ self.send(specification.find_method)
246
+ end
247
+
248
+ unless _resource_is_a_collection?(specification, resource_or_resources)
249
+ @_resources_with_specs << [resource_or_resources, specification]
250
+ instance_variable_set("@#{specification.resource_name}", resource_or_resources)
251
+ end
252
+
253
+ resource_or_resources
254
+ end
255
+
256
+ # collect a resource with the specification using the params and the resource_name
257
+ # we also want to store the found resource along with the specification
258
+ #
259
+ # @param enclosing_scope<Object>|<Objects> aka ur orm object
260
+ # @param specification<Specification>
261
+ #
262
+ # @return <Object>|<Objects>|enclosing_scope aka ur orm object
263
+ def _collect_resource_via_params(enclosing_scope, specification)
264
+ if !params.keys.include?(specification.foreign_key.to_s)
265
+ return enclosing_scope
266
+ else
267
+
268
+ resource = if enclosing_scope
269
+ enclosing_scope.send(specification.association_method).first(:conditions => {specification.permalink => params[specification.foreign_key]})
270
+ else
271
+ specification.klass.first(:conditions => {specification.permalink => params[specification.foreign_key]})
272
+ end
273
+
274
+ @_resources_with_specs << [resource, specification]
275
+ instance_variable_set("@#{specification.resource_name}", resource)
276
+ resource
277
+ end
278
+ end
279
+
280
+ # This is by default set to use datamapper
281
+ # You can override this at the application controller level
282
+ # then it will use this for all controllers; or if you have
283
+ # specific requirement on a controller;then just change it in there
284
+ #
285
+ # @api overwritable
286
+ def _resource_is_a_collection?(specification, resource_or_resources)
287
+ resource_or_resources.kind_of?(::DataMapper::Collection)
288
+ end
289
+
290
+ def _enclosing_resources_with_specs
291
+ @_resources_with_specs.slice(0..-2)
292
+ end
293
+
294
+ def _enclosing_specification
295
+ @_enclosing_specifications.last
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end