better-ripple 1.0.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/LICENSE +17 -0
- data/README.md +182 -0
- data/RELEASE_NOTES.md +284 -0
- data/better-ripple.gemspec +55 -0
- data/lib/rails/generators/ripple/configuration/configuration_generator.rb +13 -0
- data/lib/rails/generators/ripple/configuration/templates/ripple.yml +25 -0
- data/lib/rails/generators/ripple/js/js_generator.rb +13 -0
- data/lib/rails/generators/ripple/js/templates/js/contrib.js +63 -0
- data/lib/rails/generators/ripple/js/templates/js/iso8601.js +76 -0
- data/lib/rails/generators/ripple/js/templates/js/ripple.js +132 -0
- data/lib/rails/generators/ripple/model/model_generator.rb +20 -0
- data/lib/rails/generators/ripple/model/templates/model.rb.erb +10 -0
- data/lib/rails/generators/ripple/observer/observer_generator.rb +16 -0
- data/lib/rails/generators/ripple/observer/templates/observer.rb.erb +2 -0
- data/lib/rails/generators/ripple/test/templates/cucumber.rb.erb +7 -0
- data/lib/rails/generators/ripple/test/test_generator.rb +44 -0
- data/lib/rails/generators/ripple_generator.rb +79 -0
- data/lib/ripple.rb +86 -0
- data/lib/ripple/associations.rb +380 -0
- data/lib/ripple/associations/embedded.rb +35 -0
- data/lib/ripple/associations/instantiators.rb +26 -0
- data/lib/ripple/associations/linked.rb +65 -0
- data/lib/ripple/associations/many.rb +38 -0
- data/lib/ripple/associations/many_embedded_proxy.rb +39 -0
- data/lib/ripple/associations/many_linked_proxy.rb +66 -0
- data/lib/ripple/associations/many_reference_proxy.rb +95 -0
- data/lib/ripple/associations/many_stored_key_proxy.rb +76 -0
- data/lib/ripple/associations/one.rb +20 -0
- data/lib/ripple/associations/one_embedded_proxy.rb +35 -0
- data/lib/ripple/associations/one_key_proxy.rb +58 -0
- data/lib/ripple/associations/one_linked_proxy.rb +26 -0
- data/lib/ripple/associations/one_stored_key_proxy.rb +43 -0
- data/lib/ripple/associations/proxy.rb +118 -0
- data/lib/ripple/attribute_methods.rb +132 -0
- data/lib/ripple/attribute_methods/dirty.rb +59 -0
- data/lib/ripple/attribute_methods/query.rb +34 -0
- data/lib/ripple/attribute_methods/read.rb +28 -0
- data/lib/ripple/attribute_methods/write.rb +25 -0
- data/lib/ripple/callbacks.rb +71 -0
- data/lib/ripple/conflict/basic_resolver.rb +86 -0
- data/lib/ripple/conflict/document_hooks.rb +46 -0
- data/lib/ripple/conflict/resolver.rb +79 -0
- data/lib/ripple/conflict/test_helper.rb +34 -0
- data/lib/ripple/conversion.rb +29 -0
- data/lib/ripple/core_ext.rb +3 -0
- data/lib/ripple/core_ext/casting.rb +151 -0
- data/lib/ripple/core_ext/indexes.rb +89 -0
- data/lib/ripple/core_ext/object.rb +8 -0
- data/lib/ripple/document.rb +105 -0
- data/lib/ripple/document/bucket_access.rb +25 -0
- data/lib/ripple/document/finders.rb +131 -0
- data/lib/ripple/document/key.rb +35 -0
- data/lib/ripple/document/link.rb +30 -0
- data/lib/ripple/document/persistence.rb +130 -0
- data/lib/ripple/embedded_document.rb +63 -0
- data/lib/ripple/embedded_document/around_callbacks.rb +18 -0
- data/lib/ripple/embedded_document/finders.rb +26 -0
- data/lib/ripple/embedded_document/persistence.rb +75 -0
- data/lib/ripple/i18n.rb +5 -0
- data/lib/ripple/indexes.rb +151 -0
- data/lib/ripple/inspection.rb +32 -0
- data/lib/ripple/locale/en.yml +26 -0
- data/lib/ripple/locale/fr.yml +24 -0
- data/lib/ripple/nested_attributes.rb +275 -0
- data/lib/ripple/observable.rb +28 -0
- data/lib/ripple/properties.rb +74 -0
- data/lib/ripple/property_type_mismatch.rb +12 -0
- data/lib/ripple/railtie.rb +26 -0
- data/lib/ripple/railties/ripple.rake +103 -0
- data/lib/ripple/serialization.rb +82 -0
- data/lib/ripple/test_server.rb +35 -0
- data/lib/ripple/timestamps.rb +25 -0
- data/lib/ripple/translation.rb +18 -0
- data/lib/ripple/validations.rb +65 -0
- data/lib/ripple/validations/associated_validator.rb +43 -0
- data/lib/ripple/version.rb +3 -0
- metadata +310 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rails/generators/ripple_generator'
|
2
|
+
|
3
|
+
module Ripple
|
4
|
+
module Generators
|
5
|
+
class TestGenerator < Base
|
6
|
+
desc 'Generates test helpers for Ripple. Test::Unit, RSpec and Cucumber are supported.'
|
7
|
+
# Cucumber
|
8
|
+
def create_cucumber_file
|
9
|
+
if File.directory?("features/support")
|
10
|
+
template 'cucumber.rb.erb', 'features/support/ripple.rb'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# RSpec
|
15
|
+
def create_rspec_file
|
16
|
+
if File.file?('spec/spec_helper.rb')
|
17
|
+
rspec_prelude = /\s*R[Ss]pec\.configure do \|config\|/
|
18
|
+
indentation = File.binread('spec/spec_helper.rb').match(rspec_prelude)[0].match(/^\s*/)[0]
|
19
|
+
inject_into_file 'spec/spec_helper.rb', :before => rspec_prelude do
|
20
|
+
"#{indentation}require 'ripple/test_server'\n"
|
21
|
+
end
|
22
|
+
inject_into_file 'spec/spec_helper.rb', :after => rspec_prelude do
|
23
|
+
"\n#{indentation} config.before(:suite) { Ripple::TestServer.setup }" +
|
24
|
+
"\n#{indentation} config.after(:each) { Ripple::TestServer.clear }\n"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Test::Unit
|
30
|
+
def create_test_unit_file
|
31
|
+
if File.file?('test/test_helper.rb')
|
32
|
+
test_case_prelude = /\s*class ActiveSupport::TestCase/
|
33
|
+
indentation = File.binread('test/test_helper.rb').match(test_case_prelude)[0].match(/^\s*/)[0]
|
34
|
+
inject_into_file "test/test_helper.rb", :before => test_case_prelude do
|
35
|
+
"#{indentation}# Setup in-memory test server for Riak\n#{indentation}require 'ripple/test_server'\n\n"
|
36
|
+
end
|
37
|
+
inject_into_class "test/test_helper.rb", 'ActiveSupport::TestCase' do
|
38
|
+
"#{indentation} setup { Ripple::TestServer.setup }\n#{indentation} teardown { Ripple::TestServer.clear }\n\n"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require "rails/generators/named_base"
|
3
|
+
require "rails/generators/active_model"
|
4
|
+
|
5
|
+
class RippleGenerator < ::Rails::Generators::Base
|
6
|
+
def create_ripple
|
7
|
+
invoke "ripple:configuration"
|
8
|
+
invoke "ripple:js"
|
9
|
+
invoke "ripple:test"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Ripple
|
14
|
+
# ActiveModel generators for use in a Rails project.
|
15
|
+
module Generators
|
16
|
+
# @private
|
17
|
+
class Base < ::Rails::Generators::Base
|
18
|
+
def self.source_root
|
19
|
+
@_ripple_source_root ||=
|
20
|
+
File.expand_path("../#{base_name}/#{generator_name}/templates", __FILE__)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class NamedBase < ::Rails::Generators::NamedBase
|
25
|
+
def self.source_root
|
26
|
+
@_ripple_source_root ||=
|
27
|
+
File.expand_path("../#{base_name}/#{generator_name}/templates", __FILE__)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Generator for a {Ripple::Document} model
|
32
|
+
class ActiveModel < ::Rails::Generators::ActiveModel
|
33
|
+
def self.all(klass)
|
34
|
+
"#{klass}.list"
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.find(klass, params=nil)
|
38
|
+
"#{klass}.find(#{params})"
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.build(klass, params=nil)
|
42
|
+
if params
|
43
|
+
"#{klass}.new(#{params})"
|
44
|
+
else
|
45
|
+
"#{klass}.new"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def save
|
50
|
+
"#{name}.save"
|
51
|
+
end
|
52
|
+
|
53
|
+
def update_attributes(params=nil)
|
54
|
+
"#{name}.update_attributes(#{params})"
|
55
|
+
end
|
56
|
+
|
57
|
+
def errors
|
58
|
+
"#{name}.errors"
|
59
|
+
end
|
60
|
+
|
61
|
+
def destroy
|
62
|
+
"#{name}.destroy"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# @private
|
69
|
+
module Rails
|
70
|
+
module Generators
|
71
|
+
class GeneratedAttribute #:nodoc:
|
72
|
+
def type_class
|
73
|
+
return "Time" if type.to_s == "datetime"
|
74
|
+
return "String" if type.to_s == "text"
|
75
|
+
return type.to_s.camelcase
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/ripple.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'riak'
|
2
|
+
require 'erb'
|
3
|
+
require 'yaml'
|
4
|
+
require 'active_model'
|
5
|
+
require 'ripple/core_ext'
|
6
|
+
require 'ripple/translation'
|
7
|
+
require 'ripple/document'
|
8
|
+
require 'ripple/embedded_document'
|
9
|
+
|
10
|
+
# Contains the classes and modules related to the ODM built on top of
|
11
|
+
# the basic Riak client.
|
12
|
+
module Ripple
|
13
|
+
class << self
|
14
|
+
# @return [Riak::Client] The client for the current thread.
|
15
|
+
def client
|
16
|
+
Thread.current[:ripple_client] ||= Riak::Client.new(client_config)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Sets the client for the current thread.
|
20
|
+
# @param [Riak::Client] value the client
|
21
|
+
def client=(value)
|
22
|
+
Thread.current[:ripple_client] = value
|
23
|
+
end
|
24
|
+
|
25
|
+
# Sets the global Ripple configuration.
|
26
|
+
def config=(hash)
|
27
|
+
self.client = nil
|
28
|
+
@config = hash.symbolize_keys
|
29
|
+
end
|
30
|
+
|
31
|
+
# Reads the global Ripple configuration.
|
32
|
+
def config
|
33
|
+
@config ||= {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# The format in which date/time objects will be serialized to
|
37
|
+
# strings in JSON. Defaults to :iso8601, and can be set in
|
38
|
+
# Ripple.config.
|
39
|
+
# @return [Symbol] the date format
|
40
|
+
def date_format
|
41
|
+
(config[:date_format] ||= :iso8601).to_sym
|
42
|
+
end
|
43
|
+
|
44
|
+
# Sets the format for date/time objects that are serialized to
|
45
|
+
# JSON.
|
46
|
+
# @param [Symbol] format the date format
|
47
|
+
def date_format=(format)
|
48
|
+
config[:date_format] = format.to_sym
|
49
|
+
end
|
50
|
+
|
51
|
+
# Loads the Ripple configuration from a given YAML file.
|
52
|
+
# Evaluates the configuration with ERB before loading.
|
53
|
+
def load_configuration(config_file, config_keys = [:ripple])
|
54
|
+
config_file = File.expand_path(config_file)
|
55
|
+
config_hash = YAML.load(ERB.new(File.read(config_file)).result).with_indifferent_access
|
56
|
+
config_keys.each {|k| config_hash = config_hash[k]}
|
57
|
+
configure_ports(config_hash)
|
58
|
+
self.config = config_hash || {}
|
59
|
+
rescue Errno::ENOENT
|
60
|
+
raise Ripple::MissingConfiguration.new(config_file)
|
61
|
+
end
|
62
|
+
alias_method :load_config, :load_configuration
|
63
|
+
|
64
|
+
private
|
65
|
+
def client_config
|
66
|
+
config.slice(*Riak::Client::VALID_OPTIONS)
|
67
|
+
end
|
68
|
+
|
69
|
+
def configure_ports(config)
|
70
|
+
return unless config && config[:min_port]
|
71
|
+
config[:http_port] ||= (config[:min_port].to_i)
|
72
|
+
config[:pb_port] ||= (config[:min_port].to_i + 1)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Exception raised when the path passed to
|
77
|
+
# {Ripple::load_configuration} does not point to a existing file.
|
78
|
+
class MissingConfiguration < StandardError
|
79
|
+
include Translation
|
80
|
+
def initialize(file_path)
|
81
|
+
super(t("missing_configuration", :file => file_path))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
require 'ripple/railtie' if defined? Rails::Railtie
|
@@ -0,0 +1,380 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/dependencies'
|
3
|
+
require 'riak/walk_spec'
|
4
|
+
require 'ripple/translation'
|
5
|
+
require 'ripple/associations/proxy'
|
6
|
+
require 'ripple/associations/instantiators'
|
7
|
+
require 'ripple/associations/linked'
|
8
|
+
require 'ripple/associations/embedded'
|
9
|
+
require 'ripple/associations/many'
|
10
|
+
require 'ripple/associations/one'
|
11
|
+
require 'ripple/associations/linked'
|
12
|
+
require 'ripple/associations/one_embedded_proxy'
|
13
|
+
require 'ripple/associations/many_embedded_proxy'
|
14
|
+
require 'ripple/associations/one_linked_proxy'
|
15
|
+
require 'ripple/associations/many_linked_proxy'
|
16
|
+
require 'ripple/associations/many_stored_key_proxy'
|
17
|
+
require 'ripple/associations/one_key_proxy'
|
18
|
+
require 'ripple/associations/one_stored_key_proxy'
|
19
|
+
require 'ripple/associations/many_reference_proxy'
|
20
|
+
|
21
|
+
module Ripple
|
22
|
+
# Adds associations via links and embedding to {Ripple::Document}
|
23
|
+
# models. Examples:
|
24
|
+
#
|
25
|
+
# # Documents can contain embedded documents, and link to other standalone documents
|
26
|
+
# # via associations using the many and one class methods.
|
27
|
+
# class Person
|
28
|
+
# include Ripple::Document
|
29
|
+
# property :name, String
|
30
|
+
# many :addresses
|
31
|
+
# many :friends, :class_name => "Person"
|
32
|
+
# one :account
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# # Account and Address are embeddable documents
|
36
|
+
# class Account
|
37
|
+
# include Ripple::EmbeddedDocument
|
38
|
+
# property :paid_until, Time
|
39
|
+
# embedded_in :person # Adds "person" method to get parent document
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# class Address
|
43
|
+
# include Ripple::EmbeddedDocument
|
44
|
+
# property :street, String
|
45
|
+
# property :city, String
|
46
|
+
# property :state, String
|
47
|
+
# property :zip, String
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# person = Person.find("adamhunter")
|
51
|
+
# person.friends << Person.find("seancribbs") # Links to people/seancribbs with tag "friend"
|
52
|
+
# person.addresses << Address.new(:street => "100 Main Street") # Adds an embedded address
|
53
|
+
# person.account.paid_until = 3.months.from_now
|
54
|
+
module Associations
|
55
|
+
extend ActiveSupport::Concern
|
56
|
+
|
57
|
+
module ClassMethods
|
58
|
+
include Translation
|
59
|
+
# @private
|
60
|
+
def inherited(subclass)
|
61
|
+
super
|
62
|
+
subclass.associations.merge!(associations)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Associations defined on the document
|
66
|
+
def associations
|
67
|
+
@associations ||= {}.with_indifferent_access
|
68
|
+
end
|
69
|
+
|
70
|
+
# Associations of embedded documents
|
71
|
+
def embedded_associations
|
72
|
+
associations.values.select(&:embedded?)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Associations of linked documents
|
76
|
+
def linked_associations
|
77
|
+
associations.values.select(&:linked?)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Associations of stored_key documents
|
81
|
+
def stored_key_associations
|
82
|
+
associations.values.select(&:stored_key?)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Creates a singular association
|
86
|
+
def one(name, options={})
|
87
|
+
configure_for_key_correspondence if options[:using] === :key
|
88
|
+
create_association(:one, name, options)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Creates a plural association
|
92
|
+
def many(name, options={})
|
93
|
+
raise ArgumentError, t('many_key_association') if options[:using] === :key
|
94
|
+
create_association(:many, name, options)
|
95
|
+
end
|
96
|
+
|
97
|
+
def configure_for_key_correspondence
|
98
|
+
include Ripple::Associations::KeyDelegator
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
def create_association(type, name, options={})
|
103
|
+
association = associations[name] = Association.new(type, name, options)
|
104
|
+
association.validate!(self)
|
105
|
+
association.setup_on(self)
|
106
|
+
|
107
|
+
define_method(name) do
|
108
|
+
get_proxy(association)
|
109
|
+
end
|
110
|
+
|
111
|
+
define_method("#{name}=") do |value|
|
112
|
+
get_proxy(association).replace(value)
|
113
|
+
value
|
114
|
+
end
|
115
|
+
|
116
|
+
unless association.many?
|
117
|
+
define_method("#{name}?") do
|
118
|
+
get_proxy(association).present?
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
# @private
|
126
|
+
def get_proxy(association)
|
127
|
+
unless proxy = instance_variable_get(association.ivar)
|
128
|
+
proxy = association.proxy_class.new(self, association)
|
129
|
+
instance_variable_set(association.ivar, proxy)
|
130
|
+
end
|
131
|
+
proxy
|
132
|
+
end
|
133
|
+
|
134
|
+
# @private
|
135
|
+
def reset_associations
|
136
|
+
self.class.associations.each do |name, assoc_object|
|
137
|
+
send(name).reset
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Adds embedded documents to the attributes
|
142
|
+
# @private
|
143
|
+
def attributes_for_persistence
|
144
|
+
self.class.embedded_associations.inject(super) do |attrs, association|
|
145
|
+
documents = instance_variable_get(association.ivar)
|
146
|
+
# We must explicitly check #nil? (rather than just saying `if documents`)
|
147
|
+
# because documents can be an association proxy that is proxying nil.
|
148
|
+
# In this case ruby treats documents as true because it is not _really_ nil,
|
149
|
+
# but #nil? will tell us if it is proxying nil.
|
150
|
+
|
151
|
+
unless documents.nil?
|
152
|
+
attrs[association.name] = documents.is_a?(Array) ? documents.map(&:attributes_for_persistence) : documents.attributes_for_persistence
|
153
|
+
end
|
154
|
+
attrs
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def propagate_callbacks_to_embedded_associations(name, kind)
|
159
|
+
self.class.embedded_associations.each do |association|
|
160
|
+
documents = instance_variable_get(association.ivar)
|
161
|
+
# We must explicitly check #nil? (rather than just saying `if documents`)
|
162
|
+
# because documents can be an association proxy that is proxying nil.
|
163
|
+
# In this case ruby treats documents as true because it is not _really_ nil,
|
164
|
+
# but #nil? will tell us if it is proxying nil.
|
165
|
+
next if documents.nil?
|
166
|
+
|
167
|
+
Array(documents).each do |doc|
|
168
|
+
doc.send("_#{name}_callbacks").each do |callback|
|
169
|
+
next unless callback.kind == kind
|
170
|
+
doc.send(callback.filter)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Propagates callbacks (save/create/update/destroy) to embedded associated documents.
|
177
|
+
# This is necessary so that when a parent is saved, the embedded child's before_save
|
178
|
+
# hooks are run as well.
|
179
|
+
# @private
|
180
|
+
def run_callbacks(name, *args, &block)
|
181
|
+
# validation is already propagated to embedded documents via the
|
182
|
+
# AssociatedValidator. We don't need to duplicate the propagation here.
|
183
|
+
return super if name == :validation
|
184
|
+
|
185
|
+
propagate_callbacks_to_embedded_associations(name, :before)
|
186
|
+
return_value = super
|
187
|
+
propagate_callbacks_to_embedded_associations(name, :after)
|
188
|
+
return_value
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# The "reflection" for an association - metadata about how it is
|
193
|
+
# configured.
|
194
|
+
class Association
|
195
|
+
include Ripple::Translation
|
196
|
+
attr_reader :type, :name, :options
|
197
|
+
|
198
|
+
# association options :using, :class_name, :class, :extend,
|
199
|
+
# options that may be added :validate
|
200
|
+
|
201
|
+
def initialize(type, name, options={})
|
202
|
+
@type, @name, @options = type, name, options.to_options
|
203
|
+
end
|
204
|
+
|
205
|
+
def validate!(owner)
|
206
|
+
# TODO: Refactor this into an association subclass. See also GH #284
|
207
|
+
if @options[:using] == :stored_key
|
208
|
+
single_name = ActiveSupport::Inflector.singularize(@name.to_s)
|
209
|
+
prop_name = "#{single_name}_key"
|
210
|
+
prop_name << "s" if many?
|
211
|
+
raise ArgumentError, t('stored_key_requires_property', :name => prop_name) unless owner.properties.include?(prop_name)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# @return String The class name of the associated object(s)
|
216
|
+
def class_name
|
217
|
+
@class_name ||= case
|
218
|
+
when @options[:class_name]
|
219
|
+
@options[:class_name]
|
220
|
+
when @options[:class]
|
221
|
+
@options[:class].to_s
|
222
|
+
when many?
|
223
|
+
@name.to_s.classify
|
224
|
+
else
|
225
|
+
@name.to_s.camelize
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# @return [Class] The class of the associated object(s)
|
230
|
+
def klass
|
231
|
+
@klass ||= discover_class
|
232
|
+
end
|
233
|
+
|
234
|
+
# @return [true,false] Is the cardinality of the association > 1
|
235
|
+
def many?
|
236
|
+
@type == :many
|
237
|
+
end
|
238
|
+
|
239
|
+
# @return [true,false] Is the cardinality of the association == 1
|
240
|
+
def one?
|
241
|
+
@type == :one
|
242
|
+
end
|
243
|
+
|
244
|
+
# @return [true,false] Is the associated class an EmbeddedDocument
|
245
|
+
def embedded?
|
246
|
+
klass.embeddable?
|
247
|
+
end
|
248
|
+
|
249
|
+
# TODO: Polymorphic not supported
|
250
|
+
# @return [true,false] Does the association support more than one associated class
|
251
|
+
def polymorphic?
|
252
|
+
false
|
253
|
+
end
|
254
|
+
|
255
|
+
# @return [true,false] Does the association use links
|
256
|
+
def linked?
|
257
|
+
using == :linked
|
258
|
+
end
|
259
|
+
|
260
|
+
# @return [true,false] Does the association use stored_key
|
261
|
+
def stored_key?
|
262
|
+
using == :stored_key
|
263
|
+
end
|
264
|
+
|
265
|
+
# @return [String] the instance variable in the owner where the association will be stored
|
266
|
+
def ivar
|
267
|
+
"@_#{name}"
|
268
|
+
end
|
269
|
+
|
270
|
+
# @return [Class] the association proxy class
|
271
|
+
def proxy_class
|
272
|
+
@proxy_class ||= proxy_class_name.constantize
|
273
|
+
end
|
274
|
+
|
275
|
+
# @return [String] the class name of the association proxy
|
276
|
+
def proxy_class_name
|
277
|
+
klass_name = (many? ? 'Many' : 'One') + using.to_s.camelize + ('Polymorphic' if polymorphic?).to_s + 'Proxy'
|
278
|
+
"Ripple::Associations::#{klass_name}"
|
279
|
+
end
|
280
|
+
|
281
|
+
# @return [Proc] a filter proc to be used with Enumerable#select for collecting links that belong to this association (only when #linked? is true)
|
282
|
+
def link_filter
|
283
|
+
linked? ? lambda {|link| link.tag == link_tag } : lambda {|_| false }
|
284
|
+
end
|
285
|
+
|
286
|
+
# @return [String,nil] when #linked? is true, the tag for outgoing links
|
287
|
+
def link_tag
|
288
|
+
linked? ? Array(link_spec).first.tag : nil
|
289
|
+
end
|
290
|
+
|
291
|
+
def bucket_name
|
292
|
+
polymorphic? ? '_' : klass.bucket_name
|
293
|
+
end
|
294
|
+
|
295
|
+
# @return [Riak::WalkSpec] when #linked? is true, a specification for which links to follow to retrieve the associated documents
|
296
|
+
def link_spec
|
297
|
+
# TODO: support transitive linked associations
|
298
|
+
if linked?
|
299
|
+
tag = name.to_s
|
300
|
+
Riak::WalkSpec.new(:tag => tag, :bucket => bucket_name)
|
301
|
+
else
|
302
|
+
nil
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# @return [Symbol] which method is used for representing the association - currently only supports :embedded and :linked
|
307
|
+
def using
|
308
|
+
@using ||= options[:using] || (embedded? ? :embedded : :linked)
|
309
|
+
end
|
310
|
+
|
311
|
+
# @raise [ArgumentError] if the value does not match the class of the association
|
312
|
+
def verify_type!(value, owner)
|
313
|
+
unless type_matches?(value)
|
314
|
+
raise ArgumentError.new(t('invalid_association_value',
|
315
|
+
:name => name,
|
316
|
+
:owner => owner.inspect,
|
317
|
+
:klass => polymorphic? ? "<polymorphic>" : klass.name,
|
318
|
+
:value => value.inspect))
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# @private
|
323
|
+
def type_matches?(value)
|
324
|
+
case
|
325
|
+
when polymorphic?
|
326
|
+
one? || Array === value
|
327
|
+
when many?
|
328
|
+
Array === value && value.all? {|d| (embedded? && Hash === d) || klass === d }
|
329
|
+
when one?
|
330
|
+
value.nil? || (embedded? && Hash === value) || value.kind_of?(klass)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def uses_search?
|
335
|
+
(options[:using] == :reference)
|
336
|
+
end
|
337
|
+
|
338
|
+
def setup_on(model)
|
339
|
+
@model = model
|
340
|
+
define_callbacks_on(model)
|
341
|
+
if uses_search?
|
342
|
+
klass.before_save do |o|
|
343
|
+
unless o.class.bucket.is_indexed?
|
344
|
+
o.class.bucket.enable_index!
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def define_callbacks_on(klass)
|
351
|
+
_association = self
|
352
|
+
|
353
|
+
klass.before_save do
|
354
|
+
if _association.linked? && !@_in_save_loaded_documents_callback
|
355
|
+
@_in_save_loaded_documents_callback = true
|
356
|
+
|
357
|
+
begin
|
358
|
+
send(_association.name).loaded_documents.each do |document|
|
359
|
+
document.save if document.new? || document.changed?
|
360
|
+
end
|
361
|
+
ensure
|
362
|
+
remove_instance_variable(:@_in_save_loaded_documents_callback)
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
private
|
369
|
+
def discover_class
|
370
|
+
options[:class] || (@model && find_class(@model, class_name)) || class_name.constantize
|
371
|
+
end
|
372
|
+
|
373
|
+
def find_class(scope, class_name)
|
374
|
+
return nil if class_name.include?("::")
|
375
|
+
class_sym = class_name.to_sym
|
376
|
+
parent_scope = scope.parents.unshift(scope).find {|s| ActiveSupport::Dependencies.local_const_defined?(s, class_sym) }
|
377
|
+
parent_scope.const_get(class_sym) if parent_scope
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|