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