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.
@@ -0,0 +1,68 @@
1
+ require 'rpc_mapper/cacheable/store'
2
+ require 'rpc_mapper/cacheable/entry'
3
+ require 'digest/md5'
4
+
5
+ module RPCMapper::Cacheable
6
+
7
+ module ClassMethods
8
+
9
+ # TRP: Default to a 5 minute cache
10
+ DEFAULT_LONGEVITY = 5*60
11
+
12
+ protected
13
+
14
+ def fetch_records_with_caching(options={})
15
+ key = Digest::MD5.hexdigest(self.to_s + options.to_a.sort { |a,b| a.to_s.first <=> b.to_s.first }.inspect)
16
+ cache_hit = self.cacheable.read(key)
17
+
18
+ if cache_hit
19
+ self.adapter.log(options, "CACHE #{self.service_namespace}__#{self.service}")
20
+ cache_hit.each { |fv| fv.fresh = false.freeze }
21
+ cache_hit
22
+ else
23
+ fresh_value = fetch_records_without_caching(options)
24
+
25
+ # TRP: Only store in cache if record count is below the cache_record_count_threshold (if it is set)
26
+ if !self.cache_record_count_threshold || fresh_value.size <= self.cache_record_count_threshold
27
+ self.cacheable.write(key, fresh_value, (self.cache_expires) ? fresh_value[0].send(self.cache_expires) : Time.now + DEFAULT_LONGEVITY) unless fresh_value.empty?
28
+ fresh_value.each { |fv| fv.fresh = true.freeze }
29
+ end
30
+
31
+ fresh_value
32
+ end
33
+ end
34
+
35
+ def enable_caching(options)
36
+ self.cacheable = RPCMapper::Cacheable::Store.new(DEFAULT_LONGEVITY)
37
+ self.cache_expires = options[:expires]
38
+ self.cache_record_count_threshold = options[:record_count_threshold]
39
+ end
40
+
41
+ end
42
+
43
+ module InstanceMethods
44
+
45
+ def fresh?
46
+ @fresh
47
+ end
48
+
49
+ end
50
+
51
+ def self.included(receiver)
52
+ receiver.class_inheritable_accessor :cache_expires, :cache_record_count_threshold
53
+ receiver.send :attr_accessor, :fresh
54
+
55
+ receiver.extend ClassMethods
56
+ receiver.send :include, InstanceMethods
57
+
58
+ # TRP: Remap calls for RPC through the caching mechanism
59
+ receiver.class_eval do
60
+ class << self
61
+ alias_method :fetch_records_without_caching, :fetch_records
62
+ alias_method :fetch_records, :fetch_records_with_caching
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,41 @@
1
+ module RPCMapper
2
+
3
+ module ConfigOptions
4
+ module ClassMethods
5
+
6
+ def config_options(options={})
7
+ options.each do |option, default_value|
8
+ class_eval do
9
+ self.configure_options << option
10
+ class_inheritable_accessor option
11
+ self.send "#{option}=", default_value
12
+ end
13
+ # TRP: We run a direct string eval on the class because we cannot get a closure on the class << self scope
14
+ # We need that in order to use our option variable to define a class method of the same name.
15
+ self.class_eval <<-EOS
16
+ def self.#{option} # def self.my_awesome_option
17
+ val = read_inheritable_attribute(:#{option}) # val = read_inheritable_attribute(:my_awesome_option)
18
+ val.is_a?(Proc) ? write_inheritable_attribute(:#{option}, val.call) : val # val.is_a?(Proc) ? write_inheritable_attribute(:my_awesome_option, val.call) : val
19
+ end # end
20
+ EOS
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ module InstanceMethods
27
+
28
+ end
29
+
30
+ def self.included(receiver)
31
+ receiver.class_eval do
32
+ class_inheritable_accessor :configure_options
33
+ self.configure_options = []
34
+ end
35
+
36
+ receiver.extend ClassMethods
37
+ receiver.send :include, InstanceMethods
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,14 @@
1
+ # TRP: Copied from Rails 3RC1 on 8/17/2010 (not available in Rails2)
2
+ module Kernel
3
+ # Returns the object's singleton class.
4
+ def singleton_class
5
+ class << self
6
+ self
7
+ end
8
+ end unless respond_to?(:singleton_class) # exists in 1.9.2
9
+
10
+ # class_eval on an object acts like singleton_class.class_eval.
11
+ def class_eval(*args, &block)
12
+ singleton_class.class_eval(*args, &block)
13
+ end
14
+ end
@@ -0,0 +1,54 @@
1
+ module RPCMapper
2
+
3
+ module Logger
4
+ module ClassMethods
5
+
6
+ def log(params, name)
7
+ if block_given?
8
+ result = nil
9
+ ms = Benchmark.measure { result = yield }.real
10
+ log_info(params, name, ms*1000)
11
+ result
12
+ else
13
+ log_info(params, name, 0)
14
+ []
15
+ end
16
+ rescue Exception => err
17
+ log_info(params, name, 0)
18
+ RPCMapper.logger.error("#{err.message} \n\n#{err.backtrace.join('\n')}")
19
+ []
20
+ end
21
+
22
+ private
23
+
24
+ def log_info(params, name, ms)
25
+ if RPCMapper.logger && RPCMapper.logger.debug?
26
+ name = '%s (%.1fms)' % [name || 'RPC', ms]
27
+ RPCMapper.logger.debug(format_log_entry(name, params.inspect))
28
+ end
29
+ end
30
+
31
+ def format_log_entry(message, dump=nil)
32
+ message_color, dump_color = "4;33;1", "0;1"
33
+
34
+ log_entry = " \e[#{message_color}m#{message}\e[0m "
35
+ log_entry << "\e[#{dump_color}m%#{String === dump ? 's' : 'p'}\e[0m" % dump if dump
36
+ log_entry
37
+ end
38
+
39
+ end
40
+
41
+ module InstanceMethods
42
+
43
+ def log(*args, &block)
44
+ self.class.send(:log, *args, &block)
45
+ end
46
+ end
47
+
48
+ def self.included(receiver)
49
+ receiver.extend ClassMethods
50
+ receiver.send :include, InstanceMethods
51
+ end
52
+ end
53
+
54
+ end
@@ -0,0 +1,138 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+ module RPCMapper
5
+
6
+ module Mutable
7
+
8
+ module ClassMethods
9
+
10
+ protected
11
+
12
+ def save_mutable_configuration(options={})
13
+ self.mutable_service = options[:service] || self.service
14
+ self.mutable_post_body_wrapper = options[:post_body_wrapper]
15
+ end
16
+
17
+ def create_writer(attribute)
18
+ define_method("#{attribute}=") do |value|
19
+ self[attribute] = value
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+
26
+ module InstanceMethods
27
+
28
+ def []=(attribute, value)
29
+ set_attribute(attribute, value)
30
+ end
31
+
32
+ def attributes=(attributes)
33
+ set_attributes(attributes)
34
+ end
35
+
36
+ def save
37
+ new_record? ? post_http(build_params_from_attributes) : put_http(build_params_from_attributes)
38
+ end
39
+
40
+ def update_attributes(attributes)
41
+ self.attributes = attributes
42
+ save
43
+ end
44
+
45
+ def destroy
46
+ delete_http
47
+ end
48
+ alias :delete :destroy
49
+
50
+
51
+ protected
52
+
53
+
54
+ def post_http(params)
55
+ http_call(:post, params)
56
+ end
57
+
58
+ def put_http(params)
59
+ http_call(:put, params)
60
+ end
61
+
62
+ def delete_http
63
+ http_call(:delete, self.mutable_default_parameters)
64
+ end
65
+
66
+ def http_call(method, params={})
67
+ request_klass = case method
68
+ when :post then Net::HTTP::Post
69
+ when :put then Net::HTTP::Put
70
+ when :delete then Net::HTTP::Delete
71
+ end
72
+
73
+ req_url = self.build_url(method)
74
+
75
+ request = if method == :delete
76
+ request = request_klass.new([req_url.path, req_url.query].join('?'))
77
+ else
78
+ request = request_klass.new(req_url.path)
79
+ request.set_form_data(params) unless method == :delete
80
+ request
81
+ end
82
+
83
+ self.class.send(:adapter).log(params, "#{method.to_s.upcase} #{req_url}") do
84
+ @response = Net::HTTP.new(req_url.host, req_url.port).start { |http| http.request(request) }
85
+ end
86
+ parse_response_code(@response)
87
+ end
88
+
89
+ def parse_response_code(response)
90
+ case response
91
+ when Net::HTTPSuccess, Net::HTTPRedirection
92
+ true
93
+ else
94
+ false
95
+ end
96
+ end
97
+
98
+ def build_params_from_attributes
99
+ if self.mutable_post_body_wrapper
100
+ params = self.mutable_default_parameters
101
+ @attributes.each do |attribute, value|
102
+ params.merge!({"#{self.mutable_post_body_wrapper}[#{attribute}]" => value})
103
+ end
104
+ # TRP: Append ivar mutable_params to params if set
105
+ params.merge!(self.mutable_params) if self.mutable_params.is_a?(Hash)
106
+
107
+ params
108
+ else
109
+ @attributes
110
+ end
111
+ end
112
+
113
+ def build_url(method)
114
+ url = [self.mutable_host.gsub(%r{/$}, ''), self.mutable_service]
115
+ url << self.id unless self.new_record?
116
+ uri = URI.parse "#{url.join('/')}.json"
117
+
118
+ if method == :delete
119
+ uri.query = self.mutable_default_parameters.collect { |key, value| "#{key}=#{value}" }.join('&')
120
+ end
121
+
122
+ uri
123
+ end
124
+
125
+ end
126
+
127
+ def self.included(receiver)
128
+ receiver.class_inheritable_accessor :mutable_service, :mutable_post_body_wrapper
129
+ receiver.send :attr_accessor, :mutable_params
130
+ receiver.send(:attr_accessor, :response)
131
+
132
+ receiver.extend ClassMethods
133
+ receiver.send :include, InstanceMethods
134
+ end
135
+
136
+ end
137
+
138
+ end
@@ -0,0 +1,52 @@
1
+ module RPCMapper::FinderMethods
2
+
3
+ def find(ids_or_mode, options={})
4
+ case ids_or_mode
5
+ when Fixnum, String
6
+ self.where(:id => ids_or_mode.to_i).first(options)
7
+ when Array
8
+ self.where(:id => ids_or_mode).all(options)
9
+ when :all
10
+ self.all(options)
11
+ when :first
12
+ self.first(options)
13
+ else
14
+ raise ArgumentError, "Unknown arguments for method find"
15
+ end
16
+ end
17
+
18
+ def all(options={})
19
+ self.apply_finder_options(options).fetch_records
20
+ end
21
+
22
+ def first(options={})
23
+ self.apply_finder_options(options).limit(1).fetch_records.first
24
+ end
25
+
26
+ def search(options={})
27
+ relation = self
28
+ options.each do |search, *args|
29
+ relation = relation.send(search, *args) if @klass.send(:condition_details, search)
30
+ end
31
+ relation
32
+ end
33
+
34
+ def apply_finder_options(options)
35
+ relation = clone
36
+ return relation unless options
37
+
38
+ [:joins, :limit, :offset, :order, :select, :group, :having, :from].each do |finder|
39
+ relation = relation.send(finder, options[finder]) if options[finder]
40
+ end
41
+
42
+ relation = relation.where(options[:conditions]) if options.has_key?(:conditions)
43
+ relation = relation.where(options[:where]) if options.has_key?(:where)
44
+
45
+ relation = relation.search(options[:search]) if options.has_key?(:search)
46
+
47
+ relation = relation.sql(options[:sql]) if options.has_key?(:sql)
48
+
49
+ relation
50
+ end
51
+
52
+ end
@@ -0,0 +1,50 @@
1
+ module RPCMapper::QueryMethods
2
+ # TRP: Define each of the variables the query options will be stored in.
3
+ attr_accessor :select_values, :group_values, :order_values, :joins_values, :where_values,
4
+ :having_values, :limit_value, :offset_value, :from_value, :raw_sql_value
5
+
6
+ def select(*args)
7
+ if block_given?
8
+ to_a.select {|*block_args| yield(*block_args) }
9
+ else
10
+ clone.tap { |r| r.select_values += args if args && !args.empty? }
11
+ end
12
+ end
13
+
14
+ def group(*args)
15
+ clone.tap { |r| r.group_values += args if args && !args.empty? }
16
+ end
17
+
18
+ def order(*args)
19
+ clone.tap { |r| r.order_values += args if args && !args.empty? }
20
+ end
21
+
22
+ def joins(*args)
23
+ clone.tap { |r| r.joins_values += args if args && !args.empty? }
24
+ end
25
+
26
+ def where(*args)
27
+ clone.tap { |r| r.where_values += args if args && !args.empty? }
28
+ end
29
+
30
+ def having(*args)
31
+ clone.tap { |r| r.having_values += args if args && !args.empty? }
32
+ end
33
+
34
+ def limit(value = true)
35
+ clone.tap { |r| r.limit_value = value }
36
+ end
37
+
38
+ def offset(value = true)
39
+ clone.tap { |r| r.offset_value = value }
40
+ end
41
+
42
+ def from(table)
43
+ clone.tap { |r| r.from_value = table }
44
+ end
45
+
46
+ def sql(raw_sql)
47
+ clone.tap { |r| r.raw_sql_value = raw_sql }
48
+ end
49
+
50
+ end
@@ -0,0 +1,125 @@
1
+ require 'rpc_mapper/relation/query_methods'
2
+ require 'rpc_mapper/relation/finder_methods'
3
+
4
+ # TRP: The concepts behind this class are heavily influenced by ActiveRecord::Relation v.3.0.0RC1
5
+ # => http://github.com/rails/rails
6
+ # Used to achieve the chainability of scopes -- methods are delegated back and forth from BM::Base and BM::Relation
7
+ class RPCMapper::Relation
8
+ SINGLE_VALUE_METHODS = [:limit, :offset, :from]
9
+ MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having]
10
+
11
+ FINDER_OPTIONS = SINGLE_VALUE_METHODS + MULTI_VALUE_METHODS + [:conditions, :search, :sql]
12
+
13
+ include RPCMapper::QueryMethods
14
+ include RPCMapper::FinderMethods
15
+
16
+ def initialize(klass)
17
+ @klass = klass
18
+
19
+ SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)}
20
+ MULTI_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_values", [])}
21
+ end
22
+
23
+ def merge(r)
24
+ merged_relation = clone
25
+ return merged_relation unless r
26
+
27
+ SINGLE_VALUE_METHODS.each do |option|
28
+ new_value = r.send("#{option}_value")
29
+ merged_relation = merged_relation.send(option, new_value) if new_value
30
+ end
31
+
32
+ MULTI_VALUE_METHODS.each do |option|
33
+ merged_relation = merged_relation.send(option, *r.send("#{option}_values"))
34
+ end
35
+
36
+ merged_relation
37
+ end
38
+
39
+ def to_hash
40
+ # TRP: If present pass :sql option alone as it trumps all other options
41
+ if self.raw_sql_value
42
+ { :sql => raw_sql_value }
43
+ else
44
+ hash = SINGLE_VALUE_METHODS.inject({}) do |h, option|
45
+ value = self.send("#{option}_value")
46
+ value ? h.merge(option => value) : h
47
+ end
48
+
49
+ hash.merge!((MULTI_VALUE_METHODS - [:select]).inject({}) do |h, option|
50
+ value = self.send("#{option}_values")
51
+ value && !value.empty? ? h.merge(option => value.uniq) : h
52
+ end)
53
+
54
+ # TRP: If one of the select options contains a * than select options are ignored
55
+ if select_values && !select_values.empty? && !select_values.any? { |val| val.to_s.match(/\*$/) }
56
+ hash.merge!(:select => select_values.uniq)
57
+ end
58
+
59
+ hash
60
+ end
61
+ end
62
+
63
+ def to_a
64
+ @records ||= fetch_records
65
+ end
66
+
67
+ def inspect
68
+ to_a.inspect
69
+ end
70
+
71
+ def scoping
72
+ @klass.scoped_methods << self
73
+ begin
74
+ yield
75
+ ensure
76
+ @klass.scoped_methods.pop
77
+ end
78
+ end
79
+
80
+ def respond_to?(method, include_private=false)
81
+ super ||
82
+ Array.method_defined?(method) ||
83
+ @klass.scopes[method] ||
84
+ dynamic_finder_method(method) ||
85
+ @klass.respond_to?(method, false)
86
+ end
87
+
88
+ def dynamic_finder_method(method)
89
+ defined_attributes = (@klass.defined_attributes || []).join('|')
90
+ if method.to_s =~ /^(find_by|find_all_by)_(#{defined_attributes})/
91
+ [$1, $2]
92
+ else
93
+ nil
94
+ end
95
+ end
96
+
97
+ protected
98
+
99
+ def method_missing(method, *args, &block)
100
+ if Array.method_defined?(method)
101
+ to_a.send(method, *args, &block)
102
+ elsif result = dynamic_finder_method(method)
103
+ method, attribute = result
104
+ options = { :conditions => { attribute => args[0] } }
105
+ options.merge!(args[1]) if args[1] && args[1].is_a?(Hash)
106
+ case method.to_sym
107
+ when :find_by
108
+ self.first(options)
109
+ when :find_all_by
110
+ self.all(options)
111
+ end
112
+ elsif @klass.scopes[method]
113
+ merge(@klass.send(method, *args, &block))
114
+ elsif @klass.respond_to?(method, false)
115
+ scoping { @klass.send(method, *args, &block) }
116
+ else
117
+ super
118
+ end
119
+ end
120
+
121
+ def fetch_records
122
+ @klass.send(:fetch_records, to_hash)
123
+ end
124
+
125
+ end
@@ -0,0 +1,101 @@
1
+
2
+ # TRP: Implementation of this feature was heavily influenced by binarylogic's Searchlogic-2.4.19
3
+ # => http://github.com/binarylogic/searchlogic
4
+ #
5
+ # It is designed to mimick much of the API of searchlogic so that it can be used alongside AR objects utilizing Searchlogic
6
+ # without developer confusion. There are certain features that are skipped because of the nature of RPCMapper.
7
+
8
+ module RPCMapper::Scopes
9
+
10
+ module Conditions
11
+
12
+ COMPARISON_CONDITIONS = {
13
+ :equals => [:is, :eq],
14
+ :does_not_equal => [:not_equal_to, :is_not, :not, :ne],
15
+ :less_than => [:lt, :before],
16
+ :less_than_or_equal_to => [:lte],
17
+ :greater_than => [:gt, :after],
18
+ :greater_than_or_equal_to => [:gte],
19
+ }
20
+
21
+ WILDCARD_CONDITIONS = {
22
+ :like => [:contains, :includes],
23
+ :not_like => [:does_not_include],
24
+ :begins_with => [:bw],
25
+ :not_begin_with => [:does_not_begin_with],
26
+ :ends_with => [:ew],
27
+ :not_end_with => [:does_not_end_with]
28
+ }
29
+
30
+ CONDITIONS = {}
31
+
32
+ # Add any / all variations to every comparison and wildcard condition
33
+ COMPARISON_CONDITIONS.merge(WILDCARD_CONDITIONS).each do |condition, aliases|
34
+ CONDITIONS[condition] = aliases
35
+ CONDITIONS["#{condition}_any".to_sym] = aliases.collect { |a| "#{a}_any".to_sym }
36
+ CONDITIONS["#{condition}_all".to_sym] = aliases.collect { |a| "#{a}_all".to_sym }
37
+ end
38
+
39
+ CONDITIONS[:equals_any] = CONDITIONS[:equals_any] + [:in]
40
+ CONDITIONS[:does_not_equal_all] = CONDITIONS[:does_not_equal_all] + [:not_in]
41
+
42
+ PRIMARY_CONDITIONS = CONDITIONS.keys
43
+ ALIAS_CONDITIONS = CONDITIONS.values.flatten
44
+
45
+ def respond_to?(name, include_private=false)
46
+ super ||
47
+ condition_details(name)
48
+ end
49
+
50
+ private
51
+
52
+ def method_missing(name, *args, &block)
53
+ if details = condition_details(name)
54
+ create_condition(details[:attribute], details[:condition], args)
55
+ send(name, *args)
56
+ else
57
+ super
58
+ end
59
+ end
60
+
61
+ def condition_details(method_name)
62
+ return nil unless defined_attributes
63
+
64
+ attribute_name_matcher = defined_attributes.join("|")
65
+ conditions_matcher = (PRIMARY_CONDITIONS + ALIAS_CONDITIONS).join("|")
66
+
67
+ if method_name.to_s =~ /^(#{attribute_name_matcher})_(#{conditions_matcher})$/
68
+ {:attribute => $1, :condition => $2}
69
+ end
70
+ end
71
+
72
+ def create_condition(attribute, condition, args)
73
+ if PRIMARY_CONDITIONS.include?(condition.to_sym)
74
+ create_primary_condition(attribute, condition)
75
+ elsif ALIAS_CONDITIONS.include?(condition.to_sym)
76
+ create_alias_condition(attribute, condition, args)
77
+ end
78
+ end
79
+
80
+ def create_primary_condition(attribute, condition)
81
+ scope_name = "#{attribute}_#{condition}"
82
+ scope scope_name, lambda { |a| where(scope_name => a) }
83
+ end
84
+
85
+ def create_alias_condition(attribute, condition, args)
86
+ primary_condition = primary_condition(condition)
87
+ alias_name = "#{attribute}_#{condition}"
88
+ primary_name = "#{attribute}_#{primary_condition}"
89
+ send(primary_name, *args) # go back to method_missing and make sure we create the method
90
+ singleton_class.class_eval { alias_method alias_name, primary_name }
91
+ end
92
+
93
+ # Returns the primary condition for the given alias. Ex:
94
+ #
95
+ # primary_condition(:gt) => :greater_than
96
+ def primary_condition(alias_condition)
97
+ CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first
98
+ end
99
+ end
100
+
101
+ end
@@ -0,0 +1,45 @@
1
+ require 'rpc_mapper/scopes/conditions'
2
+
3
+ module RPCMapper::Scopes
4
+
5
+ module ClassMethods
6
+
7
+ def scopes
8
+ read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
9
+ end
10
+
11
+ def scoped
12
+ current_scope ? relation.merge(current_scope) : relation.clone
13
+ end
14
+
15
+ def scope(name, scope_options={})
16
+ name = name.to_sym
17
+
18
+ # TRP: Define the scope and merge onto the relation
19
+ scopes[name] = lambda do |*args|
20
+ options = scope_options.is_a?(Proc) ? scope_options.call(*args) : scope_options
21
+ if options.is_a?(Hash)
22
+ scoped.apply_finder_options(options)
23
+ else
24
+ scoped.merge(options)
25
+ end
26
+ end
27
+
28
+ # TRP: Bind the above block to a method for easy access
29
+ singleton_class.send(:define_method, name, &scopes[name])
30
+ end
31
+
32
+ end
33
+
34
+ module InstanceMethods
35
+
36
+ end
37
+
38
+ def self.included(receiver)
39
+ receiver.extend ClassMethods
40
+ receiver.send :include, InstanceMethods
41
+
42
+ receiver.extend Conditions
43
+ end
44
+
45
+ end