rpc-mapper 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,162 @@
1
+ module RPCMapper::Association; end
2
+
3
+ module RPCMapper::Association
4
+
5
+ class Base
6
+ attr_accessor :source_klass, :id, :options
7
+
8
+ def initialize(klass, id, options={})
9
+ self.source_klass = klass
10
+ self.id = id.to_sym
11
+ self.options = options
12
+ end
13
+
14
+ def type
15
+ raise NotImplementedError, "You must define the type in subclasses."
16
+ end
17
+
18
+ def polymorphic?
19
+ raise NotImplementedError, "You must define how your association is polymorphic in subclasses."
20
+ end
21
+
22
+ def collection?
23
+ raise NotImplementedError, "You must define collection? in subclasses."
24
+ end
25
+
26
+ def primary_key
27
+ options[:primary_key] || :id
28
+ end
29
+
30
+ def foreign_key
31
+ options[:foreign_key]
32
+ end
33
+
34
+ def target_klass(object=nil)
35
+ klass = if options[:polymorphic]
36
+ eval [options[:polymorphic_namespace], object.send("#{id}_type")].compact.join('::') if object
37
+ 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] =~ /^::/
40
+ eval(options[:class_name])
41
+ end
42
+
43
+ RPCMapper::Base.resolve_leaf_klass klass
44
+ end
45
+
46
+ def scope(object)
47
+ raise NotImplementedError, "You must define scope in subclasses"
48
+ end
49
+
50
+ # TRP: Only eager loadable if association query does not depend on instance data
51
+ def eager_loadable?
52
+ RPCMapper::Relation::FINDER_OPTIONS.inject(true) { |condition, key| condition && !options[key].respond_to?(:call) }
53
+ end
54
+
55
+ protected
56
+
57
+ def base_scope(object)
58
+ target_klass(object).scoped.apply_finder_options base_finder_options(object)
59
+ end
60
+
61
+ def base_finder_options(object)
62
+ RPCMapper::Relation::FINDER_OPTIONS.inject({}) do |sum, key|
63
+ value = self.options[key]
64
+ sum.merge!(key => value.respond_to?(:call) ? value.call(object) : value) if value
65
+ sum
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+
72
+ class BelongsTo < Base
73
+
74
+ def type
75
+ :belongs_to
76
+ end
77
+
78
+ def collection?
79
+ false
80
+ end
81
+
82
+ def foreign_key
83
+ super || "#{id}_id".to_sym
84
+ end
85
+
86
+ def polymorphic?
87
+ !!options[:polymorphic]
88
+ end
89
+
90
+ # Returns a scope on the target containing this association
91
+ #
92
+ # Builds conditions on top of the base_scope generated from any finder options set with the association
93
+ #
94
+ # belongs_to :foo, :foreign_key => :foo_id
95
+ #
96
+ # In addition to any finder options included with the association options the following scope will be added:
97
+ # where(:id => source[:foo_id])
98
+ def scope(object)
99
+ base_scope(object).where(self.primary_key => object[self.foreign_key]) if object[self.foreign_key]
100
+ end
101
+
102
+ end
103
+
104
+
105
+ class Has < Base
106
+
107
+ def foreign_key
108
+ super || (self.polymorphic? ? "#{options[:as]}_id" : "#{RPCMapper::Base.base_class_name(source_klass).downcase}_id").to_sym
109
+ end
110
+
111
+
112
+ # Returns a scope on the target containing this association
113
+ #
114
+ # Builds conditions on top of the base_scope generated from any finder options set with the association
115
+ #
116
+ # has_many :widgets, :class_name => "Widget", :foreign_key => :widget_id
117
+ # has_many :comments, :as => :parent
118
+ #
119
+ # In addition to any finder options included with the association options the following will be added:
120
+ # where(widget_id => source[:id])
121
+ # Or for the polymorphic :comments association:
122
+ # where(:parent_id => source[:id], :parent_type => source.class)
123
+ 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?
126
+ s
127
+ end
128
+
129
+ def polymorphic?
130
+ !!options[:as]
131
+ end
132
+
133
+ end
134
+
135
+
136
+ class HasMany < Has
137
+
138
+ def collection?
139
+ true
140
+ end
141
+
142
+ def type
143
+ :has_many
144
+ end
145
+
146
+ end
147
+
148
+
149
+ class HasOne < Has
150
+
151
+ def collection?
152
+ false
153
+ end
154
+
155
+ def type
156
+ :has_one
157
+ end
158
+
159
+ end
160
+
161
+
162
+ end
@@ -0,0 +1,50 @@
1
+ module RPCMapper::AssociationPreload
2
+ module ClassMethods
3
+
4
+ def eager_load_associations(original_results, relation)
5
+ relation.includes_values.each do |association_id|
6
+ association = self.defined_associations[association_id.to_sym]
7
+ options = association.options
8
+
9
+ unless association.eager_loadable?
10
+ raise RPCMapper::AssociationPreloadNotSupported,
11
+ "delayed execution options (block options) cannot be used for eager loaded associations"
12
+ end
13
+
14
+ case association.type
15
+ when :has_many, :has_one
16
+ fks = original_results.collect { |record| record.send(association.primary_key) }.compact
17
+
18
+ pre_records = association.target_klass.where(association.foreign_key => fks).all
19
+
20
+ original_results.each do |record|
21
+ pk = record.send(association.primary_key)
22
+ relevant_records = pre_records.select { |r| r.send(association.foreign_key) == pk }
23
+ relevant_records = association.collection? ? relevant_records : relevant_records.first
24
+ record.send("#{association.id}=", relevant_records)
25
+ end
26
+
27
+ when :belongs_to
28
+ fks = original_results.collect { |record| record.send(association.foreign_key) }.compact
29
+
30
+ pre_records = association.target_klass.where(association.primary_key => fks).all
31
+
32
+ original_results.each do |record|
33
+ fk = record.send(association.foreign_key)
34
+ relevant_records = pre_records.select { |r| r.send(association.primary_key) == fk }.first
35
+ record.send("#{association.id}=", relevant_records)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ module InstanceMethods
44
+ end
45
+
46
+ def self.included(receiver)
47
+ receiver.extend ClassMethods
48
+ receiver.send :include, InstanceMethods
49
+ end
50
+ end
@@ -3,6 +3,15 @@ module RPCMapper::Associations
3
3
  module Common
4
4
  module ClassMethods
5
5
 
6
+ def base_class_name(klass)
7
+ klass.to_s.split("::").last
8
+ end
9
+
10
+ # TRP: This will return the most recent extension of klass or klass if it is a leaf node in the hierarchy
11
+ def resolve_leaf_klass(klass)
12
+ extension_map[klass].empty? ? klass : resolve_leaf_klass(extension_map[klass].last)
13
+ end
14
+
6
15
  protected
7
16
 
8
17
  def extension_map
@@ -13,15 +22,6 @@ module RPCMapper::Associations
13
22
  self.extension_map[old_klass] += [new_klass] if base_class_name(old_klass) == base_class_name(new_klass)
14
23
  end
15
24
 
16
- # TRP: This will return the most recent extension of klass or klass if it is a leaf node in the hierarchy
17
- def resolve_leaf_klass(klass)
18
- extension_map[klass].empty? ? klass : resolve_leaf_klass(extension_map[klass].last)
19
- end
20
-
21
- def base_class_name(klass)
22
- klass.to_s.split("::").last
23
- end
24
-
25
25
  end
26
26
 
27
27
  module InstanceMethods
@@ -32,9 +32,9 @@ module RPCMapper::Associations
32
32
  klass = if options[:polymorphic]
33
33
  eval [options[:polymorphic_namespace], self.send("#{association}_type")].compact.join('::')
34
34
  else
35
- raise(ArgumentError, ":class_name or :klass option required for association declaration.") unless options[:class_name] || options[:klass]
35
+ raise(ArgumentError, ":class_name option required for association declaration.") unless options[:class_name]
36
36
  options[:class_name] = "::#{options[:class_name]}" unless options[:class_name] =~ /^::/
37
- options[:klass] || eval(options[:class_name])
37
+ eval(options[:class_name])
38
38
  end
39
39
 
40
40
  self.class.send :resolve_leaf_klass, klass
@@ -1,47 +1,38 @@
1
1
  require 'rpc_mapper/associations/common'
2
+ require 'rpc_mapper/association'
2
3
 
3
4
  module RPCMapper::Associations
4
5
 
5
6
  module External
6
7
  module ClassMethods
7
8
 
8
- def belongs_to(association, options={})
9
- create_external_association(:belongs, association, options)
9
+ def belongs_to(id, options={})
10
+ create_external_association RPCMapper::Association::BelongsTo.new(self, id, options)
10
11
  end
11
12
 
12
- def has_one(association, options={})
13
- create_external_association(:one, association, options)
13
+ def has_one(id, options={})
14
+ create_external_association RPCMapper::Association::HasOne.new(self, id, options)
14
15
  end
15
16
 
16
- def has_many(association, options={})
17
- create_external_association(:many, association, options)
17
+ def has_many(id, options={})
18
+ create_external_association RPCMapper::Association::HasMany.new(self, id, options)
18
19
  end
19
20
 
20
21
  protected
21
22
 
22
- def create_external_association(association_type, association, association_options={})
23
- cache_ivar = "@association_#{association}"
24
- self.declared_associations[association] = [association_type, association_options]
23
+ def create_external_association(association)
24
+ cache_ivar = "@association_#{association.id}"
25
+ self.defined_associations[association.id] = association
25
26
 
26
- define_method(association) do
27
- type = self.class.declared_associations[association].first
28
- options = self.class.declared_associations[association].last.dup
29
- options = options.is_a?(Proc) ? options.call(self) : options
30
- klass = klass_from_association_options(association, options)
27
+ define_method(association.id) do
31
28
  cached_value = instance_variable_get(cache_ivar)
32
29
 
33
- options[:primary_key] = (options[:primary_key] || "id").to_sym
34
- options[:foreign_key] = (options[:foreign_key] || ((type == :belongs) ? "#{association}_id" : "#{self.class.name.split('::').last.downcase}_id")).to_sym
35
-
36
30
  # TRP: Logic for actually pulling setting the value
37
31
  unless cached_value
38
- query_options = build_query_options_for_external_association(type, options)
39
- cached_value = case type
40
- when :belongs, :one
41
- klass.first(query_options) if query_options[:conditions] # TRP: Only run query if conditions is not nil
42
- when :many
43
- klass.apply_finder_options(query_options) if query_options[:conditions] # TRP: Only apply scopes if conditions is not nil
44
- end
32
+ scoped = association.scope(self)
33
+
34
+ cached_value = association.collection? ? scoped : scoped.first if scoped && !scoped.where_values.empty?
35
+
45
36
  instance_variable_set(cache_ivar, cached_value)
46
37
  end
47
38
 
@@ -49,7 +40,7 @@ module RPCMapper::Associations
49
40
  end
50
41
 
51
42
  # TRP: Allow eager loading of the association without having to load it through the normal accessor
52
- define_method("#{association}=") do |value|
43
+ define_method("#{association.id}=") do |value|
53
44
  instance_variable_set(cache_ivar, value)
54
45
  end
55
46
  end
@@ -57,39 +48,6 @@ module RPCMapper::Associations
57
48
  end
58
49
 
59
50
  module InstanceMethods
60
-
61
- protected
62
-
63
- def build_query_options_for_external_association(type, options)
64
- as = options[:as]
65
- as_type = self.class.send(:base_class_name, self.class) if as
66
-
67
- pk = options[:primary_key].to_sym
68
- fk = (as ? "#{as}_id" : options[:foreign_key]).to_sym
69
-
70
- default_query_options = case type
71
- when :belongs
72
- { :conditions => (self[fk] ? { pk => self[fk] } : nil) } # TRP: Only add conditions if the fk is not nil
73
- when :one, :many
74
- # TRP: Only add conditions if the pk is not nil
75
- if self[pk]
76
- conditions = { fk => self[pk] }
77
- conditions.merge!(:"#{as}_type" => as_type) if as
78
- { :conditions => conditions }
79
- else
80
- {}
81
- end
82
- end
83
-
84
- configured_query_options = {}
85
- RPCMapper::Relation::FINDER_OPTIONS.each do |key|
86
- value = options[key]
87
- configured_query_options.merge!({ key => value.respond_to?(:call) ? value.call(self) : value }) if value
88
- end
89
-
90
- default_query_options.deep_merge(configured_query_options)
91
- end
92
-
93
51
  end
94
52
 
95
53
  def self.included(receiver)
@@ -2,6 +2,7 @@
2
2
  require 'rpc_mapper/errors'
3
3
  require 'rpc_mapper/associations/contains'
4
4
  require 'rpc_mapper/associations/external'
5
+ require 'rpc_mapper/association_preload'
5
6
  require 'rpc_mapper/cacheable'
6
7
  require 'rpc_mapper/serialization'
7
8
  require 'rpc_mapper/relation'
@@ -12,16 +13,17 @@ require 'rpc_mapper/adapters'
12
13
  class RPCMapper::Base
13
14
  include RPCMapper::Associations::Contains
14
15
  include RPCMapper::Associations::External
16
+ include RPCMapper::AssociationPreload
15
17
  include RPCMapper::Serialization
16
18
  include RPCMapper::Scopes
17
19
 
18
20
  attr_accessor :attributes, :new_record, :read_options, :write_options
19
21
  alias :new_record? :new_record
20
22
 
21
- class_inheritable_accessor :read_adapter, :write_adapter, :cacheable, :defined_attributes, :scoped_methods, :declared_associations
23
+ class_inheritable_accessor :read_adapter, :write_adapter, :cacheable, :defined_attributes, :scoped_methods, :defined_associations
22
24
 
23
25
  self.cacheable = false
24
- self.declared_associations = {}
26
+ self.defined_associations = {}
25
27
  self.defined_attributes = []
26
28
 
27
29
  def initialize(attributes={})
@@ -88,8 +90,9 @@ class RPCMapper::Base
88
90
 
89
91
  protected
90
92
 
91
- def fetch_records(options={})
92
- self.read_adapter.read(options).collect { |hash| self.new_from_data_store(hash) }.compact
93
+ def fetch_records(relation)
94
+ options = relation.to_hash
95
+ self.read_adapter.read(options).collect { |hash| self.new_from_data_store(hash) }.compact.tap { |result| eager_load_associations(result, relation) }
93
96
  end
94
97
 
95
98
  def read_with(adapter_type)
@@ -20,7 +20,8 @@ module RPCMapper::Cacheable
20
20
 
21
21
  protected
22
22
 
23
- def fetch_records_with_caching(options={})
23
+ def fetch_records_with_caching(relation)
24
+ options = relation.to_hash
24
25
  key = Digest::MD5.hexdigest(self.to_s + options.to_a.sort { |a,b| a.to_s.first <=> b.to_s.first }.inspect)
25
26
  cache_hit = self.cache_store.read(key)
26
27
  get_fresh = options.delete(:fresh)
@@ -30,7 +31,7 @@ module RPCMapper::Cacheable
30
31
  cache_hit.each { |fv| fv.fresh = false.freeze }
31
32
  cache_hit
32
33
  else
33
- fresh_value = fetch_records_without_caching(options)
34
+ fresh_value = fetch_records_without_caching(relation)
34
35
 
35
36
  # TRP: Only store in cache if record count is below the cache_record_count_threshold (if it is set)
36
37
  if !self.cache_record_count_threshold || fresh_value.size <= self.cache_record_count_threshold
@@ -12,4 +12,13 @@ module RPCMapper
12
12
  class RecordNotSaved < RPCMapperError
13
13
  end
14
14
 
15
+ # Raised when trying to eager load an association that relies on instance level data.
16
+ # class Article < RPCMapper::Base
17
+ # has_many :comments, :conditions => lambda { |article| ... }
18
+ # end
19
+ #
20
+ # Article.recent.includes(:comments) # Raises AssociationPreloadNotSupported
21
+ class AssociationPreloadNotSupported < RPCMapperError
22
+ end
23
+
15
24
  end
@@ -18,11 +18,11 @@ module RPCMapper::FinderMethods
18
18
  end
19
19
 
20
20
  def all(options={})
21
- self.apply_finder_options(options).fetch_records
21
+ self.apply_finder_options(options).to_a
22
22
  end
23
23
 
24
24
  def first(options={})
25
- self.apply_finder_options(options).limit(1).fetch_records.first
25
+ self.apply_finder_options(options).limit(1).to_a.first
26
26
  end
27
27
 
28
28
  def search(options={})
@@ -1,8 +1,8 @@
1
1
  module RPCMapper::QueryMethods
2
2
  # TRP: Define each of the variables the query options will be stored in.
3
- attr_accessor :select_values, :group_values, :order_values, :joins_values, :where_values,
4
- :having_values, :limit_value, :offset_value, :from_value, :raw_sql_value,
5
- :fresh_value
3
+ attr_accessor :select_values, :group_values, :order_values, :joins_values, :includes_values, :where_values, :having_values,
4
+ :limit_value, :offset_value, :from_value,
5
+ :raw_sql_value, :fresh_value
6
6
 
7
7
  def select(*args)
8
8
  if block_given?
@@ -24,8 +24,13 @@ module RPCMapper::QueryMethods
24
24
  clone.tap { |r| r.joins_values += args if args && !args.empty? }
25
25
  end
26
26
 
27
+ def includes(*args)
28
+ args.reject! { |a| a.nil? }
29
+ clone.tap { |r| r.includes_values += (r.includes_values + args).flatten.uniq if args && !args.empty? }
30
+ end
31
+
27
32
  def where(*args)
28
- clone.tap { |r| r.where_values += args if args && !args.empty? }
33
+ clone.tap { |r| r.where_values += args.compact.reject(&:empty?) if args && !args.empty? }
29
34
  end
30
35
 
31
36
  def having(*args)
@@ -45,7 +50,10 @@ module RPCMapper::QueryMethods
45
50
  end
46
51
 
47
52
  def fresh(val=true)
48
- clone.tap { |r| r.fresh_value = val }
53
+ clone.tap do |r|
54
+ r.fresh_value = val
55
+ r.instance_variable_set(:@records, nil) if r.fresh_value
56
+ end
49
57
  end
50
58
 
51
59
  def sql(raw_sql)
@@ -5,8 +5,9 @@ require 'rpc_mapper/relation/finder_methods'
5
5
  # => http://github.com/rails/rails
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
+ attr_reader :klass
8
9
  SINGLE_VALUE_METHODS = [:limit, :offset, :from, :fresh]
9
- MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having]
10
+ MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :includes, :where, :having]
10
11
 
11
12
  FINDER_OPTIONS = SINGLE_VALUE_METHODS + MULTI_VALUE_METHODS + [:conditions, :search, :sql]
12
13
 
@@ -38,7 +39,7 @@ class RPCMapper::Relation
38
39
 
39
40
  def to_hash
40
41
  # TRP: If present pass :sql option alone as it trumps all other options
41
- if self.raw_sql_value
42
+ @hash ||= if self.raw_sql_value
42
43
  { :sql => raw_sql_value }
43
44
  else
44
45
  hash = SINGLE_VALUE_METHODS.inject({}) do |h, option|
@@ -64,6 +65,10 @@ class RPCMapper::Relation
64
65
  @records ||= fetch_records
65
66
  end
66
67
 
68
+ def eager_load?
69
+ @includes_values && !@includes_values.empty?
70
+ end
71
+
67
72
  def inspect
68
73
  to_a.inspect
69
74
  end
@@ -119,7 +124,7 @@ class RPCMapper::Relation
119
124
  end
120
125
 
121
126
  def fetch_records
122
- @klass.send(:fetch_records, to_hash)
127
+ @klass.send(:fetch_records, self)
123
128
  end
124
129
 
125
130
  end
@@ -2,8 +2,8 @@ module RPCMapper
2
2
  module Version
3
3
 
4
4
  MAJOR = 0
5
- MINOR = 1
6
- TINY = 6
5
+ MINOR = 2
6
+ TINY = 0
7
7
 
8
8
  def self.to_s # :nodoc:
9
9
  [MAJOR, MINOR, TINY].join('.')
metadata CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 6
10
- version: 0.1.6
8
+ - 2
9
+ - 0
10
+ version: 0.2.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-11-18 00:00:00 -06:00
18
+ date: 2010-11-22 00:00:00 -06:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -127,6 +127,8 @@ files:
127
127
  - lib/rpc_mapper/adapters/bertrpc_adapter.rb
128
128
  - lib/rpc_mapper/adapters/restful_http_adapter.rb
129
129
  - lib/rpc_mapper/adapters.rb
130
+ - lib/rpc_mapper/association.rb
131
+ - lib/rpc_mapper/association_preload.rb
130
132
  - lib/rpc_mapper/associations/common.rb
131
133
  - lib/rpc_mapper/associations/contains.rb
132
134
  - lib/rpc_mapper/associations/external.rb