rpc-mapper 0.1.6 → 0.2.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.
@@ -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