rpc-mapper 0.2.1 → 0.3.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.
@@ -2,6 +2,23 @@ module RPCMapper::Association; end
2
2
 
3
3
  module RPCMapper::Association
4
4
 
5
+ ##
6
+ # Association::Base
7
+ #
8
+ # This is the base class for all associations. It defines the basic structure
9
+ # of an association. The basic nomenclature is as follows:
10
+ #
11
+ # TODO: Clean up this nomenclature. Source and Target should be switched. From
12
+ # the configuration side this is already done but the internal naming is backwards.
13
+ #
14
+ # Source: The start point of the association. The source class is the class
15
+ # on which the association is defined.
16
+ #
17
+ # Proxy: On a through association the proxy is the class on which the target
18
+ # association lives
19
+ #
20
+ # Target: The class that will ultimately be returned by the association
21
+ #
5
22
  class Base
6
23
  attr_accessor :source_klass, :id, :options
7
24
 
@@ -16,7 +33,10 @@ module RPCMapper::Association
16
33
  end
17
34
 
18
35
  def polymorphic?
19
- raise NotImplementedError, "You must define how your association is polymorphic in subclasses."
36
+ raise(
37
+ NotImplementedError,
38
+ "You must define how your association is polymorphic in subclasses."
39
+ )
20
40
  end
21
41
 
22
42
  def collection?
@@ -32,11 +52,33 @@ module RPCMapper::Association
32
52
  end
33
53
 
34
54
  def target_klass(object=nil)
35
- klass = if options[:polymorphic]
36
- eval [options[:polymorphic_namespace], object.send("#{id}_type")].compact.join('::') if object
55
+ if options[:polymorphic] && object
56
+ poly_type = object.is_a?(RPCMapper::Base) ? object.send("#{id}_type") : object
57
+ end
58
+
59
+ klass = if poly_type
60
+ type_string = [
61
+ options[:polymorphic_namespace],
62
+ sanitize_type_attribute(poly_type)
63
+ ].compact.join('::')
64
+ begin
65
+ eval(type_string)
66
+ rescue NameError => err
67
+ raise(
68
+ RPCMapper::PolymorphicAssociationTypeError,
69
+ "No constant defined called #{type_string}"
70
+ )
71
+ end
37
72
  else
38
- raise(ArgumentError, ":class_name option required for association declaration.") unless options[:class_name]
39
- options[:class_name] = "::#{options[:class_name]}" unless options[:class_name] =~ /^::/
73
+ unless options[:class_name]
74
+ raise(ArgumentError,
75
+ ":class_name option required for association declaration.")
76
+ end
77
+
78
+ unless options[:class_name] =~ /^::/
79
+ options[:class_name] = "::#{options[:class_name]}"
80
+ end
81
+
40
82
  eval(options[:class_name])
41
83
  end
42
84
 
@@ -47,9 +89,12 @@ module RPCMapper::Association
47
89
  raise NotImplementedError, "You must define scope in subclasses"
48
90
  end
49
91
 
50
- # TRP: Only eager loadable if association query does not depend on instance data
92
+ # TRP: Only eager loadable if association query does not depend on instance
93
+ # data
51
94
  def eager_loadable?
52
- RPCMapper::Relation::FINDER_OPTIONS.inject(true) { |condition, key| condition && !options[key].respond_to?(:call) }
95
+ RPCMapper::Relation::FINDER_OPTIONS.inject(true) do |condition, key|
96
+ condition && !options[key].respond_to?(:call)
97
+ end
53
98
  end
54
99
 
55
100
  protected
@@ -66,6 +111,11 @@ module RPCMapper::Association
66
111
  end
67
112
  end
68
113
 
114
+ # TRP: Make sure the value looks like a variable syntaxtually
115
+ def sanitize_type_attribute(string)
116
+ string.gsub(/[^a-zA-Z]\w*/, '')
117
+ end
118
+
69
119
  end
70
120
 
71
121
 
@@ -87,16 +137,26 @@ module RPCMapper::Association
87
137
  !!options[:polymorphic]
88
138
  end
89
139
 
140
+ def polymorphic_type
141
+ "#{id}_type".to_sym
142
+ end
143
+
144
+ ##
90
145
  # Returns a scope on the target containing this association
91
146
  #
92
- # Builds conditions on top of the base_scope generated from any finder options set with the association
147
+ # Builds conditions on top of the base_scope generated from any finder
148
+ # options set with the association
93
149
  #
94
150
  # belongs_to :foo, :foreign_key => :foo_id
95
151
  #
96
- # In addition to any finder options included with the association options the following scope will be added:
152
+ # In addition to any finder options included with the association options
153
+ # the following scope will be added:
97
154
  # where(:id => source[:foo_id])
155
+ #
98
156
  def scope(object)
99
- base_scope(object).where(self.primary_key => object[self.foreign_key]) if object[self.foreign_key]
157
+ if object[self.foreign_key]
158
+ base_scope(object).where(self.primary_key => object[self.foreign_key])
159
+ end
100
160
  end
101
161
 
102
162
  end
@@ -105,27 +165,46 @@ module RPCMapper::Association
105
165
  class Has < Base
106
166
 
107
167
  def foreign_key
108
- super || (self.polymorphic? ? "#{options[:as]}_id" : "#{RPCMapper::Base.base_class_name(source_klass).downcase}_id").to_sym
168
+ super || if self.polymorphic?
169
+ "#{options[:as]}_id"
170
+ else
171
+ "#{RPCMapper::Base.base_class_name(source_klass).downcase}_id"
172
+ end.to_sym
109
173
  end
110
174
 
111
175
 
176
+ ##
112
177
  # Returns a scope on the target containing this association
113
178
  #
114
- # Builds conditions on top of the base_scope generated from any finder options set with the association
179
+ # Builds conditions on top of the base_scope generated from any finder
180
+ # options set with the association
181
+ #
182
+ # has_many :widgets, :class_name => "Widget", :foreign_key => :widget_id
183
+ # has_many :comments, :as => :parent
184
+ #
185
+ # In addition to any finder options included with the association options
186
+ # the following will be added:
115
187
  #
116
- # has_many :widgets, :class_name => "Widget", :foreign_key => :widget_id
117
- # has_many :comments, :as => :parent
188
+ # where(widget_id => source[:id])
118
189
  #
119
- # In addition to any finder options included with the association options the following will be added:
120
- # where(widget_id => source[:id])
121
190
  # Or for the polymorphic :comments association:
122
- # where(:parent_id => source[:id], :parent_type => source.class)
191
+ #
192
+ # where(:parent_id => source[:id], :parent_type => source.class)
193
+ #
123
194
  def scope(object)
124
- s = base_scope(object).where(self.foreign_key => object[self.primary_key]) if object[self.primary_key]
125
- s = s.where(:"#{options[:as]}_type" => RPCMapper::Base.base_class_name(object.class)) if s && polymorphic?
195
+ return nil unless object[self.primary_key]
196
+ s = base_scope(object).where(self.foreign_key => object[self.primary_key])
197
+ if polymorphic?
198
+ s = s.where(
199
+ polymorphic_type => RPCMapper::Base.base_class_name(object.class) )
200
+ end
126
201
  s
127
202
  end
128
203
 
204
+ def polymorphic_type
205
+ :"#{options[:as]}_type"
206
+ end
207
+
129
208
  def polymorphic?
130
209
  !!options[:as]
131
210
  end
@@ -159,4 +238,93 @@ module RPCMapper::Association
159
238
  end
160
239
 
161
240
 
241
+ class HasManyThrough < HasMany
242
+ attr_accessor :proxy_association
243
+ attr_accessor :target_association
244
+
245
+ def proxy_association
246
+ @proxy_association ||= source_klass.defined_associations[options[:through]] ||
247
+ raise(
248
+ RPCMapper::AssociationNotFound,
249
+ ":has_many_through: '#{options[:through]}' is not an association " +
250
+ "on #{source_klass}"
251
+ )
252
+ end
253
+
254
+ def target_association
255
+ return @target_association if @target_association
256
+
257
+ klass = proxy_association.target_klass
258
+ @target_association = klass.defined_associations[self.id] ||
259
+ klass.defined_associations[self.options[:source]] ||
260
+ raise(RPCMapper::AssociationNotFound,
261
+ ":has_many_through: '#{options[:source] || self.id}' is not an " +
262
+ "association on #{klass}"
263
+ )
264
+ end
265
+
266
+ def scope(object)
267
+ # Which attribute's values should be used on the proxy
268
+ key = if target_is_has?
269
+ target_association.primary_key.to_sym
270
+ else
271
+ target_association.foreign_key.to_sym
272
+ end
273
+
274
+ # Fetch the ids of all records on the proxy using the correct key
275
+ proxy_ids = proc do
276
+ proxy_association.scope(object).select(key).collect(&key)
277
+ end
278
+
279
+ # Use these ids to build a scope on the target object
280
+ relation = target_klass.scoped
281
+
282
+ if target_is_has?
283
+ relation = relation.where(
284
+ proc do
285
+ { target_association.foreign_key => proxy_ids.call }
286
+ end
287
+ )
288
+ else
289
+ relation = relation.where(
290
+ proc do
291
+ { target_association.primary_key => proxy_ids.call }
292
+ end
293
+ )
294
+ end
295
+
296
+ # Add polymorphic type condition if target is polymorphic and has
297
+ if target_association.polymorphic? && target_is_has?
298
+ relation = relation.where(
299
+ target_association.polymorphic_type =>
300
+ RPCMapper::Base.base_class_name(proxy_association.target_klass(object))
301
+ )
302
+ end
303
+
304
+ relation
305
+ end
306
+
307
+ def target_klass
308
+ target_association.target_klass(options[:source_type])
309
+ end
310
+
311
+ def target_type
312
+ target_association.type
313
+ end
314
+
315
+ def target_is_has?
316
+ target_association.is_a?(Has)
317
+ end
318
+
319
+ def type
320
+ :has_many_through
321
+ end
322
+
323
+ def polymorphic?
324
+ false
325
+ end
326
+
327
+ end
328
+
329
+
162
330
  end
@@ -4,30 +4,49 @@ module RPCMapper::AssociationPreload
4
4
  def eager_load_associations(original_results, relation)
5
5
  relation.includes_values.each do |association_id|
6
6
  association = self.defined_associations[association_id.to_sym]
7
- options = association.options
7
+ force_fresh = relation.fresh_value
8
+
9
+ unless association
10
+ raise(
11
+ RPCMapper::AssociationNotFound,
12
+ "no such association (#{association_id})"
13
+ )
14
+ end
8
15
 
9
16
  unless association.eager_loadable?
10
- raise RPCMapper::AssociationPreloadNotSupported,
11
- "delayed execution options (block options) cannot be used for eager loaded associations"
17
+ raise(
18
+ RPCMapper::AssociationPreloadNotSupported,
19
+ "delayed execution options (block options) cannot be used for eager loaded associations"
20
+ )
12
21
  end
13
22
 
23
+ options = association.options
24
+
14
25
  case association.type
15
26
  when :has_many, :has_one
16
27
  fks = original_results.collect { |record| record.send(association.primary_key) }.compact
17
28
 
18
- pre_records = association.target_klass.where(association.foreign_key => fks).all
29
+ pre_records = association.target_klass.where(association.foreign_key => fks).all(:fresh => force_fresh)
19
30
 
20
31
  original_results.each do |record|
21
32
  pk = record.send(association.primary_key)
22
33
  relevant_records = pre_records.select { |r| r.send(association.foreign_key) == pk }
23
- relevant_records = association.collection? ? relevant_records : relevant_records.first
34
+
35
+ relevant_records = if association.collection?
36
+ scope = association.scope(record).where(association.foreign_key => pk)
37
+ scope.records = relevant_records
38
+ scope
39
+ else
40
+ relevant_records.first
41
+ end
42
+
24
43
  record.send("#{association.id}=", relevant_records)
25
44
  end
26
45
 
27
46
  when :belongs_to
28
47
  fks = original_results.collect { |record| record.send(association.foreign_key) }.compact
29
48
 
30
- pre_records = association.target_klass.where(association.primary_key => fks).all
49
+ pre_records = association.target_klass.where(association.primary_key => fks).all(:fresh => force_fresh)
31
50
 
32
51
  original_results.each do |record|
33
52
  fk = record.send(association.foreign_key)
@@ -47,4 +66,4 @@ module RPCMapper::AssociationPreload
47
66
  receiver.extend ClassMethods
48
67
  receiver.send :include, InstanceMethods
49
68
  end
50
- end
69
+ end
@@ -15,7 +15,12 @@ module RPCMapper::Associations
15
15
  end
16
16
 
17
17
  def has_many(id, options={})
18
- create_external_association RPCMapper::Association::HasMany.new(self, id, options)
18
+ klass = if options.include?(:through)
19
+ RPCMapper::Association::HasManyThrough
20
+ else
21
+ RPCMapper::Association::HasMany
22
+ end
23
+ create_external_association klass.new(self, id, options)
19
24
  end
20
25
 
21
26
  protected
@@ -27,11 +32,13 @@ module RPCMapper::Associations
27
32
  define_method(association.id) do
28
33
  cached_value = instance_variable_get(cache_ivar)
29
34
 
30
- # TRP: Logic for actually pulling setting the value
35
+ # TRP: Logic for actually pulling & setting the value
31
36
  unless cached_value
32
37
  scoped = association.scope(self)
33
38
 
34
- cached_value = association.collection? ? scoped : scoped.first if scoped && !scoped.where_values.empty?
39
+ if scoped && !scoped.where_values.empty?
40
+ cached_value = association.collection? ? scoped : scoped.first
41
+ end
35
42
 
36
43
  instance_variable_set(cache_ivar, cached_value)
37
44
  end
@@ -57,4 +64,4 @@ module RPCMapper::Associations
57
64
  end
58
65
  end
59
66
 
60
- end
67
+ end
@@ -1,24 +1,49 @@
1
1
  module RPCMapper
2
2
 
3
3
  # Generic RPCMapper error
4
+ #
4
5
  class RPCMapperError < StandardError
5
6
  end
6
7
 
7
8
  # Raised when RPCMapper cannot find records from a given id or set of ids
9
+ #
8
10
  class RecordNotFound < RPCMapperError
9
11
  end
10
12
 
11
- # Raised when RPCMapper cannot save a record through the write_adapter and save! or update_attributes! was used
13
+ # Raised when RPCMapper cannot save a record through the write_adapter
14
+ # and save! or update_attributes! was used
15
+ #
12
16
  class RecordNotSaved < RPCMapperError
13
17
  end
14
18
 
15
- # Raised when trying to eager load an association that relies on instance level data.
19
+ # Used for all association related errors
20
+ #
21
+ class AssociationError < RPCMapperError
22
+ end
23
+
24
+ # Raised when trying to eager load an association that relies on instance
25
+ # level data.
16
26
  # class Article < RPCMapper::Base
17
27
  # has_many :comments, :conditions => lambda { |article| ... }
18
28
  # end
19
29
  #
20
30
  # Article.recent.includes(:comments) # Raises AssociationPreloadNotSupported
21
- class AssociationPreloadNotSupported < RPCMapperError
31
+ #
32
+ class AssociationPreloadNotSupported < AssociationError
33
+ end
34
+
35
+ # Raised when trying to eager load an association that does not exist or
36
+ # trying to create a :has_many_through association with a nonexistant
37
+ # association as the source
38
+ #
39
+ class AssociationNotFound < AssociationError
40
+ end
41
+
42
+ # Raised when a polymorphic association type is not present in the specified
43
+ # scope. Always be sure that any values set for the type attribute on any
44
+ # polymorphic association are real constants defined in :polymorphic_namespace
45
+ #
46
+ class PolymorphicAssociationTypeError < AssociationError
22
47
  end
23
48
 
24
49
  end
@@ -6,6 +6,8 @@ require 'rpc_mapper/relation/finder_methods'
6
6
  # Used to achieve the chainability of scopes -- methods are delegated back and forth from BM::Base and BM::Relation
7
7
  class RPCMapper::Relation
8
8
  attr_reader :klass
9
+ attr_accessor :records
10
+
9
11
  SINGLE_VALUE_METHODS = [:limit, :offset, :from, :fresh]
10
12
  MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :includes, :where, :having]
11
13
 
@@ -44,23 +46,31 @@ class RPCMapper::Relation
44
46
  else
45
47
  hash = SINGLE_VALUE_METHODS.inject({}) do |h, option|
46
48
  value = self.send("#{option}_value")
49
+ value = call_procs(value)
47
50
  value ? h.merge(option => value) : h
48
51
  end
49
52
 
50
53
  hash.merge!((MULTI_VALUE_METHODS - [:select]).inject({}) do |h, option|
51
54
  value = self.send("#{option}_values")
55
+ value = call_procs(value)
52
56
  value && !value.empty? ? h.merge(option => value.uniq) : h
53
57
  end)
54
58
 
55
59
  # TRP: If one of the select options contains a * than select options are ignored
56
60
  if select_values && !select_values.empty? && !select_values.any? { |val| val.to_s.match(/\*$/) }
57
- hash.merge!(:select => select_values.uniq)
61
+ value = call_procs(select_values)
62
+ hash.merge!(:select => value.uniq)
58
63
  end
59
64
 
60
65
  hash
61
66
  end
62
67
  end
63
68
 
69
+ def reset_queries
70
+ @hash = nil
71
+ @records = nil
72
+ end
73
+
64
74
  def to_a
65
75
  @records ||= fetch_records
66
76
  end
@@ -127,4 +137,17 @@ class RPCMapper::Relation
127
137
  @klass.send(:fetch_records, self)
128
138
  end
129
139
 
140
+ private
141
+
142
+ def call_procs(values)
143
+ case values
144
+ when Array:
145
+ values.collect { |v| v.is_a?(Proc) ? v.call : v }
146
+ when Proc:
147
+ values.call
148
+ else
149
+ values
150
+ end
151
+ end
152
+
130
153
  end
@@ -8,33 +8,33 @@ module RPCMapper::QueryMethods
8
8
  if block_given?
9
9
  to_a.select {|*block_args| yield(*block_args) }
10
10
  else
11
- clone.tap { |r| r.select_values += args if args && !args.empty? }
11
+ clone.tap { |r| r.select_values += args if args_valid? args }
12
12
  end
13
13
  end
14
14
 
15
15
  def group(*args)
16
- clone.tap { |r| r.group_values += args if args && !args.empty? }
16
+ clone.tap { |r| r.group_values += args if args_valid? args }
17
17
  end
18
18
 
19
19
  def order(*args)
20
- clone.tap { |r| r.order_values += args if args && !args.empty? }
20
+ clone.tap { |r| r.order_values += args if args_valid? args }
21
21
  end
22
22
 
23
23
  def joins(*args)
24
- clone.tap { |r| r.joins_values += args if args && !args.empty? }
24
+ clone.tap { |r| r.joins_values += args if args_valid?(args) }
25
25
  end
26
26
 
27
27
  def includes(*args)
28
28
  args.reject! { |a| a.nil? }
29
- clone.tap { |r| r.includes_values += (r.includes_values + args).flatten.uniq if args && !args.empty? }
29
+ clone.tap { |r| r.includes_values += (r.includes_values + args).flatten.uniq if args_valid? args }
30
30
  end
31
31
 
32
32
  def where(*args)
33
- clone.tap { |r| r.where_values += args.compact.reject(&:empty?) if args && !args.empty? }
33
+ clone.tap { |r| r.where_values += args.compact.select { |i| args_valid? i } if args_valid? args }
34
34
  end
35
35
 
36
36
  def having(*args)
37
- clone.tap { |r| r.having_values += args if args && !args.empty? }
37
+ clone.tap { |r| r.having_values += args if args_valid? args }
38
38
  end
39
39
 
40
40
  def limit(value = true)
@@ -52,7 +52,7 @@ module RPCMapper::QueryMethods
52
52
  def fresh(val=true)
53
53
  clone.tap do |r|
54
54
  r.fresh_value = val
55
- r.instance_variable_set(:@records, nil) if r.fresh_value
55
+ r.reset_queries if r.fresh_value
56
56
  end
57
57
  end
58
58
 
@@ -60,4 +60,10 @@ module RPCMapper::QueryMethods
60
60
  clone.tap { |r| r.raw_sql_value = raw_sql }
61
61
  end
62
62
 
63
+ protected
64
+
65
+ def args_valid?(args)
66
+ args.respond_to?(:empty?) ? !args.empty? : !!args
67
+ end
68
+
63
69
  end
@@ -2,12 +2,12 @@ module RPCMapper
2
2
  module Version
3
3
 
4
4
  MAJOR = 0
5
- MINOR = 2
6
- TINY = 1
5
+ MINOR = 3
6
+ TINY = 0
7
7
 
8
8
  def self.to_s # :nodoc:
9
9
  [MAJOR, MINOR, TINY].join('.')
10
10
  end
11
11
 
12
12
  end
13
- end
13
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rpc-mapper
3
3
  version: !ruby/object:Gem::Version
4
- hash: 21
5
- prerelease: false
4
+ hash: 19
5
+ prerelease:
6
6
  segments:
7
7
  - 0
8
- - 2
9
- - 1
10
- version: 0.2.1
8
+ - 3
9
+ - 0
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Travis Petticrew
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-12-03 00:00:00 -06:00
18
+ date: 2011-02-28 00:00:00 -06:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -179,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
179
179
  requirements: []
180
180
 
181
181
  rubyforge_project:
182
- rubygems_version: 1.3.7
182
+ rubygems_version: 1.4.2
183
183
  signing_key:
184
184
  specification_version: 3
185
185
  summary: Ruby library for querying and mapping data over RPC