perry 0.4.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,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