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 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,10 @@
1
+ module RPCMapper::Adapters
2
+ class BERTRPCAdapter < RPCMapper::Adapters::AbstractAdapter
3
+ register_as :bertrpc
4
+
5
+ def service
6
+ @service ||= BERTRPC::Service.new(self.options[:host], self.options[:port])
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,4 @@
1
+ module RPCMapper::Adapters; end
2
+
3
+ require 'rpc_mapper/adapters/abstract_adapter'
4
+ require 'rpc_mapper/adapters/bertrpc_adapter'
@@ -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