rpc-mapper 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|