hookercookerman-merb-resource-scope 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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