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