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.
- data/README.rdoc +38 -0
- data/Rakefile +70 -0
- data/lib/perry.rb +48 -0
- data/lib/perry/adapters.rb +5 -0
- data/lib/perry/adapters/abstract_adapter.rb +92 -0
- data/lib/perry/adapters/bertrpc_adapter.rb +32 -0
- data/lib/perry/adapters/restful_http_adapter.rb +99 -0
- data/lib/perry/association.rb +330 -0
- data/lib/perry/association_preload.rb +69 -0
- data/lib/perry/associations/common.rb +51 -0
- data/lib/perry/associations/contains.rb +64 -0
- data/lib/perry/associations/external.rb +67 -0
- data/lib/perry/base.rb +204 -0
- data/lib/perry/cacheable.rb +80 -0
- data/lib/perry/cacheable/entry.rb +18 -0
- data/lib/perry/cacheable/store.rb +45 -0
- data/lib/perry/core_ext/kernel/singleton_class.rb +14 -0
- data/lib/perry/errors.rb +49 -0
- data/lib/perry/logger.rb +54 -0
- data/lib/perry/persistence.rb +55 -0
- data/lib/perry/relation.rb +153 -0
- data/lib/perry/relation/finder_methods.rb +56 -0
- data/lib/perry/relation/query_methods.rb +69 -0
- data/lib/perry/scopes.rb +45 -0
- data/lib/perry/scopes/conditions.rb +101 -0
- data/lib/perry/serialization.rb +48 -0
- data/lib/perry/version.rb +13 -0
- metadata +187 -0
@@ -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
|