benhoskings-hammock 0.2.5 → 0.2.6

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.
@@ -1,3 +1,32 @@
1
+ == 0.2.6 2009-03-05
2
+
3
+ Access current_nested_records in nested_route_for through the method instead of directly.
4
+ Replaced #assign_nestable_resources with #assign_nesting_entities, to separately kick and assign an ivar for each of the current rou
5
+ Added nesting_scope_list to encapsulate what @hammock_nesting_scopes was set to before.
6
+ Updated nested_within? to use methods instead of ivars, removed duping from current_nested_[record,resource]s, and added add_nested_en
7
+ Updated empty_scope for sqlite, to bring it in line with public_scope.
8
+ Updated current_scope to fail gracefully if current_verb_scope (renamed from verb_scope along with [current_]nest_scope) returns nil.
9
+ Replaced RouteNode#nesting_scope_for with nesting_scope_list_for, and changed nest_scope call appropriately. The scopes are returned s
10
+ Replaced AR::Base#id_or_description with #description, added .description, and use in link_class_for for more consistent class names.
11
+ Split Hammock::Callback (previously HammockCallback) out into its own file.
12
+ Split RouteNode and RouteStep (previously HammockResource and HammockRoutePiece) out into their own files.
13
+ Reject Hammock:: classes from hammock load. Classes are modules. Who knew?
14
+ Changed inheritable_attribute keys from strings to symbols.
15
+ Caching the model within HammockResource was causing stale class fails in development mode.
16
+ retrieve_record uses mdl.routing_attribute now instead of the old find_column_name.
17
+ Default routing_attribute to :id.
18
+ Fixed the 'no valid AR reflections' message to be a user error instead of an internal hammock error (they just need to add a reflection)
19
+ HammockRoutePiece erroneously had a root? check instead of a parent.nil? check - fixed.
20
+ Only set creator_id if someone is logged in.
21
+ Updated #nest_scope to use #nesting_scope_for instead of manually recursively selecting within a single context, and updated #curren
22
+ Re-enabled logging of Ambition queries as they are assigned as entities.
23
+ Replaced the questionable #current_route with #current_hammock_resource, which uses HammockResource#base_for.
24
+ Added HammockResource#base_for, #nesting_scope_for and #nesting_scope_segment_for.
25
+ Added HammockResource#determine_routing_parent, to determine which ActiveRecord reflection to use to generate joins for route queryi
26
+ Added Hash#selekt, to sidestep the ruby-1.8/1.9 #select changes.
27
+ Commented current_route-dependent code in hamlink_to until that is sorted out.
28
+ Test if obj is defined before attempting to select .spinner within it.
29
+
1
30
  == 0.2.4 2009-02-25
2
31
 
3
32
  * Initial gem release
@@ -6,6 +6,7 @@ Rakefile
6
6
  hammock.gemspec
7
7
  lib/hammock.rb
8
8
  lib/hammock/ajaxinate.rb
9
+ lib/hammock/callback.rb
9
10
  lib/hammock/callbacks.rb
10
11
  lib/hammock/canned_scopes.rb
11
12
  lib/hammock/constants.rb
@@ -34,6 +35,8 @@ lib/hammock/restful_rendering.rb
34
35
  lib/hammock/restful_support.rb
35
36
  lib/hammock/route_drawing_hooks.rb
36
37
  lib/hammock/route_for.rb
38
+ lib/hammock/route_node.rb
39
+ lib/hammock/route_step.rb
37
40
  lib/hammock/scope.rb
38
41
  lib/hammock/suggest.rb
39
42
  lib/hammock/utils.rb
@@ -2,15 +2,15 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{hammock}
5
- s.version = "0.2.5"
5
+ s.version = "0.2.6"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Ben Hoskings"]
9
- s.date = %q{2009-02-27}
9
+ s.date = %q{2009-03-05}
10
10
  s.description = %q{Hammock is a Rails plugin that eliminates redundant code in a very RESTful manner. It does this in lots in lots of different places, but in one manner: it encourages specification in place of implementation. Hammock enforces RESTful resource access by abstracting actions away from the controller in favour of a clean, model-like callback system. Hammock tackles the hard and soft sides of security at once with a scoping security system on your models. Specify who can verb what resources under what conditions once, and everything else - the actual security, link generation, index filtering - just happens. Hammock inspects your routes and resources to generate a routing tree for each resource. Parent resources in a nested route are handled transparently at every point - record retrieval, creation, and linking. It makes more sense when you see how it works though, so check out the screencast!}
11
11
  s.email = ["ben@hoskings.net"]
12
12
  s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.rdoc", "misc/scaffold.txt"]
13
- s.files = ["History.txt", "LICENSE", "Manifest.txt", "README.rdoc", "Rakefile", "hammock.gemspec", "lib/hammock.rb", "lib/hammock/ajaxinate.rb", "lib/hammock/callbacks.rb", "lib/hammock/canned_scopes.rb", "lib/hammock/constants.rb", "lib/hammock/controller_attributes.rb", "lib/hammock/export_scope.rb", "lib/hammock/hamlink_to.rb", "lib/hammock/javascript_buffer.rb", "lib/hammock/logging.rb", "lib/hammock/model_attributes.rb", "lib/hammock/model_logging.rb", "lib/hammock/monkey_patches/action_pack.rb", "lib/hammock/monkey_patches/active_record.rb", "lib/hammock/monkey_patches/array.rb", "lib/hammock/monkey_patches/hash.rb", "lib/hammock/monkey_patches/logger.rb", "lib/hammock/monkey_patches/module.rb", "lib/hammock/monkey_patches/numeric.rb", "lib/hammock/monkey_patches/object.rb", "lib/hammock/monkey_patches/route_set.rb", "lib/hammock/monkey_patches/string.rb", "lib/hammock/overrides.rb", "lib/hammock/resource_mapping_hooks.rb", "lib/hammock/resource_retrieval.rb", "lib/hammock/restful_actions.rb", "lib/hammock/restful_rendering.rb", "lib/hammock/restful_support.rb", "lib/hammock/route_drawing_hooks.rb", "lib/hammock/route_for.rb", "lib/hammock/scope.rb", "lib/hammock/suggest.rb", "lib/hammock/utils.rb", "misc/scaffold.txt", "misc/template.rb", "tasks/hammock_tasks.rake", "test/hammock_test.rb", "test/rails_root/test/test_helper.rb"]
13
+ s.files = ["History.txt", "LICENSE", "Manifest.txt", "README.rdoc", "Rakefile", "hammock.gemspec", "lib/hammock.rb", "lib/hammock/ajaxinate.rb", "lib/hammock/callback.rb", "lib/hammock/callbacks.rb", "lib/hammock/canned_scopes.rb", "lib/hammock/constants.rb", "lib/hammock/controller_attributes.rb", "lib/hammock/export_scope.rb", "lib/hammock/hamlink_to.rb", "lib/hammock/javascript_buffer.rb", "lib/hammock/logging.rb", "lib/hammock/model_attributes.rb", "lib/hammock/model_logging.rb", "lib/hammock/monkey_patches/action_pack.rb", "lib/hammock/monkey_patches/active_record.rb", "lib/hammock/monkey_patches/array.rb", "lib/hammock/monkey_patches/hash.rb", "lib/hammock/monkey_patches/logger.rb", "lib/hammock/monkey_patches/module.rb", "lib/hammock/monkey_patches/numeric.rb", "lib/hammock/monkey_patches/object.rb", "lib/hammock/monkey_patches/route_set.rb", "lib/hammock/monkey_patches/string.rb", "lib/hammock/overrides.rb", "lib/hammock/resource_mapping_hooks.rb", "lib/hammock/resource_retrieval.rb", "lib/hammock/restful_actions.rb", "lib/hammock/restful_rendering.rb", "lib/hammock/restful_support.rb", "lib/hammock/route_drawing_hooks.rb", "lib/hammock/route_for.rb", "lib/hammock/route_node.rb", "lib/hammock/route_step.rb", "lib/hammock/scope.rb", "lib/hammock/suggest.rb", "lib/hammock/utils.rb", "misc/scaffold.txt", "misc/template.rb", "tasks/hammock_tasks.rake", "test/hammock_test.rb"]
14
14
  s.has_rdoc = true
15
15
  s.homepage = %q{http://github.com/benhoskings/hammock}
16
16
  s.rdoc_options = ["--main", "README.rdoc"]
@@ -18,7 +18,6 @@ Gem::Specification.new do |s|
18
18
  s.rubyforge_project = %q{hammock}
19
19
  s.rubygems_version = %q{1.3.1}
20
20
  s.summary = %q{Hammock is a Rails plugin that eliminates redundant code in a very RESTful manner}
21
- s.test_files = ["test/rails_root/test/test_helper.rb"]
22
21
 
23
22
  if s.respond_to? :specification_version then
24
23
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
@@ -8,13 +8,13 @@ Dir.glob("#{File.dirname __FILE__}/hammock/**/*.rb").each {|dep|
8
8
  } if defined?(RAILS_ROOT) # Loading Hammock components under 'rake package' fails.
9
9
 
10
10
  module Hammock
11
- VERSION = '0.2.5'
11
+ VERSION = '0.2.6'
12
12
 
13
13
  def self.included base # :nodoc:
14
14
  Hammock.constants.map {|constant_name|
15
15
  Hammock.const_get constant_name
16
16
  }.select {|constant|
17
- constant.is_a? Module
17
+ constant.is_a?(Module) && !constant.is_a?(Class)
18
18
  }.partition {|mod|
19
19
  mod.constants.include?('LoadFirst') && mod::LoadFirst
20
20
  }.flatten.each {|mod|
@@ -133,7 +133,7 @@ module Hammock
133
133
  private
134
134
 
135
135
  def link_class_for verb, record, attribute = nil
136
- [verb, record.base_model, record.id, attribute].compact.join('_')
136
+ [verb, record.description, attribute].compact.join('_')
137
137
  end
138
138
 
139
139
  def clean_snippet snippet
@@ -0,0 +1,16 @@
1
+ module Hammock
2
+
3
+ class Callback < ActiveSupport::Callbacks::Callback
4
+ private
5
+
6
+ def evaluate_method method, *args, &block
7
+ if method.is_a? Proc
8
+ # puts "was a Hammock::Callback proc within #{args.first.class}."
9
+ method.bind(args.shift).call(*args, &block)
10
+ else
11
+ super
12
+ end
13
+ end
14
+ end
15
+
16
+ end
@@ -28,19 +28,6 @@ module Hammock
28
28
 
29
29
  module ClassMethods
30
30
 
31
- class HammockCallback < ActiveSupport::Callbacks::Callback
32
- private
33
-
34
- def evaluate_method method, *args, &block
35
- if method.is_a? Proc
36
- # puts "was a HammockCallback proc within #{args.first.class}."
37
- method.bind(args.shift).call(*args, &block)
38
- else
39
- super
40
- end
41
- end
42
- end
43
-
44
31
  def define_hammock_callbacks *callbacks
45
32
  callbacks.each do |callback|
46
33
  class_eval <<-"end_eval"
@@ -54,7 +41,7 @@ module Hammock
54
41
  raise ArgumentError, "Inline callback definitions require a description as their sole argument."
55
42
  else
56
43
  # logger.info "defining \#{methods.first} on \#{name} with method \#{block.inspect}."
57
- [HammockCallback.new(:#{callback}, block, :identifier => methods.first)]
44
+ [Hammock::Callback.new(:#{callback}, block, :identifier => methods.first)]
58
45
  end || []
59
46
  end
60
47
  # log callbacks
@@ -109,7 +109,11 @@ module Hammock
109
109
  end
110
110
 
111
111
  def empty_scope
112
- lambda {|record| false }
112
+ if sqlite?
113
+ lambda {|record| 0 } # TODO check what this should be
114
+ else
115
+ lambda {|record| false }
116
+ end
113
117
  end
114
118
 
115
119
  end
@@ -1,4 +1,6 @@
1
1
  module Hammock
2
+ PleaseFileABug = "Ack, that shouldn't have happened. Please email the full stacktrace to <ben@hoskings.net>, so the problem you uncovered can be addressed. Thanks!"
3
+
2
4
  module Constants
3
5
 
4
6
  ImpliedUnsafeActions = [:new, :edit, :destroy]
@@ -31,15 +31,6 @@ module Hammock
31
31
  def find_on_create
32
32
  write_inheritable_attribute :find_on_create, true
33
33
  end
34
-
35
- # Use +find_column+ to specify the name of an alternate column with which record lookups should be performed.
36
- #
37
- # This is useful for controllers that are indexed by primary key, but are accessed with URLs containing some other unique attribute of the resource, like a randomly-generated key.
38
- # find_column :key
39
- def find_column column_name
40
- # TODO define to_param on model.
41
- write_inheritable_attribute :find_column, column_name
42
- end
43
34
  end
44
35
 
45
36
  module InstanceMethods
@@ -57,10 +48,6 @@ module Hammock
57
48
  def findable_on_create?
58
49
  self.class.read_inheritable_attribute :find_on_create
59
50
  end
60
-
61
- def find_column_name
62
- self.class.read_inheritable_attribute(:find_column) || :id
63
- end
64
51
  end
65
52
  end
66
53
  end
@@ -9,24 +9,39 @@ module Hammock
9
9
 
10
10
  module ClassMethods
11
11
 
12
+ def route_by attribute
13
+ write_inheritable_attribute :routing_attribute, attribute.to_sym
14
+ define_method :to_param do
15
+ send self.class.routing_attribute
16
+ end
17
+ end
18
+ def nest_within *attributes
19
+ write_inheritable_attribute :nestable_routing_resources, attributes
20
+ end
12
21
  def has_defaults attrs
13
- write_inheritable_attribute "default_attributes", (default_attributes || {}).merge(attrs)
22
+ write_inheritable_attribute :default_attributes, (default_attributes || {}).merge(attrs)
14
23
  end
15
24
  def attr_accessible_on_create *attributes
16
- write_inheritable_attribute "attr_accessible_on_create", Set.new(attributes.map(&:to_s)) + (accessible_attributes_on_create || [])
25
+ write_inheritable_attribute :attr_accessible_on_create, Set.new(attributes.map(&:to_s)) + (accessible_attributes_on_create || [])
17
26
  end
18
27
  def attr_accessible_on_update *attributes
19
- write_inheritable_attribute "attr_accessible_on_update", Set.new(attributes.map(&:to_s)) + (accessible_attributes_on_update || [])
28
+ write_inheritable_attribute :attr_accessible_on_update, Set.new(attributes.map(&:to_s)) + (accessible_attributes_on_update || [])
20
29
  end
21
30
 
31
+ def routing_attribute
32
+ read_inheritable_attribute(:routing_attribute) || :id
33
+ end
34
+ def nestable_routing_resources
35
+ read_inheritable_attribute(:nestable_routing_resources)
36
+ end
22
37
  def default_attributes
23
- read_inheritable_attribute("default_attributes") || {}
38
+ read_inheritable_attribute(:default_attributes) || {}
24
39
  end
25
40
  def accessible_attributes_on_create
26
- read_inheritable_attribute "attr_accessible_on_create"
41
+ read_inheritable_attribute :attr_accessible_on_create
27
42
  end
28
43
  def accessible_attributes_on_update
29
- read_inheritable_attribute "attr_accessible_on_update"
44
+ read_inheritable_attribute :attr_accessible_on_update
30
45
  end
31
46
 
32
47
  end
@@ -39,10 +39,18 @@ module Hammock
39
39
  base_class.to_s.pluralize.underscore
40
40
  end
41
41
 
42
+ def param_key
43
+ "#{base_model}_id"
44
+ end
45
+
42
46
  def base_model
43
47
  base_class.to_s.underscore
44
48
  end
45
49
 
50
+ def description
51
+ base_model
52
+ end
53
+
46
54
  def record?; false end
47
55
  def resource?; true end
48
56
 
@@ -122,12 +130,8 @@ module Hammock
122
130
  end
123
131
  end
124
132
 
125
- def id_or_description
126
- new_record? ? new_record_description : id
127
- end
128
-
129
- def new_record_description
130
- attributes.map {|k,v| "#{k}-#{(v.to_s || '')[0..10]}" }.join("_")
133
+ def description
134
+ new_record? ? "new_#{base_model}" : "#{base_model}_#{id}"
131
135
  end
132
136
 
133
137
  def base_model
@@ -21,6 +21,14 @@ module Hammock
21
21
  dup.discard! *keys
22
22
  end
23
23
 
24
+ def selekt &block
25
+ hsh = {}
26
+ each_pair {|k,v|
27
+ hsh[k] = v if yield(k,v)
28
+ }
29
+ hsh
30
+ end
31
+
24
32
  def dragnet *keys
25
33
  dup.dragnet! *keys
26
34
  end
@@ -20,179 +20,7 @@ module Hammock
20
20
  private
21
21
 
22
22
  def initialize_hammock_route_map
23
- self.route_map = HammockResource.new
24
- end
25
-
26
- class HammockResource < ActionController::Resources::Resource
27
- class HammockRoutePiece
28
- attr_reader :resource, :routeable_as, :verb, :entity, :parent
29
-
30
- def initialize resource
31
- @resource = resource
32
- end
33
-
34
- def for verb, entity
35
- routeable_as = resource.routeable_as(verb, entity)
36
-
37
- if !routeable_as
38
- raise "The verb '#{verb}' can't be applied to " + (entity.record? ? "#{entity.resource} records" : "the #{entity.resource} resource") + "."
39
- elsif (:record == routeable_as) && entity.new_record?
40
- raise "The verb '#{verb}' requires a #{entity.resource} with an ID (i.e. not a new record)."
41
- elsif (:build == routeable_as) && entity.record? && !entity.new_record?
42
- raise "The verb '#{verb}' requires either the #{entity.resource} resource, or a #{entity.resource} without an ID (i.e. a new record)."
43
- else
44
- @verb, @entity, @routeable_as = verb, entity, routeable_as
45
- end
46
-
47
- self
48
- end
49
-
50
- def within parent
51
- @parent = parent
52
- self
53
- end
54
-
55
- def setup?
56
- !@entity.nil?
57
- end
58
-
59
- def path params = nil
60
- raise_unless_setup_while_trying_to 'render a path'
61
-
62
- buf = '/'
63
- buf << entity.resource_name
64
- buf << '/' + entity.to_param if entity.record? && !entity.new_record?
65
- buf << '/' + verb.to_s unless verb.nil? or implied_verb?(verb)
66
-
67
- buf = parent.path + buf unless parent.nil?
68
- buf << param_str(params)
69
-
70
- buf
71
- end
72
-
73
- def http_method
74
- raise_unless_setup_while_trying_to 'extract the HTTP method'
75
- resource.send("#{routeable_as}_routes")[verb]
76
- end
77
-
78
- def fake_http_method
79
- http_method.in?(:get, :post) ? http_method : :post
80
- end
81
-
82
- def get?; :get == http_method end
83
- def post?; :post == http_method end
84
- def put?; :put == http_method end
85
- def delete?; :delete == http_method end
86
-
87
- def safe?
88
- get? && !verb.in?(Hammock::Constants::ImpliedUnsafeActions)
89
- end
90
-
91
- private
92
-
93
- def implied_verb? verb
94
- verb.in? :index, :create, :show, :update, :destroy
95
- end
96
-
97
- def raise_unless_setup_while_trying_to task
98
- raise "You have to call for(verb, entity) (and optionally within(parent)) on this HammockRoutePiece before you can #{task}." unless setup?
99
- end
100
-
101
- def param_str params
102
- link_params = entity.record? ? entity.unsaved_attributes.merge(params || {}) : params
103
-
104
- if link_params.blank?
105
- ''
106
- else
107
- '?' + {entity.base_model => link_params}.to_query
108
- end
109
- end
110
-
111
- end
112
-
113
- DefaultRecordVerbs = {
114
- :show => :get,
115
- :edit => :get,
116
- :update => :put,
117
- :destroy => :delete
118
- }.freeze
119
- DefaultResourceVerbs = {
120
- :index => :get
121
- }.freeze
122
- DefaultBuildVerbs = {
123
- :new => :get,
124
- :create => :post
125
- }.freeze
126
-
127
- attr_reader :mdl, :parent, :children, :record_routes, :resource_routes, :build_routes
128
-
129
- def initialize entity = nil, options = {}
130
- @mdl = entity if entity.is_a?(Symbol)
131
- @parent = options[:parent]
132
- @children = {}
133
- define_routes options
134
- end
135
-
136
- def ancestry
137
- parent.nil? ? [] : parent.ancestry.push(self)
138
- end
139
-
140
- def for verb, entities, options
141
- raise "HammockResource#for requires an explicitly specified verb as its first argument." unless verb.is_a?(Symbol)
142
- raise "You have to supply at least one record or resource." if entities.empty?
143
-
144
- entity = entities.shift
145
-
146
- if entities.empty?
147
- piece_for verb, entity
148
- else
149
- children[entity.resource_sym].for(verb, entities, options).within piece_for(nil, entity)
150
- end
151
- end
152
-
153
- def add entity, options, steps = nil
154
- if steps.nil?
155
- add entity, options, (options[:name_prefix] || '').chomp('_').split('_').map {|i| i.pluralize.underscore.to_sym }
156
- elsif steps.empty?
157
- add_child entity, options
158
- else
159
- children[steps.shift].add entity, options, steps
160
- end
161
- end
162
-
163
- def routeable_as verb, entity
164
- if entity.record? && record_routes[verb || :show]
165
- :record
166
- elsif entity.resource? && resource_routes[verb || :index]
167
- :resource
168
- elsif !verb.nil? && build_routes[verb]
169
- :build
170
- end
171
- end
172
-
173
- private
174
-
175
- def define_routes options
176
- @record_routes = DefaultRecordVerbs.dup.update(options[:member] || {})
177
- @resource_routes = DefaultResourceVerbs.dup.update(options[:collection] || {})
178
- @build_routes = DefaultBuildVerbs.dup.update(options[:build] || {})
179
- end
180
-
181
- def add_child entity, options
182
- child = HammockResource.new entity, options.merge(:parent => self)
183
- children[child.mdl] = child
184
- end
185
-
186
- def piece_for verb, entity
187
- child = children[entity.resource_sym]
188
-
189
- if child.nil?
190
- raise "No routes are defined for #{entity.resource}#{' within ' + ancestry.map {|r| r.mdl.to_s }.join(', ') unless ancestry.empty?}."
191
- else
192
- HammockRoutePiece.new(child).for(verb, entity)
193
- end
194
- end
195
-
23
+ self.route_map = Hammock::RouteNode.new
196
24
  end
197
25
 
198
26
  end
@@ -15,10 +15,10 @@ module Hammock
15
15
  find_record :find_with_deleted
16
16
  end
17
17
 
18
- def find_record finder = :find
18
+ def find_record
19
19
  result = if !callback(:before_find)
20
20
  # callbacks failed
21
- elsif (record = retrieve_record(finder)).nil?
21
+ elsif (record = retrieve_record).nil?
22
22
  log "#{mdl}<#{params[:id]}> doesn't exist within #{requester_name.possessive} #{action_name} scope."
23
23
  :not_found
24
24
  elsif :ok != (verbability = can_verb_record?(action_name.to_sym, record))
@@ -44,11 +44,11 @@ module Hammock
44
44
  end
45
45
  end
46
46
 
47
- def retrieve_record finder
47
+ def retrieve_record
48
48
  if (scope = current_scope).nil?
49
49
 
50
50
  else
51
- record = scope.send finder, :first, :conditions => {find_column_name => params[:id]}
51
+ record = scope.send :find, :first, :conditions => {mdl.routing_attribute => params[:id]}
52
52
  record || required_callback(:after_failed_find)
53
53
  end
54
54
  end
@@ -142,7 +142,7 @@ module Hammock
142
142
 
143
143
  def find_record_on_create
144
144
  if findable_on_create?
145
- if record = nest_scope.find(:first, :conditions => params_for(mdl.symbolize))
145
+ if record = current_nest_scope.find(:first, :conditions => params_for(mdl.symbolize))
146
146
  log "suitable record already exists: #{record}"
147
147
  assign_entity record
148
148
  else
@@ -29,8 +29,10 @@ module Hammock
29
29
  @hammock_cached_mdl_name ||= self.class.to_s.sub('Controller', '').singularize.underscore
30
30
  end
31
31
 
32
- def current_route
33
- @hammock_cached_current_route ||= route_for(action_name.to_sym, *current_nested_records.push(@entity)) unless @entity.nil?
32
+ # Returns the node in the Hammock routing map corresponding to the (possibly nested) resource handling the current request.
33
+ def current_hammock_resource
34
+ nesting_resources = params.keys.select {|k| /_id$/ =~ k }.map {|k| k.gsub(/_id$/, '').pluralize }
35
+ ActionController::Routing::Routes.route_map.base_for nesting_resources.push(mdl.resource_name).map(&:to_sym)
34
36
  end
35
37
 
36
38
  # Returns true if the current action represents an edit on +record+.
@@ -57,7 +59,7 @@ module Hammock
57
59
  elsif record_or_records.is_a? ActiveRecord::Base
58
60
  instance_variable_set "@#{mdl_name}", (@record = record_or_records)
59
61
  elsif record_or_records.is_a? Ambition::Context
60
- # log "Unkicked query: #{record_or_records.to_s}"
62
+ log "Unkicked query: #{record_or_records.to_hash.inspect}"
61
63
  instance_variable_set "@#{mdl_name.pluralize}", (@records = record_or_records)
62
64
  elsif record_or_records.is_a? Array
63
65
  instance_variable_set "@#{mdl_name.pluralize}", (@records = record_or_records)
@@ -65,15 +67,25 @@ module Hammock
65
67
  raise "Unknown record(s) type #{record_or_records.class}."
66
68
  end
67
69
 
68
- if assign_nestable_resources
69
- @entity
70
- else
71
- escort :not_found
72
- end
70
+ @entity if assign_nesting_entities
73
71
  end
74
72
 
75
73
  private
76
74
 
75
+ def assign_nesting_entities
76
+ results = nesting_scope_list.map &:kick
77
+
78
+ if results.all? {|records| records.length == 1 }
79
+ results.map {|records|
80
+ records.first
81
+ }.all? {|record|
82
+ log "nested record #{record.concise_inspect} assigned to @#{record.base_model}."
83
+ add_nested_entity record
84
+ instance_variable_set "@#{record.base_model}", record
85
+ }
86
+ end
87
+ end
88
+
77
89
  def make_new_record resource = mdl
78
90
  resource.new_with(params_for(resource.symbolize))
79
91
  end
@@ -92,39 +104,24 @@ module Hammock
92
104
  end
93
105
  end
94
106
 
95
- def assign_nestable_resources
96
- @current_nested_records, @current_nested_resources = [], []
97
- params.symbolize_keys.dragnet(*nestable_resources.keys).all? {|param_name,column_name|
98
- constant_name = param_name.to_s.sub(/_id$/, '').camelize
99
- constant = Object.const_get constant_name rescue nil
100
-
101
- if constant.nil?
102
- log "'#{constant_name}' is not available for #{param_name}."
103
- elsif (record = constant.find_by_id(params[param_name])).nil?
104
- log "#{constant}<#{params[param_name]}> not found."
105
- else
106
- @current_nested_records << record
107
- @current_nested_resources << record.class
108
- @record.send "#{nestable_resources[param_name]}=", params[param_name] unless @record.nil?
109
- # log "Assigning @#{constant.name.underscore} with #{record.inspect}."
110
- instance_variable_set "@#{constant_name.underscore}", record
111
- end
112
- }
113
- end
114
-
115
107
  def current_nested_records
116
- @current_nested_records.nil? ? [] : @current_nested_records.dup
108
+ (@current_nested_records || []).dup
117
109
  end
118
110
 
119
111
  def current_nested_resources
120
- @current_nested_resources.nil? ? [] : @current_nested_resources.dup
112
+ (@current_nested_resources || []).dup
113
+ end
114
+
115
+ def add_nested_entity entity
116
+ (@current_nested_records ||= []).push entity
117
+ (@current_nested_resources ||= []).push entity.resource
121
118
  end
122
119
 
123
120
  def nested_within? record_or_resource
124
121
  if record_or_resource.is_a? ActiveRecord::Base
125
- @current_nested_records.include? record_or_resource
122
+ current_nested_records.include? record_or_resource
126
123
  else
127
- @current_nested_resources.include? record_or_resource
124
+ current_nested_resources.include? record_or_resource
128
125
  end
129
126
  end
130
127
 
@@ -136,8 +133,15 @@ module Hammock
136
133
  @editing = @record
137
134
  end
138
135
 
136
+ # TODO process /^creating_\w+_id$/ as well
139
137
  def set_creator_id_if_appropriate
140
- @record.creator_id = @current_account.id if @record.respond_to?(:creator_id=)
138
+ if @record.respond_to?(:creator_id=)
139
+ if @current_account.nil?
140
+ log "Warning: @#{@record.base_model}.creator_id isn't being set, since @current_account was nil."
141
+ else
142
+ @record.creator_id = @current_account.id
143
+ end
144
+ end
141
145
  end
142
146
 
143
147
  def partial_exists? name, extension = nil
@@ -47,7 +47,7 @@ module Hammock
47
47
  def nested_route_for *resources
48
48
  resources.delete_if &:nil?
49
49
  requested_verb = resources.shift if resources.first.is_a?(Symbol)
50
- args = @current_nested_records.dup.concat(resources)
50
+ args = current_nested_records.concat(resources)
51
51
 
52
52
  args.unshift(requested_verb) unless requested_verb.nil?
53
53
  route_for *args
@@ -0,0 +1,159 @@
1
+ module Hammock
2
+ class RouteNode < ActionController::Resources::Resource
3
+
4
+ DefaultRecordVerbs = {
5
+ :show => :get,
6
+ :edit => :get,
7
+ :update => :put,
8
+ :destroy => :delete
9
+ }.freeze
10
+ DefaultResourceVerbs = {
11
+ :index => :get
12
+ }.freeze
13
+ DefaultBuildVerbs = {
14
+ :new => :get,
15
+ :create => :post
16
+ }.freeze
17
+
18
+ attr_reader :mdl, :resource, :parent, :children, :routing_parent, :record_routes, :resource_routes, :build_routes
19
+
20
+ def initialize entity = nil, options = {}
21
+ @parent = options[:parent]
22
+ @children = {}
23
+ unless root?
24
+ @mdl = entity if entity.is_a?(Symbol)
25
+ @routing_parent = determine_routing_parent
26
+ define_routes options
27
+ end
28
+ end
29
+
30
+ def resource
31
+ # TODO performance
32
+ Object.const_get mdl.to_s.classify rescue nil
33
+ end
34
+
35
+ def root?
36
+ parent.nil?
37
+ end
38
+
39
+ def ancestry
40
+ root? ? [] : parent.ancestry.push(self)
41
+ end
42
+
43
+ def for verb, entities, options
44
+ raise "Hammock::RouteNode#for requires an explicitly specified verb as its first argument." unless verb.is_a?(Symbol)
45
+ raise "You have to supply an Array of at least one record or resource." if entities.empty? unless entities.is_a?(Array)
46
+
47
+ entity = entities.shift
48
+
49
+ if entities.empty?
50
+ steps_for verb, entity
51
+ else
52
+ children[entity.resource_sym].for(verb, entities, options).within steps_for(nil, entity)
53
+ end
54
+ end
55
+
56
+ def base_for resources
57
+ # puts "base_for<#{mdl}>: resources=#{resources.inspect}."
58
+ if resources.empty?
59
+ self
60
+ else
61
+ match = nil
62
+ children.values.detect {|child|
63
+ # puts " Trying #{child.ancestry.map(&:mdl).inspect} for #{resources.inspect}"
64
+ if !resources.include?(child.mdl)
65
+ # puts " Can't match #{resources.inspect} against #{child.mdl}."
66
+ nil
67
+ else
68
+ # puts " Matched #{child.mdl} from #{resources.inspect}."
69
+ match = child.base_for resources.discard(child.mdl)
70
+ end
71
+ } #|| raise("There is no routing path for #{resources.map(&:inspect).inspect}.")
72
+ match
73
+ end
74
+ end
75
+
76
+ def nesting_scope_list_for params
77
+ if root?
78
+ [ ]
79
+ else
80
+ parent.nesting_scope_list_for(params).push nesting_scope_segment_for(params)
81
+ end
82
+ end
83
+
84
+ def nesting_scope_segment_for params
85
+ raise "The root of the route map isn't associated with a resource." if root?
86
+ puts "is this undefined? #{resource.accessible_attributes_on_create.inspect}"
87
+ value = params.delete resource.param_key
88
+ puts "resource.select {|r| r.#{resource.routing_attribute} == value }"
89
+ eval "resource.select {|r| r.#{resource.routing_attribute} == value }"
90
+ end
91
+
92
+ def add entity, options, steps = nil
93
+ if steps.nil?
94
+ add entity, options, (options[:name_prefix] || '').chomp('_').split('_').map {|i| i.pluralize.underscore.to_sym }
95
+ elsif steps.empty?
96
+ add_child entity, options
97
+ else
98
+ children[steps.shift].add entity, options, steps
99
+ end
100
+ end
101
+
102
+ def routeable_as verb, entity
103
+ if entity.record? && record_routes[verb || :show]
104
+ :record
105
+ elsif entity.resource? && resource_routes[verb || :index]
106
+ :resource
107
+ elsif !verb.nil? && build_routes[verb]
108
+ :build
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def define_routes options
115
+ @record_routes = DefaultRecordVerbs.dup.update(options[:member] || {})
116
+ @resource_routes = DefaultResourceVerbs.dup.update(options[:collection] || {})
117
+ @build_routes = DefaultBuildVerbs.dup.update(options[:build] || {})
118
+ end
119
+
120
+ def determine_routing_parent
121
+ puts "\ndetermine_routing_parent: #{mdl}:"
122
+ if parent.nil? || parent.resource.nil?
123
+ puts "Resource for #{mdl} has either no parent or no resource - not nestable."
124
+ nil
125
+ else
126
+ puts "reflections: #{resource.reflections.keys.inspect}"
127
+ puts "nestable_routing_resources: #{resource.nestable_routing_resources.inspect}"
128
+ scannable_reflections = resource.nestable_routing_resources.nil? ? resource.reflections : resource.reflections.dragnet(*resource.nestable_routing_resources)
129
+ puts "scannable reflections: #{scannable_reflections.keys.inspect}"
130
+ valid_reflections = scannable_reflections.selekt {|k,v| puts "#{v.klass}<#{v.object_id}> == #{parent.resource}<#{parent.resource.object_id}> #=> #{v.klass == parent.resource}"; v.klass == parent.resource }
131
+ puts "valid reflections: #{valid_reflections.keys.inspect}"
132
+
133
+ if valid_reflections.keys.length < 1
134
+ raise "The routing table specifies that #{mdl} is nested within #{parent.mdl}, but there is no ActiveRecord association linking #{resource} to #{parent.resource}. Example: 'belongs_to :#{parent.resource.base_model}' in the #{resource} model."
135
+ elsif valid_reflections.keys.length > 1
136
+ raise "#{resource} defines more than one association to #{parent.resource} (#{valid_reflections.keys.map(&:to_s).join(', ')}). That's fine, but you need to use #{resource}.nest_within to specify the one Hammock should nest scopes through. For example, 'nest_within #{valid_reflections.keys.first.inspect}' in the #{resource} model."
137
+ end
138
+
139
+ valid_reflections.keys.first
140
+ end
141
+ end
142
+
143
+ def add_child entity, options
144
+ child = RouteNode.new entity, options.merge(:parent => self)
145
+ children[child.mdl] = child
146
+ end
147
+
148
+ def steps_for verb, entity
149
+ child = children[entity.resource_sym]
150
+
151
+ if child.nil?
152
+ raise "No routes are defined for #{entity.resource}#{' within ' + ancestry.map {|r| r.mdl.to_s }.join(', ') unless ancestry.empty?}."
153
+ else
154
+ RouteStep.new(child).for(verb, entity)
155
+ end
156
+ end
157
+
158
+ end
159
+ end
@@ -0,0 +1,87 @@
1
+ module Hammock
2
+ class RouteStep
3
+ attr_reader :resource, :routeable_as, :verb, :entity, :parent
4
+
5
+ def initialize resource
6
+ @resource = resource
7
+ end
8
+
9
+ def for verb, entity
10
+ routeable_as = resource.routeable_as(verb, entity)
11
+
12
+ if !routeable_as
13
+ raise "The verb '#{verb}' can't be applied to " + (entity.record? ? "#{entity.resource} records" : "the #{entity.resource} resource") + "."
14
+ elsif (:record == routeable_as) && entity.new_record?
15
+ raise "The verb '#{verb}' requires a #{entity.resource} with an ID (i.e. not a new record)."
16
+ elsif (:build == routeable_as) && entity.record? && !entity.new_record?
17
+ raise "The verb '#{verb}' requires either the #{entity.resource} resource, or a #{entity.resource} without an ID (i.e. a new record)."
18
+ else
19
+ @verb, @entity, @routeable_as = verb, entity, routeable_as
20
+ end
21
+
22
+ self
23
+ end
24
+
25
+ def within parent
26
+ @parent = parent
27
+ self
28
+ end
29
+
30
+ def setup?
31
+ !@entity.nil?
32
+ end
33
+
34
+ def path params = nil
35
+ raise_unless_setup_while_trying_to 'render a path'
36
+
37
+ buf = '/'
38
+ buf << entity.resource_name
39
+ buf << '/' + entity.to_param if entity.record? && !entity.new_record?
40
+ buf << '/' + verb.to_s unless verb.nil? or implied_verb?(verb)
41
+
42
+ buf = parent.path + buf unless parent.nil?
43
+ buf << param_str(params)
44
+
45
+ buf
46
+ end
47
+
48
+ def http_method
49
+ raise_unless_setup_while_trying_to 'extract the HTTP method'
50
+ resource.send("#{routeable_as}_routes")[verb]
51
+ end
52
+
53
+ def fake_http_method
54
+ http_method.in?(:get, :post) ? http_method : :post
55
+ end
56
+
57
+ def get?; :get == http_method end
58
+ def post?; :post == http_method end
59
+ def put?; :put == http_method end
60
+ def delete?; :delete == http_method end
61
+
62
+ def safe?
63
+ get? && !verb.in?(Hammock::Constants::ImpliedUnsafeActions)
64
+ end
65
+
66
+ private
67
+
68
+ def implied_verb? verb
69
+ verb.in? :index, :create, :show, :update, :destroy
70
+ end
71
+
72
+ def raise_unless_setup_while_trying_to task
73
+ raise "You have to call for(verb, entity) (and optionally within(parent)) on this RouteStep before you can #{task}." unless setup?
74
+ end
75
+
76
+ def param_str params
77
+ link_params = entity.record? ? entity.unsaved_attributes.merge(params || {}) : params
78
+
79
+ if link_params.blank?
80
+ ''
81
+ else
82
+ '?' + {entity.base_model => link_params}.to_query
83
+ end
84
+ end
85
+
86
+ end
87
+ end
@@ -62,7 +62,7 @@ module Hammock
62
62
  end
63
63
  end
64
64
 
65
- def verb_scope
65
+ def current_verb_scope
66
66
  if @current_account && (scope_name = account_verb_scope?)
67
67
  # log "got an account_verb_scope #{scope_name}."
68
68
  mdl.send scope_name, @current_account
@@ -75,17 +75,24 @@ module Hammock
75
75
  end
76
76
  end
77
77
 
78
- def nest_scope
79
- params.symbolize_keys.dragnet(*nestable_resources.keys).inject(mdl.ambition_context) {|acc,(k,v)|
80
- # TODO this would be more ductile if it used AR assocs instead of explicit FK
81
- eval "acc.select {|r| r.#{nestable_resources[k]} == v }"
82
- }
78
+ def nesting_scope_list
79
+ @hammock_cached_nesting_scope_list = current_hammock_resource.parent.nesting_scope_list_for params.selekt {|k,v| /_id$/ =~ k }
80
+ end
81
+
82
+ def current_nest_scope
83
+ nesting_scope_list.reverse.inject {|acc,scope| acc.within scope }
83
84
  end
84
85
 
85
86
  def current_scope
86
- if (resultant_scope = nest_scope.chain(verb_scope)).nil?
87
+ log "#{current_hammock_resource.mdl}, #{current_hammock_resource.ancestry.map(&:mdl).inspect}"
88
+ if (verb_scope = current_verb_scope).nil?
89
+ nil
90
+ elsif (resultant_scope = verb_scope.within(current_nest_scope, current_hammock_resource.routing_parent)).nil?
87
91
  nil
88
92
  else
93
+ # puts "nest: #{current_nest_scope.clauses.inspect}"
94
+ # puts "verb: #{current_verb_scope.clauses.inspect}"
95
+ puts "chained in (#{resultant_scope.owner}) current_scope: #{resultant_scope.clauses.inspect}"
89
96
  resultant_scope = resultant_scope.chain(custom_scope) unless custom_scope.nil?
90
97
  resultant_scope.sort_by &mdl.sorter
91
98
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: benhoskings-hammock
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Hoskings
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-02-27 00:00:00 -08:00
12
+ date: 2009-03-05 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -63,6 +63,7 @@ files:
63
63
  - hammock.gemspec
64
64
  - lib/hammock.rb
65
65
  - lib/hammock/ajaxinate.rb
66
+ - lib/hammock/callback.rb
66
67
  - lib/hammock/callbacks.rb
67
68
  - lib/hammock/canned_scopes.rb
68
69
  - lib/hammock/constants.rb
@@ -91,6 +92,8 @@ files:
91
92
  - lib/hammock/restful_support.rb
92
93
  - lib/hammock/route_drawing_hooks.rb
93
94
  - lib/hammock/route_for.rb
95
+ - lib/hammock/route_node.rb
96
+ - lib/hammock/route_step.rb
94
97
  - lib/hammock/scope.rb
95
98
  - lib/hammock/suggest.rb
96
99
  - lib/hammock/utils.rb
@@ -98,7 +101,6 @@ files:
98
101
  - misc/template.rb
99
102
  - tasks/hammock_tasks.rake
100
103
  - test/hammock_test.rb
101
- - test/rails_root/test/test_helper.rb
102
104
  has_rdoc: true
103
105
  homepage: http://github.com/benhoskings/hammock
104
106
  post_install_message:
@@ -126,5 +128,5 @@ rubygems_version: 1.2.0
126
128
  signing_key:
127
129
  specification_version: 2
128
130
  summary: Hammock is a Rails plugin that eliminates redundant code in a very RESTful manner
129
- test_files:
130
- - test/rails_root/test/test_helper.rb
131
+ test_files: []
132
+