perry 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,69 @@
1
+ module Perry::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
+ force_fresh = relation.fresh_value
8
+
9
+ unless association
10
+ raise(
11
+ Perry::AssociationNotFound,
12
+ "no such association (#{association_id})"
13
+ )
14
+ end
15
+
16
+ unless association.eager_loadable?
17
+ raise(
18
+ Perry::AssociationPreloadNotSupported,
19
+ "delayed execution options (block options) cannot be used for eager loaded associations"
20
+ )
21
+ end
22
+
23
+ options = association.options
24
+
25
+ case association.type
26
+ when :has_many, :has_one
27
+ fks = original_results.collect { |record| record.send(association.primary_key) }.compact
28
+
29
+ pre_records = association.target_klass.where(association.foreign_key => fks).all(:fresh => force_fresh)
30
+
31
+ original_results.each do |record|
32
+ pk = record.send(association.primary_key)
33
+ relevant_records = pre_records.select { |r| r.send(association.foreign_key) == pk }
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
+
43
+ record.send("#{association.id}=", relevant_records)
44
+ end
45
+
46
+ when :belongs_to
47
+ fks = original_results.collect { |record| record.send(association.foreign_key) }.compact
48
+
49
+ pre_records = association.target_klass.where(association.primary_key => fks).all(:fresh => force_fresh)
50
+
51
+ original_results.each do |record|
52
+ fk = record.send(association.foreign_key)
53
+ relevant_records = pre_records.select { |r| r.send(association.primary_key) == fk }.first
54
+ record.send("#{association.id}=", relevant_records)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ module InstanceMethods
63
+ end
64
+
65
+ def self.included(receiver)
66
+ receiver.extend ClassMethods
67
+ receiver.send :include, InstanceMethods
68
+ end
69
+ end
@@ -0,0 +1,51 @@
1
+ module Perry::Associations
2
+
3
+ module Common
4
+ module ClassMethods
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
+
15
+ protected
16
+
17
+ def extension_map
18
+ @@extension_map ||= Hash.new(Array.new)
19
+ end
20
+
21
+ def update_extension_map(old_klass, new_klass)
22
+ self.extension_map[old_klass] += [new_klass] if base_class_name(old_klass) == base_class_name(new_klass)
23
+ end
24
+
25
+ end
26
+
27
+ module InstanceMethods
28
+
29
+ protected
30
+
31
+ def klass_from_association_options(association, options)
32
+ klass = if options[:polymorphic]
33
+ eval [options[:polymorphic_namespace], self.send("#{association}_type")].compact.join('::')
34
+ else
35
+ raise(ArgumentError, ":class_name option required for association declaration.") unless options[:class_name]
36
+ options[:class_name] = "::#{options[:class_name]}" unless options[:class_name] =~ /^::/
37
+ eval(options[:class_name])
38
+ end
39
+
40
+ self.class.send :resolve_leaf_klass, klass
41
+ end
42
+
43
+ end
44
+
45
+ def self.included(receiver)
46
+ receiver.extend ClassMethods
47
+ receiver.send :include, InstanceMethods
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,64 @@
1
+ require 'perry/associations/common'
2
+
3
+ module Perry::Associations
4
+
5
+ module Contains
6
+ module ClassMethods
7
+
8
+ # TRP: Define an association that is serialized within the data for this class.
9
+ # If a subset of the data returned for a class contains the data for another class you can use the contains_many
10
+ # association to (lazy) auto initialize the specified object(s) using the data from that attribute.
11
+ def contains_many(association, options={})
12
+ create_contains_association(:many, association, options)
13
+ end
14
+
15
+ # TRP: Same as contains_many, but only works with a single record
16
+ def contains_one(association, options={})
17
+ create_contains_association(:one, association, options)
18
+ end
19
+
20
+ private
21
+
22
+ def create_contains_association(type, association, options)
23
+ attribute = (options[:attribute] || association).to_sym
24
+ cache_variable = "@association_#{association}"
25
+
26
+ self.defined_attributes << association.to_s unless self.defined_attributes.include?(association.to_s)
27
+ define_method(association) do
28
+ klass = klass_from_association_options(association, options)
29
+ records = instance_variable_get(cache_variable)
30
+
31
+ unless records
32
+ records = case type
33
+ when :many
34
+ self[attribute].collect { |record| klass.new_from_data_store(record) } if self[attribute]
35
+ when :one
36
+ klass.new_from_data_store(self[attribute]) if self[attribute]
37
+ end
38
+ instance_variable_set(cache_variable, records)
39
+ end
40
+
41
+ records
42
+ end
43
+
44
+ # TRP: This method will allow eager loaded values to be loaded into the association
45
+ define_method("#{association}=") do |value|
46
+ instance_variable_set(cache_variable, value)
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+
53
+ module InstanceMethods
54
+
55
+ end
56
+
57
+ def self.included(receiver)
58
+ receiver.send :include, Perry::Associations::Common
59
+ receiver.extend ClassMethods
60
+ receiver.send :include, InstanceMethods
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,67 @@
1
+ require 'perry/associations/common'
2
+ require 'perry/association'
3
+
4
+ module Perry::Associations
5
+
6
+ module External
7
+ module ClassMethods
8
+
9
+ def belongs_to(id, options={})
10
+ create_external_association Perry::Association::BelongsTo.new(self, id, options)
11
+ end
12
+
13
+ def has_one(id, options={})
14
+ create_external_association Perry::Association::HasOne.new(self, id, options)
15
+ end
16
+
17
+ def has_many(id, options={})
18
+ klass = if options.include?(:through)
19
+ Perry::Association::HasManyThrough
20
+ else
21
+ Perry::Association::HasMany
22
+ end
23
+ create_external_association klass.new(self, id, options)
24
+ end
25
+
26
+ protected
27
+
28
+ def create_external_association(association)
29
+ cache_ivar = "@association_#{association.id}"
30
+ self.defined_associations[association.id] = association
31
+
32
+ define_method(association.id) do
33
+ cached_value = instance_variable_get(cache_ivar)
34
+
35
+ # TRP: Logic for actually pulling & setting the value
36
+ unless cached_value
37
+ scoped = association.scope(self)
38
+
39
+ if scoped && !scoped.where_values.empty?
40
+ cached_value = association.collection? ? scoped : scoped.first
41
+ end
42
+
43
+ instance_variable_set(cache_ivar, cached_value)
44
+ end
45
+
46
+ cached_value
47
+ end
48
+
49
+ # TRP: Allow eager loading of the association without having to load it through the normal accessor
50
+ define_method("#{association.id}=") do |value|
51
+ instance_variable_set(cache_ivar, value)
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ module InstanceMethods
58
+ end
59
+
60
+ def self.included(receiver)
61
+ receiver.send :include, Perry::Associations::Common
62
+ receiver.extend ClassMethods
63
+ receiver.send :include, InstanceMethods
64
+ end
65
+ end
66
+
67
+ end
data/lib/perry/base.rb ADDED
@@ -0,0 +1,204 @@
1
+ # TRP: Perry modules
2
+ require 'perry/errors'
3
+ require 'perry/associations/contains'
4
+ require 'perry/associations/external'
5
+ require 'perry/association_preload'
6
+ require 'perry/cacheable'
7
+ require 'perry/serialization'
8
+ require 'perry/relation'
9
+ require 'perry/scopes'
10
+ require 'perry/adapters'
11
+
12
+
13
+ class Perry::Base
14
+ include Perry::Associations::Contains
15
+ include Perry::Associations::External
16
+ include Perry::AssociationPreload
17
+ include Perry::Serialization
18
+ include Perry::Scopes
19
+
20
+ attr_accessor :attributes, :new_record, :read_options, :write_options
21
+ alias :new_record? :new_record
22
+
23
+ class_inheritable_accessor :read_adapter, :write_adapter, :cacheable,
24
+ :defined_attributes, :scoped_methods, :defined_associations
25
+
26
+ self.cacheable = false
27
+ self.defined_associations = {}
28
+ self.defined_attributes = []
29
+
30
+ def initialize(attributes={})
31
+ self.new_record = true
32
+ set_attributes(attributes)
33
+ end
34
+
35
+ def [](attribute)
36
+ @attributes[attribute.to_s]
37
+ end
38
+
39
+ protected
40
+
41
+ # TRP: Common interface for setting attributes to keep things consistent; if
42
+ # force is true the defined_attributes list will be ignored
43
+ def set_attributes(attributes, force=false)
44
+ attributes = attributes.inject({}) do |options, (key, value)|
45
+ options[key.to_s] = value
46
+ options
47
+ end
48
+ @attributes = {} if @attributes.nil?
49
+ @attributes.merge!(attributes.reject { |field, value|
50
+ !self.defined_attributes.include?(field) && !force
51
+ })
52
+ end
53
+
54
+ def set_attribute(attribute, value, force=false)
55
+ set_attributes({ attribute => value }, force)
56
+ end
57
+
58
+ # Class Methods
59
+ class << self
60
+ public
61
+
62
+ delegate :find, :first, :all, :search, :apply_finder_options, :to => :scoped
63
+ delegate :select, :group, :order, :joins, :where, :having, :limit, :offset,
64
+ :from, :fresh, :to => :scoped
65
+
66
+ def new_from_data_store(hash)
67
+ if hash.nil?
68
+ nil
69
+ else
70
+ record = self.new(hash)
71
+ record.new_record = false
72
+ record
73
+ end
74
+ end
75
+
76
+ def unscoped
77
+ current_scopes = self.scoped_methods
78
+ self.scoped_methods = []
79
+ begin
80
+ yield
81
+ ensure
82
+ self.scoped_methods = current_scopes
83
+ end
84
+ end
85
+
86
+ def inherited(subclass)
87
+ update_extension_map(self, subclass)
88
+ super
89
+ end
90
+
91
+ def respond_to?(method, include_private=false)
92
+ super ||
93
+ scoped.dynamic_finder_method(method)
94
+ end
95
+
96
+ protected
97
+
98
+ def fetch_records(relation)
99
+ options = relation.to_hash
100
+ self.read_adapter.read(options).collect { |hash| self.new_from_data_store(hash) }.compact.tap { |result| eager_load_associations(result, relation) }
101
+ end
102
+
103
+ def read_with(adapter_type)
104
+ setup_adapter(:read, { :type => adapter_type })
105
+ end
106
+
107
+ def write_with(adapter_type)
108
+ if write_adapter
109
+ write_inheritable_attribute :write_adapter, nil if adapter_type != write_adapter.type
110
+ else
111
+ # TRP: Pull in methods and libraries needed for mutable functionality
112
+ require 'perry/persistence' unless defined?(Perry::Persistence)
113
+ self.send(:include, Perry::Persistence) unless self.class.ancestors.include?(Perry::Persistence)
114
+
115
+ # TRP: Create writers if attributes are declared before configure_mutable is called
116
+ self.defined_attributes.each { |attribute| create_writer(attribute) }
117
+ # TRP: Create serialized writers if attributes are declared serialized before this call
118
+ self.serialized_attributes.each { |attribute| set_serialize_writers(attribute) }
119
+ end
120
+ setup_adapter(:write, { :type => adapter_type })
121
+ end
122
+
123
+ def configure_read(&block)
124
+ configure_adapter(:read, &block)
125
+ end
126
+
127
+ def configure_write(&block)
128
+ configure_adapter(:write, &block)
129
+ end
130
+
131
+ def method_missing(method, *args, &block)
132
+ if scoped.dynamic_finder_method(method)
133
+ scoped.send(method, *args, &block)
134
+ else
135
+ super
136
+ end
137
+ end
138
+
139
+ def configure_cacheable(options={})
140
+ unless cacheable
141
+ self.send(:include, Perry::Cacheable)
142
+ self.enable_caching(options)
143
+ end
144
+ end
145
+
146
+ # TRP: Used to declare attributes -- only attributes that are declared will be available
147
+ def attributes(*attributes)
148
+ return self.defined_attributes if attributes.empty?
149
+
150
+ [*attributes].each do |attribute|
151
+ self.defined_attributes << attribute.to_s
152
+
153
+ define_method(attribute) do
154
+ self[attribute]
155
+ end
156
+
157
+ # TRP: Setup the writers if mutable is set
158
+ create_writer(attribute) if self.write_adapter
159
+
160
+ end
161
+ end
162
+ def attribute(*attrs)
163
+ self.attributes(*attrs)
164
+ end
165
+
166
+ def relation
167
+ @relation ||= Perry::Relation.new(self)
168
+ end
169
+
170
+ def default_scope(scope)
171
+ base_scope = current_scope || relation
172
+ self.scoped_methods << (scope.is_a?(Hash) ? base_scope.apply_finder_options(scope) : base_scope.merge(scope))
173
+ end
174
+
175
+ def current_scope
176
+ self.scoped_methods ||= []
177
+ self.scoped_methods.last
178
+ end
179
+
180
+ private
181
+
182
+ def setup_adapter(mode, config)
183
+ current_adapter = read_inheritable_attribute :"#{mode}_adapter"
184
+ type = config[:type] if config.is_a?(Hash)
185
+
186
+ new_adapter = if current_adapter
187
+ current_adapter.extend_adapter(config)
188
+ else
189
+ Perry::Adapters::AbstractAdapter.create(type, config)
190
+ end
191
+
192
+ write_inheritable_attribute :"#{mode}_adapter", new_adapter
193
+ end
194
+
195
+ def configure_adapter(mode, &block)
196
+ raise ArgumentError, "A block must be passed to configure_#{mode} method." unless block_given?
197
+ raise ArgumentError, "You must first define an adapter before configuring it. Use #{mode}_with :adapter_type." unless self.send(:"#{mode}_adapter")
198
+
199
+ setup_adapter(mode, block)
200
+ end
201
+
202
+ end
203
+
204
+ end