perry 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +38 -0
- data/Rakefile +70 -0
- data/lib/perry.rb +48 -0
- data/lib/perry/adapters.rb +5 -0
- data/lib/perry/adapters/abstract_adapter.rb +92 -0
- data/lib/perry/adapters/bertrpc_adapter.rb +32 -0
- data/lib/perry/adapters/restful_http_adapter.rb +99 -0
- data/lib/perry/association.rb +330 -0
- data/lib/perry/association_preload.rb +69 -0
- data/lib/perry/associations/common.rb +51 -0
- data/lib/perry/associations/contains.rb +64 -0
- data/lib/perry/associations/external.rb +67 -0
- data/lib/perry/base.rb +204 -0
- data/lib/perry/cacheable.rb +80 -0
- data/lib/perry/cacheable/entry.rb +18 -0
- data/lib/perry/cacheable/store.rb +45 -0
- data/lib/perry/core_ext/kernel/singleton_class.rb +14 -0
- data/lib/perry/errors.rb +49 -0
- data/lib/perry/logger.rb +54 -0
- data/lib/perry/persistence.rb +55 -0
- data/lib/perry/relation.rb +153 -0
- data/lib/perry/relation/finder_methods.rb +56 -0
- data/lib/perry/relation/query_methods.rb +69 -0
- data/lib/perry/scopes.rb +45 -0
- data/lib/perry/scopes/conditions.rb +101 -0
- data/lib/perry/serialization.rb +48 -0
- data/lib/perry/version.rb +13 -0
- metadata +187 -0
data/README.rdoc
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
= Perry
|
2
|
+
|
3
|
+
== Description
|
4
|
+
|
5
|
+
Ruby library for querying and mapping data through generic interfaces
|
6
|
+
|
7
|
+
== Installation
|
8
|
+
|
9
|
+
gem install perry
|
10
|
+
|
11
|
+
== Usage
|
12
|
+
|
13
|
+
require 'perry'
|
14
|
+
|
15
|
+
== License
|
16
|
+
|
17
|
+
Copyright (c) 2011 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,70 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
require 'lib/perry/version'
|
6
|
+
|
7
|
+
spec = Gem::Specification.new do |s|
|
8
|
+
s.name = 'perry'
|
9
|
+
s.version = Perry::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 through generic interfaces"
|
14
|
+
s.author = 'Travis Petticrew'
|
15
|
+
s.email = 'bobo@petticrew.net'
|
16
|
+
s.homepage = 'http://github.com/tpett/perry'
|
17
|
+
s.files = %w(README.rdoc Rakefile) + Dir.glob("{lib}/**/*")
|
18
|
+
# s.executables = ['perry']
|
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
|
+
[:rails2, :rails3].each do |name|
|
34
|
+
|
35
|
+
Rake::TestTask.new("test_#{name}") do |t|
|
36
|
+
t.libs << 'test'
|
37
|
+
t.ruby_opts << "-r load_#{name}"
|
38
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
39
|
+
t.verbose = true
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "Test all gem versions"
|
45
|
+
task :test => [:test_rails2, :test_rails3] do
|
46
|
+
end
|
47
|
+
|
48
|
+
begin
|
49
|
+
require 'rcov/rcovtask'
|
50
|
+
|
51
|
+
Rcov::RcovTask.new(:coverage) do |t|
|
52
|
+
t.libs = ['test']
|
53
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
54
|
+
t.verbose = true
|
55
|
+
t.rcov_opts = ['--text-report', "-x #{Gem.path}", '-x /Library/Ruby', '-x /usr/lib/ruby']
|
56
|
+
end
|
57
|
+
|
58
|
+
task :default => :coverage
|
59
|
+
|
60
|
+
rescue LoadError
|
61
|
+
warn "\n**** Install rcov (sudo gem install relevance-rcov) to get coverage stats ****\n"
|
62
|
+
task :default => :test
|
63
|
+
end
|
64
|
+
|
65
|
+
desc 'Generate the gemspec to serve this gem'
|
66
|
+
task :gemspec do
|
67
|
+
file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
|
68
|
+
File.open(file, 'w') {|f| f << spec.to_ruby }
|
69
|
+
puts "Created gemspec: #{file}"
|
70
|
+
end
|
data/lib/perry.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# TRP: Cherry pick some goodies from active_support
|
2
|
+
require 'active_support/core_ext/array'
|
3
|
+
require 'active_support/core_ext/class/inheritable_attributes'
|
4
|
+
require 'active_support/core_ext/hash/deep_merge'
|
5
|
+
begin
|
6
|
+
require 'active_support/core_ext/duplicable' #ActiveSupport 2.3.x
|
7
|
+
Hash.send(:include, ActiveSupport::CoreExtensions::Hash::DeepMerge) unless Hash.instance_methods.include?('deep_merge')
|
8
|
+
rescue LoadError => exception
|
9
|
+
require 'active_support/core_ext/object/duplicable' #ActiveSupport 3.0.x
|
10
|
+
end
|
11
|
+
require 'active_support/core_ext/module/delegation'
|
12
|
+
|
13
|
+
# TRP: Used for pretty logging
|
14
|
+
autoload :Benchmark, 'benchmark'
|
15
|
+
|
16
|
+
require 'ostruct'
|
17
|
+
|
18
|
+
# TRP: Perry core_ext
|
19
|
+
require 'perry/core_ext/kernel/singleton_class'
|
20
|
+
|
21
|
+
module Perry
|
22
|
+
@@log_file = nil
|
23
|
+
|
24
|
+
def self.logger
|
25
|
+
@@logger ||= default_logger
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.logger=(logger)
|
29
|
+
@@logger = logger
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.log_file=(file)
|
33
|
+
@@log_file = file
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.default_logger
|
37
|
+
if defined?(Rails)
|
38
|
+
Rails.logger
|
39
|
+
else
|
40
|
+
require 'logger' unless defined?(::Logger)
|
41
|
+
::Logger.new(@@log_file)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
require 'perry/base'
|
48
|
+
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'perry/logger'
|
2
|
+
|
3
|
+
module Perry::Adapters
|
4
|
+
class AbstractAdapter
|
5
|
+
include Perry::Logger
|
6
|
+
|
7
|
+
attr_accessor :config
|
8
|
+
attr_reader :type
|
9
|
+
@@registered_adapters ||= {}
|
10
|
+
|
11
|
+
def initialize(type, config)
|
12
|
+
@type = type.to_sym
|
13
|
+
@configuration_contexts = config.is_a?(Array) ? config : [config]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.create(type, config)
|
17
|
+
klass = @@registered_adapters[type.to_sym]
|
18
|
+
klass.new(type, config)
|
19
|
+
end
|
20
|
+
|
21
|
+
def extend_adapter(config)
|
22
|
+
config = config.is_a?(Array) ? config : [config]
|
23
|
+
self.class.create(self.type, @configuration_contexts + config)
|
24
|
+
end
|
25
|
+
|
26
|
+
def config
|
27
|
+
@config ||= build_configuration
|
28
|
+
end
|
29
|
+
|
30
|
+
def call(mode, options)
|
31
|
+
@stack ||= self.middlewares.reverse.inject(self.method(mode)) do |below, (above_klass, above_config)|
|
32
|
+
above_klass.new(below, above_config)
|
33
|
+
end
|
34
|
+
|
35
|
+
@stack.call(options)
|
36
|
+
end
|
37
|
+
|
38
|
+
def middlewares
|
39
|
+
self.config[:middlewares] || []
|
40
|
+
end
|
41
|
+
|
42
|
+
def read(options)
|
43
|
+
raise(NotImplementedError,
|
44
|
+
"You must not use the abstract adapter. Implement an adapter that extends the " +
|
45
|
+
"Perry::Adapters::AbstractAdapter class and overrides this method.")
|
46
|
+
end
|
47
|
+
|
48
|
+
def write(object)
|
49
|
+
raise(NotImplementedError,
|
50
|
+
"You must not use the abstract adapter. Implement an adapter that extends the " +
|
51
|
+
"Perry::Adapters::AbstractAdapter class and overrides this method.")
|
52
|
+
end
|
53
|
+
|
54
|
+
def delete(object)
|
55
|
+
raise(NotImplementedError,
|
56
|
+
"You must not use the abstract adapter. Implement an adapter that extends the " +
|
57
|
+
"Perry::Adapters::AbstractAdapter class and overrides this method.")
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.register_as(name)
|
61
|
+
@@registered_adapters[name.to_sym] = self
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# TRP: Run each configure block in order of class hierarchy / definition and merge the results.
|
67
|
+
def build_configuration
|
68
|
+
@configuration_contexts.inject({}) do |sum, config|
|
69
|
+
if config.is_a?(Hash)
|
70
|
+
sum.merge(config)
|
71
|
+
else
|
72
|
+
AdapterConfig.new(sum).tap { |ac| config.call(ac) }.marshal_dump
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class AdapterConfig < OpenStruct
|
78
|
+
|
79
|
+
def add_middleware(klass, config={})
|
80
|
+
self.middlewares ||= []
|
81
|
+
self.middlewares << [klass, config]
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_hash
|
85
|
+
marshal_dump
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
autoload :BERTRPC, 'bertrpc'
|
2
|
+
|
3
|
+
module Perry::Adapters
|
4
|
+
class BERTRPCAdapter < Perry::Adapters::AbstractAdapter
|
5
|
+
register_as :bertrpc
|
6
|
+
|
7
|
+
@@service_pool ||= {}
|
8
|
+
|
9
|
+
def service
|
10
|
+
@@service_pool["#{config[:host]}:#{config[:port]}"] ||=
|
11
|
+
BERTRPC::Service.new(self.config[:host], self.config[:port])
|
12
|
+
end
|
13
|
+
|
14
|
+
def read(options)
|
15
|
+
log(options, "RPC #{config[:service]}") {
|
16
|
+
self.service.call.send(self.namespace).send(self.service_name,
|
17
|
+
options.merge(config[:default_options] || {}))
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def namespace
|
24
|
+
self.config[:namespace]
|
25
|
+
end
|
26
|
+
|
27
|
+
def service_name
|
28
|
+
self.config[:service]
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
autoload :Net, 'net/http'
|
2
|
+
autoload :URI, 'uri'
|
3
|
+
|
4
|
+
module Perry::Adapters
|
5
|
+
class RestfulHTTPAdapter < Perry::Adapters::AbstractAdapter
|
6
|
+
register_as :restful_http
|
7
|
+
|
8
|
+
attr_reader :last_response
|
9
|
+
|
10
|
+
def initialize(*args)
|
11
|
+
super
|
12
|
+
@configuration_contexts << { :primary_key => :id }
|
13
|
+
end
|
14
|
+
|
15
|
+
def write(object)
|
16
|
+
params = build_params_from_attributes(object)
|
17
|
+
object.new_record? ? post_http(object, params) : put_http(object, params)
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete(object)
|
21
|
+
delete_http(object)
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def post_http(object, params)
|
27
|
+
http_call(object, :post, params)
|
28
|
+
end
|
29
|
+
|
30
|
+
def put_http(object, params)
|
31
|
+
http_call(object, :put, params)
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete_http(object)
|
35
|
+
http_call(object, :delete, self.config[:default_options])
|
36
|
+
end
|
37
|
+
|
38
|
+
def http_call(object, method, params={})
|
39
|
+
request_klass = case method
|
40
|
+
when :post then Net::HTTP::Post
|
41
|
+
when :put then Net::HTTP::Put
|
42
|
+
when :delete then Net::HTTP::Delete
|
43
|
+
end
|
44
|
+
|
45
|
+
req_uri = self.build_uri(object, method)
|
46
|
+
|
47
|
+
request = if method == :delete
|
48
|
+
request = request_klass.new([req_uri.path, req_uri.query].join('?'))
|
49
|
+
else
|
50
|
+
request = request_klass.new(req_uri.path)
|
51
|
+
request.set_form_data(params) unless method == :delete
|
52
|
+
request
|
53
|
+
end
|
54
|
+
|
55
|
+
self.log(params, "#{method.to_s.upcase} #{req_uri}") do
|
56
|
+
@last_response = Net::HTTP.new(req_uri.host, req_uri.port).start { |http| http.request(request) }
|
57
|
+
end
|
58
|
+
parse_response_code(@last_response)
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_response_code(response)
|
62
|
+
case response
|
63
|
+
when Net::HTTPSuccess, Net::HTTPRedirection
|
64
|
+
true
|
65
|
+
else
|
66
|
+
false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_params_from_attributes(object)
|
71
|
+
if self.config[:post_body_wrapper]
|
72
|
+
params = self.config[:default_options] || {}
|
73
|
+
params.merge!(object.write_options[:default_options]) if object.write_options.is_a?(Hash) && object.write_options[:default_options].is_a?(Hash)
|
74
|
+
|
75
|
+
object.attributes.each do |attribute, value|
|
76
|
+
params.merge!({"#{self.config[:post_body_wrapper]}[#{attribute}]" => value})
|
77
|
+
end
|
78
|
+
|
79
|
+
params
|
80
|
+
else
|
81
|
+
object.attributes
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_uri(object, method)
|
86
|
+
url = [self.config[:host].gsub(%r{/$}, ''), self.config[:service]]
|
87
|
+
url << object.send(self.config[:primary_key]) unless object.new_record?
|
88
|
+
uri = URI.parse "#{url.join('/')}#{self.config[:format]}"
|
89
|
+
|
90
|
+
# TRP: method DELETE has no POST body so we have to append any default options onto the query string
|
91
|
+
if method == :delete
|
92
|
+
uri.query = (self.config[:default_options] || {}).collect { |key, value| "#{key}=#{value}" }.join('&')
|
93
|
+
end
|
94
|
+
|
95
|
+
uri
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,330 @@
|
|
1
|
+
module Perry::Association; end
|
2
|
+
|
3
|
+
module Perry::Association
|
4
|
+
|
5
|
+
##
|
6
|
+
# Association::Base
|
7
|
+
#
|
8
|
+
# This is the base class for all associations. It defines the basic structure
|
9
|
+
# of an association. The basic nomenclature is as follows:
|
10
|
+
#
|
11
|
+
# TODO: Clean up this nomenclature. Source and Target should be switched. From
|
12
|
+
# the configuration side this is already done but the internal naming is backwards.
|
13
|
+
#
|
14
|
+
# Source: The start point of the association. The source class is the class
|
15
|
+
# on which the association is defined.
|
16
|
+
#
|
17
|
+
# Proxy: On a through association the proxy is the class on which the target
|
18
|
+
# association lives
|
19
|
+
#
|
20
|
+
# Target: The class that will ultimately be returned by the association
|
21
|
+
#
|
22
|
+
class Base
|
23
|
+
attr_accessor :source_klass, :id, :options
|
24
|
+
|
25
|
+
def initialize(klass, id, options={})
|
26
|
+
self.source_klass = klass
|
27
|
+
self.id = id.to_sym
|
28
|
+
self.options = options
|
29
|
+
end
|
30
|
+
|
31
|
+
def type
|
32
|
+
raise NotImplementedError, "You must define the type in subclasses."
|
33
|
+
end
|
34
|
+
|
35
|
+
def polymorphic?
|
36
|
+
raise(
|
37
|
+
NotImplementedError,
|
38
|
+
"You must define how your association is polymorphic in subclasses."
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def collection?
|
43
|
+
raise NotImplementedError, "You must define collection? in subclasses."
|
44
|
+
end
|
45
|
+
|
46
|
+
def primary_key
|
47
|
+
options[:primary_key] || :id
|
48
|
+
end
|
49
|
+
|
50
|
+
def foreign_key
|
51
|
+
options[:foreign_key]
|
52
|
+
end
|
53
|
+
|
54
|
+
def target_klass(object=nil)
|
55
|
+
if options[:polymorphic] && object
|
56
|
+
poly_type = object.is_a?(Perry::Base) ? object.send("#{id}_type") : object
|
57
|
+
end
|
58
|
+
|
59
|
+
klass = if poly_type
|
60
|
+
type_string = [
|
61
|
+
options[:polymorphic_namespace],
|
62
|
+
sanitize_type_attribute(poly_type)
|
63
|
+
].compact.join('::')
|
64
|
+
begin
|
65
|
+
eval(type_string)
|
66
|
+
rescue NameError => err
|
67
|
+
raise(
|
68
|
+
Perry::PolymorphicAssociationTypeError,
|
69
|
+
"No constant defined called #{type_string}"
|
70
|
+
)
|
71
|
+
end
|
72
|
+
else
|
73
|
+
unless options[:class_name]
|
74
|
+
raise(ArgumentError,
|
75
|
+
":class_name option required for association declaration.")
|
76
|
+
end
|
77
|
+
|
78
|
+
unless options[:class_name] =~ /^::/
|
79
|
+
options[:class_name] = "::#{options[:class_name]}"
|
80
|
+
end
|
81
|
+
|
82
|
+
eval(options[:class_name])
|
83
|
+
end
|
84
|
+
|
85
|
+
Perry::Base.resolve_leaf_klass klass
|
86
|
+
end
|
87
|
+
|
88
|
+
def scope(object)
|
89
|
+
raise NotImplementedError, "You must define scope in subclasses"
|
90
|
+
end
|
91
|
+
|
92
|
+
# TRP: Only eager loadable if association query does not depend on instance
|
93
|
+
# data
|
94
|
+
def eager_loadable?
|
95
|
+
Perry::Relation::FINDER_OPTIONS.inject(true) do |condition, key|
|
96
|
+
condition && !options[key].respond_to?(:call)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
def base_scope(object)
|
103
|
+
target_klass(object).scoped.apply_finder_options base_finder_options(object)
|
104
|
+
end
|
105
|
+
|
106
|
+
def base_finder_options(object)
|
107
|
+
Perry::Relation::FINDER_OPTIONS.inject({}) do |sum, key|
|
108
|
+
value = self.options[key]
|
109
|
+
sum.merge!(key => value.respond_to?(:call) ? value.call(object) : value) if value
|
110
|
+
sum
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# TRP: Make sure the value looks like a variable syntaxtually
|
115
|
+
def sanitize_type_attribute(string)
|
116
|
+
string.gsub(/[^a-zA-Z]\w*/, '')
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
class BelongsTo < Base
|
123
|
+
|
124
|
+
def type
|
125
|
+
:belongs_to
|
126
|
+
end
|
127
|
+
|
128
|
+
def collection?
|
129
|
+
false
|
130
|
+
end
|
131
|
+
|
132
|
+
def foreign_key
|
133
|
+
super || "#{id}_id".to_sym
|
134
|
+
end
|
135
|
+
|
136
|
+
def polymorphic?
|
137
|
+
!!options[:polymorphic]
|
138
|
+
end
|
139
|
+
|
140
|
+
def polymorphic_type
|
141
|
+
"#{id}_type".to_sym
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Returns a scope on the target containing this association
|
146
|
+
#
|
147
|
+
# Builds conditions on top of the base_scope generated from any finder
|
148
|
+
# options set with the association
|
149
|
+
#
|
150
|
+
# belongs_to :foo, :foreign_key => :foo_id
|
151
|
+
#
|
152
|
+
# In addition to any finder options included with the association options
|
153
|
+
# the following scope will be added:
|
154
|
+
# where(:id => source[:foo_id])
|
155
|
+
#
|
156
|
+
def scope(object)
|
157
|
+
if object[self.foreign_key]
|
158
|
+
base_scope(object).where(self.primary_key => object[self.foreign_key])
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
class Has < Base
|
166
|
+
|
167
|
+
def foreign_key
|
168
|
+
super || if self.polymorphic?
|
169
|
+
"#{options[:as]}_id"
|
170
|
+
else
|
171
|
+
"#{Perry::Base.base_class_name(source_klass).downcase}_id"
|
172
|
+
end.to_sym
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
##
|
177
|
+
# Returns a scope on the target containing this association
|
178
|
+
#
|
179
|
+
# Builds conditions on top of the base_scope generated from any finder
|
180
|
+
# options set with the association
|
181
|
+
#
|
182
|
+
# has_many :widgets, :class_name => "Widget", :foreign_key => :widget_id
|
183
|
+
# has_many :comments, :as => :parent
|
184
|
+
#
|
185
|
+
# In addition to any finder options included with the association options
|
186
|
+
# the following will be added:
|
187
|
+
#
|
188
|
+
# where(widget_id => source[:id])
|
189
|
+
#
|
190
|
+
# Or for the polymorphic :comments association:
|
191
|
+
#
|
192
|
+
# where(:parent_id => source[:id], :parent_type => source.class)
|
193
|
+
#
|
194
|
+
def scope(object)
|
195
|
+
return nil unless object[self.primary_key]
|
196
|
+
s = base_scope(object).where(self.foreign_key => object[self.primary_key])
|
197
|
+
if polymorphic?
|
198
|
+
s = s.where(
|
199
|
+
polymorphic_type => Perry::Base.base_class_name(object.class) )
|
200
|
+
end
|
201
|
+
s
|
202
|
+
end
|
203
|
+
|
204
|
+
def polymorphic_type
|
205
|
+
:"#{options[:as]}_type"
|
206
|
+
end
|
207
|
+
|
208
|
+
def polymorphic?
|
209
|
+
!!options[:as]
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
|
214
|
+
|
215
|
+
class HasMany < Has
|
216
|
+
|
217
|
+
def collection?
|
218
|
+
true
|
219
|
+
end
|
220
|
+
|
221
|
+
def type
|
222
|
+
:has_many
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
class HasOne < Has
|
229
|
+
|
230
|
+
def collection?
|
231
|
+
false
|
232
|
+
end
|
233
|
+
|
234
|
+
def type
|
235
|
+
:has_one
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
|
240
|
+
|
241
|
+
class HasManyThrough < HasMany
|
242
|
+
attr_accessor :proxy_association
|
243
|
+
attr_accessor :target_association
|
244
|
+
|
245
|
+
def proxy_association
|
246
|
+
@proxy_association ||= source_klass.defined_associations[options[:through]] ||
|
247
|
+
raise(
|
248
|
+
Perry::AssociationNotFound,
|
249
|
+
":has_many_through: '#{options[:through]}' is not an association " +
|
250
|
+
"on #{source_klass}"
|
251
|
+
)
|
252
|
+
end
|
253
|
+
|
254
|
+
def target_association
|
255
|
+
return @target_association if @target_association
|
256
|
+
|
257
|
+
klass = proxy_association.target_klass
|
258
|
+
@target_association = klass.defined_associations[self.id] ||
|
259
|
+
klass.defined_associations[self.options[:source]] ||
|
260
|
+
raise(Perry::AssociationNotFound,
|
261
|
+
":has_many_through: '#{options[:source] || self.id}' is not an " +
|
262
|
+
"association on #{klass}"
|
263
|
+
)
|
264
|
+
end
|
265
|
+
|
266
|
+
def scope(object)
|
267
|
+
# Which attribute's values should be used on the proxy
|
268
|
+
key = if target_is_has?
|
269
|
+
target_association.primary_key.to_sym
|
270
|
+
else
|
271
|
+
target_association.foreign_key.to_sym
|
272
|
+
end
|
273
|
+
|
274
|
+
# Fetch the ids of all records on the proxy using the correct key
|
275
|
+
proxy_ids = proc do
|
276
|
+
proxy_association.scope(object).select(key).collect(&key)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Use these ids to build a scope on the target object
|
280
|
+
relation = target_klass.scoped
|
281
|
+
|
282
|
+
if target_is_has?
|
283
|
+
relation = relation.where(
|
284
|
+
proc do
|
285
|
+
{ target_association.foreign_key => proxy_ids.call }
|
286
|
+
end
|
287
|
+
)
|
288
|
+
else
|
289
|
+
relation = relation.where(
|
290
|
+
proc do
|
291
|
+
{ target_association.primary_key => proxy_ids.call }
|
292
|
+
end
|
293
|
+
)
|
294
|
+
end
|
295
|
+
|
296
|
+
# Add polymorphic type condition if target is polymorphic and has
|
297
|
+
if target_association.polymorphic? && target_is_has?
|
298
|
+
relation = relation.where(
|
299
|
+
target_association.polymorphic_type =>
|
300
|
+
Perry::Base.base_class_name(proxy_association.target_klass(object))
|
301
|
+
)
|
302
|
+
end
|
303
|
+
|
304
|
+
relation
|
305
|
+
end
|
306
|
+
|
307
|
+
def target_klass
|
308
|
+
target_association.target_klass(options[:source_type])
|
309
|
+
end
|
310
|
+
|
311
|
+
def target_type
|
312
|
+
target_association.type
|
313
|
+
end
|
314
|
+
|
315
|
+
def target_is_has?
|
316
|
+
target_association.is_a?(Has)
|
317
|
+
end
|
318
|
+
|
319
|
+
def type
|
320
|
+
:has_many_through
|
321
|
+
end
|
322
|
+
|
323
|
+
def polymorphic?
|
324
|
+
false
|
325
|
+
end
|
326
|
+
|
327
|
+
end
|
328
|
+
|
329
|
+
|
330
|
+
end
|