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 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