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