rpc-mapper 0.0.1
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 +61 -0
- data/lib/rpc_mapper/adapters/abstract_adapter.rb +36 -0
- data/lib/rpc_mapper/adapters/bertrpc_adapter.rb +10 -0
- data/lib/rpc_mapper/adapters.rb +4 -0
- data/lib/rpc_mapper/associations/common.rb +51 -0
- data/lib/rpc_mapper/associations/contains.rb +64 -0
- data/lib/rpc_mapper/associations/external.rb +102 -0
- data/lib/rpc_mapper/base.rb +202 -0
- data/lib/rpc_mapper/cacheable/entry.rb +18 -0
- data/lib/rpc_mapper/cacheable/store.rb +45 -0
- data/lib/rpc_mapper/cacheable.rb +68 -0
- data/lib/rpc_mapper/config_options.rb +41 -0
- data/lib/rpc_mapper/core_ext/kernel/singleton_class.rb +14 -0
- data/lib/rpc_mapper/logger.rb +54 -0
- data/lib/rpc_mapper/mutable.rb +138 -0
- data/lib/rpc_mapper/relation/finder_methods.rb +52 -0
- data/lib/rpc_mapper/relation/query_methods.rb +50 -0
- data/lib/rpc_mapper/relation.rb +125 -0
- data/lib/rpc_mapper/scopes/conditions.rb +101 -0
- data/lib/rpc_mapper/scopes.rb +45 -0
- data/lib/rpc_mapper/serialization.rb +48 -0
- data/lib/rpc_mapper/version.rb +13 -0
- data/lib/rpc_mapper.rb +30 -0
- metadata +184 -0
data/README.rdoc
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
= RPCMapper
|
2
|
+
|
3
|
+
== Description
|
4
|
+
|
5
|
+
Ruby library for querying and mapping data over RPC
|
6
|
+
|
7
|
+
== Installation
|
8
|
+
|
9
|
+
gem install rpc-mapper
|
10
|
+
|
11
|
+
== Usage
|
12
|
+
|
13
|
+
require 'rpc_mapper'
|
14
|
+
|
15
|
+
== License
|
16
|
+
|
17
|
+
Copyright (c) 2010 Travis Petticrew
|
18
|
+
|
19
|
+
Permission is hereby granted, free of charge, to any person
|
20
|
+
obtaining a copy of this software and associated documentation
|
21
|
+
files (the "Software"), to deal in the Software without
|
22
|
+
restriction, including without limitation the rights to use,
|
23
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
24
|
+
copies of the Software, and to permit persons to whom the
|
25
|
+
Software is furnished to do so, subject to the following
|
26
|
+
conditions:
|
27
|
+
|
28
|
+
The above copyright notice and this permission notice shall be
|
29
|
+
included in all copies or substantial portions of the Software.
|
30
|
+
|
31
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
32
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
33
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
34
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
35
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
36
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
37
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
38
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
require 'lib/rpc_mapper/version'
|
6
|
+
|
7
|
+
spec = Gem::Specification.new do |s|
|
8
|
+
s.name = 'rpc-mapper'
|
9
|
+
s.version = RPCMapper::Version.to_s
|
10
|
+
s.has_rdoc = true
|
11
|
+
s.extra_rdoc_files = %w(README.rdoc)
|
12
|
+
s.rdoc_options = %w(--main README.rdoc)
|
13
|
+
s.summary = "Ruby library for querying and mapping data over RPC"
|
14
|
+
s.author = 'Travis Petticrew'
|
15
|
+
s.email = 'bobo@petticrew.net'
|
16
|
+
s.homepage = 'http://github.com/tpett/rpc-mapper'
|
17
|
+
s.files = %w(README.rdoc Rakefile) + Dir.glob("{lib}/**/*")
|
18
|
+
# s.executables = ['rpc-mapper']
|
19
|
+
|
20
|
+
s.add_development_dependency("shoulda", [">= 2.10.0"])
|
21
|
+
s.add_development_dependency("leftright", [">= 0.0.6"])
|
22
|
+
s.add_development_dependency("fakeweb", [">= 1.3.0"])
|
23
|
+
s.add_development_dependency("factory_girl", [">= 0"])
|
24
|
+
|
25
|
+
s.add_dependency("activesupport", [">= 2.3.0"])
|
26
|
+
s.add_dependency("bertrpc", [">= 1.3.0"])
|
27
|
+
end
|
28
|
+
|
29
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
30
|
+
pkg.gem_spec = spec
|
31
|
+
end
|
32
|
+
|
33
|
+
Rake::TestTask.new do |t|
|
34
|
+
t.libs << 'test'
|
35
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
36
|
+
t.verbose = true
|
37
|
+
end
|
38
|
+
|
39
|
+
begin
|
40
|
+
require 'rcov/rcovtask'
|
41
|
+
|
42
|
+
Rcov::RcovTask.new(:coverage) do |t|
|
43
|
+
t.libs = ['test']
|
44
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
45
|
+
t.verbose = true
|
46
|
+
t.rcov_opts = ['--text-report', "-x #{Gem.path}", '-x /Library/Ruby', '-x /usr/lib/ruby']
|
47
|
+
end
|
48
|
+
|
49
|
+
task :default => :coverage
|
50
|
+
|
51
|
+
rescue LoadError
|
52
|
+
warn "\n**** Install rcov (sudo gem install relevance-rcov) to get coverage stats ****\n"
|
53
|
+
task :default => :test
|
54
|
+
end
|
55
|
+
|
56
|
+
desc 'Generate the gemspec to serve this gem'
|
57
|
+
task :gemspec do
|
58
|
+
file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
|
59
|
+
File.open(file, 'w') {|f| f << spec.to_ruby }
|
60
|
+
puts "Created gemspec: #{file}"
|
61
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rpc_mapper/logger'
|
2
|
+
|
3
|
+
module RPCMapper::Adapters
|
4
|
+
class AbstractAdapter
|
5
|
+
include RPCMapper::Logger
|
6
|
+
|
7
|
+
attr_accessor :options
|
8
|
+
@@registered_adapters ||= {}
|
9
|
+
|
10
|
+
def initialize(options={})
|
11
|
+
@options = options.dup
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.create(type, options={})
|
15
|
+
type = type ? @@registered_adapters[type.to_sym] : self
|
16
|
+
type.new(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(procedure, options={})
|
20
|
+
log(options, "#{service_log_prefix} #{procedure}") { self.service.call.data_server.send(procedure, options) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def service
|
24
|
+
raise NotImplementedError, "You must not use the AbstractAdapter. Implement an adapter that extends the AbstractAdapter class and overrides this method."
|
25
|
+
end
|
26
|
+
|
27
|
+
def service_log_prefix
|
28
|
+
"RPC"
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.register_as(name)
|
32
|
+
@@registered_adapters[name.to_sym] = self
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module RPCMapper::Associations
|
2
|
+
|
3
|
+
module Common
|
4
|
+
module ClassMethods
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
def extension_map
|
9
|
+
@@extension_map ||= Hash.new(Array.new)
|
10
|
+
end
|
11
|
+
|
12
|
+
def update_extension_map(old_klass, new_klass)
|
13
|
+
self.extension_map[old_klass] += [new_klass] if base_class_name(old_klass) == base_class_name(new_klass)
|
14
|
+
end
|
15
|
+
|
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
|
+
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 or :klass option required for association declaration.") unless options[:class_name] || options[:klass]
|
36
|
+
options[:class_name] = "::#{options[:class_name]}" unless options[:class_name] =~ /^::/
|
37
|
+
options[:klass] || 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 'rpc_mapper/associations/common'
|
2
|
+
|
3
|
+
module RPCMapper::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, RPCMapper::Associations::Common
|
59
|
+
receiver.extend ClassMethods
|
60
|
+
receiver.send :include, InstanceMethods
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'rpc_mapper/associations/common'
|
2
|
+
|
3
|
+
module RPCMapper::Associations
|
4
|
+
|
5
|
+
module External
|
6
|
+
module ClassMethods
|
7
|
+
|
8
|
+
def belongs_to(association, options={})
|
9
|
+
create_external_association(:belongs, association, options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def has_one(association, options={})
|
13
|
+
create_external_association(:one, association, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def has_many(association, options={})
|
17
|
+
create_external_association(:many, association, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def create_external_association(association_type, association, association_options={})
|
23
|
+
cache_ivar = "@association_#{association}"
|
24
|
+
self.declared_associations[association] = [association_type, association_options]
|
25
|
+
|
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)
|
31
|
+
cached_value = instance_variable_get(cache_ivar)
|
32
|
+
|
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
|
+
# TRP: Logic for actually pulling setting the value
|
37
|
+
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.all(query_options) if query_options[:conditions] # TRP: Only run query if conditions is not nil
|
44
|
+
end
|
45
|
+
instance_variable_set(cache_ivar, cached_value)
|
46
|
+
end
|
47
|
+
|
48
|
+
cached_value
|
49
|
+
end
|
50
|
+
|
51
|
+
# TRP: Allow eager loading of the association without having to load it through the normal accessor
|
52
|
+
define_method("#{association}=") do |value|
|
53
|
+
instance_variable_set(cache_ivar, value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
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
|
+
end
|
94
|
+
|
95
|
+
def self.included(receiver)
|
96
|
+
receiver.send :include, RPCMapper::Associations::Common
|
97
|
+
receiver.extend ClassMethods
|
98
|
+
receiver.send :include, InstanceMethods
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# TRP: Cherry pick some goodies from active_support
|
2
|
+
require 'active_support/core_ext/array'
|
3
|
+
begin
|
4
|
+
require 'active_support/core_ext/duplicable' #ActiveSupport 2.3.5
|
5
|
+
rescue LoadError => exception
|
6
|
+
require 'active_support/core_ext/object/duplicable' #ActiveSupport 3.0.0.RC
|
7
|
+
end
|
8
|
+
require 'active_support/core_ext/class/inheritable_attributes'
|
9
|
+
require 'active_support/core_ext/hash/deep_merge'
|
10
|
+
require 'active_support/core_ext/module/delegation'
|
11
|
+
Hash.send(:include, ActiveSupport::CoreExtensions::Hash::DeepMerge) unless Hash.new.respond_to?(:deep_merge)
|
12
|
+
|
13
|
+
# TRP: Used for pretty logging
|
14
|
+
require 'benchmark'
|
15
|
+
|
16
|
+
# TRP: RPCMapper core_ext
|
17
|
+
require 'rpc_mapper/core_ext/kernel/singleton_class'
|
18
|
+
|
19
|
+
# TRP: RPCMapper modules
|
20
|
+
require 'rpc_mapper/config_options'
|
21
|
+
require 'rpc_mapper/associations/contains'
|
22
|
+
require 'rpc_mapper/associations/external'
|
23
|
+
require 'rpc_mapper/cacheable'
|
24
|
+
require 'rpc_mapper/serialization'
|
25
|
+
require 'rpc_mapper/relation'
|
26
|
+
require 'rpc_mapper/scopes'
|
27
|
+
require 'rpc_mapper/adapters'
|
28
|
+
|
29
|
+
|
30
|
+
class RPCMapper::Base
|
31
|
+
include RPCMapper::ConfigOptions
|
32
|
+
include RPCMapper::Associations::Contains
|
33
|
+
include RPCMapper::Associations::External
|
34
|
+
include RPCMapper::Serialization
|
35
|
+
include RPCMapper::Scopes
|
36
|
+
|
37
|
+
attr_accessor :attributes, :new_record
|
38
|
+
alias :new_record? :new_record
|
39
|
+
|
40
|
+
class_inheritable_accessor :defined_attributes, :mutable, :cacheable, :scoped_methods, :declared_associations
|
41
|
+
config_options :rpc_server_host => 'localhost',
|
42
|
+
:rpc_server_port => 8000,
|
43
|
+
:service => nil,
|
44
|
+
:service_namespace => nil,
|
45
|
+
:adapter_type => nil,
|
46
|
+
# TRP: Only used if configure_mutable is called
|
47
|
+
:mutable_default_parameters => {},
|
48
|
+
:mutable_host => nil,
|
49
|
+
# TRP: This is used to append any options to the call (e.g. value to authenticate the client with the server, :app_key)
|
50
|
+
:default_options => {}
|
51
|
+
|
52
|
+
self.mutable = false
|
53
|
+
self.cacheable = false
|
54
|
+
self.declared_associations = {}
|
55
|
+
self.defined_attributes = []
|
56
|
+
@@adapter_pool = {}
|
57
|
+
|
58
|
+
def initialize(attributes={})
|
59
|
+
self.new_record = true
|
60
|
+
set_attributes(attributes)
|
61
|
+
end
|
62
|
+
|
63
|
+
def [](attribute)
|
64
|
+
@attributes[attribute.to_s]
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
|
69
|
+
# TRP: Common interface for setting attributes to keep things consistent
|
70
|
+
def set_attributes(attributes)
|
71
|
+
attributes = attributes.inject({}) do |options, (key, value)|
|
72
|
+
options[key.to_s] = value
|
73
|
+
options
|
74
|
+
end
|
75
|
+
@attributes = {} if @attributes.nil?
|
76
|
+
@attributes.merge!(attributes.reject { |field, value| !self.defined_attributes.include?(field) })
|
77
|
+
end
|
78
|
+
|
79
|
+
def set_attribute(attribute, value)
|
80
|
+
set_attributes({ attribute => value })
|
81
|
+
end
|
82
|
+
|
83
|
+
# Class Methods
|
84
|
+
class << self
|
85
|
+
public
|
86
|
+
|
87
|
+
delegate :find, :first, :all, :search, :to => :scoped
|
88
|
+
delegate :select, :group, :order, :joins, :where, :having, :limit, :offset, :from, :to => :scoped
|
89
|
+
|
90
|
+
def new_from_data_store(hash)
|
91
|
+
if hash.nil?
|
92
|
+
nil
|
93
|
+
else
|
94
|
+
record = self.new(hash)
|
95
|
+
record.new_record = false
|
96
|
+
record
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def unscoped
|
101
|
+
current_scopes = self.scoped_methods
|
102
|
+
self.scoped_methods = []
|
103
|
+
begin
|
104
|
+
yield
|
105
|
+
ensure
|
106
|
+
self.scoped_methods = current_scopes
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def inherited(subclass)
|
111
|
+
update_extension_map(self, subclass)
|
112
|
+
super
|
113
|
+
end
|
114
|
+
|
115
|
+
def respond_to?(method, include_private=false)
|
116
|
+
super ||
|
117
|
+
scoped.dynamic_finder_method(method)
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
|
122
|
+
def fetch_records(options={})
|
123
|
+
self.adapter.call("#{self.service_namespace}__#{self.service}", options.merge(default_options)).collect { |hash| self.new_from_data_store(hash) }.compact
|
124
|
+
end
|
125
|
+
|
126
|
+
def adapter
|
127
|
+
@@adapter_pool[self.adapter_type] ||= RPCMapper::Adapters::AbstractAdapter.create(self.adapter_type, { :host => self.rpc_server_host, :port => self.rpc_server_port })
|
128
|
+
end
|
129
|
+
|
130
|
+
def method_missing(method, *args, &block)
|
131
|
+
if scoped.dynamic_finder_method(method)
|
132
|
+
scoped.send(method, *args, &block)
|
133
|
+
else
|
134
|
+
super
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def configure(options={})
|
139
|
+
self.configure_options.each { |option| self.send("#{option}=", options[option]) if options[option] }
|
140
|
+
end
|
141
|
+
|
142
|
+
# TRP: Only pulls in mutable module if configure_mutable is called
|
143
|
+
def configure_mutable(options={})
|
144
|
+
unless mutable
|
145
|
+
self.mutable = true
|
146
|
+
|
147
|
+
# TRP: Pull in methods and libraries needed for mutable functionality
|
148
|
+
require 'rpc_mapper/mutable'
|
149
|
+
self.send(:include, RPCMapper::Mutable)
|
150
|
+
self.save_mutable_configuration(options)
|
151
|
+
|
152
|
+
# TRP: Create writers if attributes are declared before configure_mutable is called
|
153
|
+
self.defined_attributes.each { |attribute| create_writer(attribute) }
|
154
|
+
# TRP: Create serialized writers if attributes are declared serialized before this call
|
155
|
+
self.serialized_attributes.each { |attribute| set_serialize_writers(attribute) }
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def configure_cacheable(options={})
|
160
|
+
unless cacheable
|
161
|
+
self.send(:include, RPCMapper::Cacheable)
|
162
|
+
self.enable_caching(options)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# TRP: Used to declare attributes -- only attributes that are declared will be available
|
167
|
+
def attributes(*attributes)
|
168
|
+
return self.defined_attributes if attributes.empty?
|
169
|
+
|
170
|
+
[*attributes].each do |attribute|
|
171
|
+
self.defined_attributes << attribute.to_s
|
172
|
+
|
173
|
+
define_method(attribute) do
|
174
|
+
self[attribute]
|
175
|
+
end
|
176
|
+
|
177
|
+
# TRP: Setup the writers if mutable is set
|
178
|
+
create_writer(attribute) if self.mutable
|
179
|
+
|
180
|
+
end
|
181
|
+
end
|
182
|
+
def attribute(*attrs)
|
183
|
+
self.attributes(*attrs)
|
184
|
+
end
|
185
|
+
|
186
|
+
def relation
|
187
|
+
@relation ||= RPCMapper::Relation.new(self)
|
188
|
+
end
|
189
|
+
|
190
|
+
def default_scope(scope)
|
191
|
+
base_scope = current_scope || relation
|
192
|
+
self.scoped_methods << (scope.is_a?(Hash) ? base_scope.apply_finder_options(scope) : base_scope.merge(scope))
|
193
|
+
end
|
194
|
+
|
195
|
+
def current_scope
|
196
|
+
self.scoped_methods ||= []
|
197
|
+
self.scoped_methods.last
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module RPCMapper::Cacheable
|
2
|
+
|
3
|
+
class Entry
|
4
|
+
|
5
|
+
attr_accessor :value, :expire_at
|
6
|
+
|
7
|
+
def initialize(value, expire_at)
|
8
|
+
self.value = value
|
9
|
+
self.expire_at = expire_at
|
10
|
+
end
|
11
|
+
|
12
|
+
def expired?
|
13
|
+
Time.now > self.expire_at rescue true
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module RPCMapper::Cacheable
|
2
|
+
|
3
|
+
class Store
|
4
|
+
|
5
|
+
attr_reader :store
|
6
|
+
attr_accessor :default_longevity
|
7
|
+
|
8
|
+
# TRP: Specify the default longevity of a cache entry in seconds
|
9
|
+
def initialize(default_longevity)
|
10
|
+
@store = {}
|
11
|
+
@default_longevity = default_longevity
|
12
|
+
end
|
13
|
+
|
14
|
+
def clear(key=nil)
|
15
|
+
if key
|
16
|
+
@store.delete(key)
|
17
|
+
else
|
18
|
+
@store.each do |key, entry|
|
19
|
+
clear(key) if key && (!entry || entry.expired?)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# TRP: Returns value if a cache hit and the entry is not expired, nil otherwise
|
25
|
+
def read(key)
|
26
|
+
if entry = @store[key]
|
27
|
+
if entry.expired?
|
28
|
+
clear(key)
|
29
|
+
nil
|
30
|
+
else
|
31
|
+
entry.value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# TRP: Write a new cache value and optionally specify the expire time
|
37
|
+
# this also will clear all expired items out of the store to keep memory consumption as low as possible
|
38
|
+
def write(key, value, expires=Time.now + default_longevity)
|
39
|
+
clear
|
40
|
+
@store[key] = Entry.new(value, expires)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|