rpc-mapper 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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