seomoz-ripple 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (149) hide show
  1. data/Gemfile +20 -0
  2. data/Guardfile +15 -0
  3. data/Rakefile +88 -0
  4. data/lib/rails/generators/ripple/configuration/configuration_generator.rb +13 -0
  5. data/lib/rails/generators/ripple/configuration/templates/ripple.yml +24 -0
  6. data/lib/rails/generators/ripple/js/js_generator.rb +13 -0
  7. data/lib/rails/generators/ripple/js/templates/js/contrib.js +63 -0
  8. data/lib/rails/generators/ripple/js/templates/js/iso8601.js +76 -0
  9. data/lib/rails/generators/ripple/js/templates/js/ripple.js +132 -0
  10. data/lib/rails/generators/ripple/model/model_generator.rb +20 -0
  11. data/lib/rails/generators/ripple/model/templates/model.rb +10 -0
  12. data/lib/rails/generators/ripple/observer/observer_generator.rb +16 -0
  13. data/lib/rails/generators/ripple/observer/templates/observer.rb +4 -0
  14. data/lib/rails/generators/ripple/test/templates/test_server.rb +46 -0
  15. data/lib/rails/generators/ripple/test/test_generator.rb +39 -0
  16. data/lib/rails/generators/ripple_generator.rb +78 -0
  17. data/lib/ripple.rb +79 -0
  18. data/lib/ripple/associations.rb +356 -0
  19. data/lib/ripple/associations/embedded.rb +35 -0
  20. data/lib/ripple/associations/instantiators.rb +26 -0
  21. data/lib/ripple/associations/linked.rb +65 -0
  22. data/lib/ripple/associations/many.rb +38 -0
  23. data/lib/ripple/associations/many_embedded_proxy.rb +38 -0
  24. data/lib/ripple/associations/many_linked_proxy.rb +66 -0
  25. data/lib/ripple/associations/many_reference_proxy.rb +93 -0
  26. data/lib/ripple/associations/many_stored_key_proxy.rb +76 -0
  27. data/lib/ripple/associations/one.rb +20 -0
  28. data/lib/ripple/associations/one_embedded_proxy.rb +35 -0
  29. data/lib/ripple/associations/one_key_proxy.rb +58 -0
  30. data/lib/ripple/associations/one_linked_proxy.rb +22 -0
  31. data/lib/ripple/associations/one_stored_key_proxy.rb +43 -0
  32. data/lib/ripple/associations/proxy.rb +118 -0
  33. data/lib/ripple/attribute_methods.rb +118 -0
  34. data/lib/ripple/attribute_methods/dirty.rb +50 -0
  35. data/lib/ripple/attribute_methods/query.rb +34 -0
  36. data/lib/ripple/attribute_methods/read.rb +26 -0
  37. data/lib/ripple/attribute_methods/write.rb +25 -0
  38. data/lib/ripple/callbacks.rb +74 -0
  39. data/lib/ripple/conflict/basic_resolver.rb +82 -0
  40. data/lib/ripple/conflict/document_hooks.rb +20 -0
  41. data/lib/ripple/conflict/resolver.rb +71 -0
  42. data/lib/ripple/conflict/test_helper.rb +33 -0
  43. data/lib/ripple/conversion.rb +28 -0
  44. data/lib/ripple/core_ext.rb +2 -0
  45. data/lib/ripple/core_ext/casting.rb +148 -0
  46. data/lib/ripple/core_ext/object.rb +8 -0
  47. data/lib/ripple/document.rb +104 -0
  48. data/lib/ripple/document/bucket_access.rb +25 -0
  49. data/lib/ripple/document/finders.rb +131 -0
  50. data/lib/ripple/document/key.rb +43 -0
  51. data/lib/ripple/document/link.rb +30 -0
  52. data/lib/ripple/document/persistence.rb +115 -0
  53. data/lib/ripple/embedded_document.rb +64 -0
  54. data/lib/ripple/embedded_document/around_callbacks.rb +18 -0
  55. data/lib/ripple/embedded_document/finders.rb +26 -0
  56. data/lib/ripple/embedded_document/persistence.rb +77 -0
  57. data/lib/ripple/i18n.rb +2 -0
  58. data/lib/ripple/inspection.rb +32 -0
  59. data/lib/ripple/locale/en.yml +21 -0
  60. data/lib/ripple/nested_attributes.rb +265 -0
  61. data/lib/ripple/observable.rb +28 -0
  62. data/lib/ripple/properties.rb +73 -0
  63. data/lib/ripple/property_type_mismatch.rb +12 -0
  64. data/lib/ripple/railtie.rb +13 -0
  65. data/lib/ripple/serialization.rb +84 -0
  66. data/lib/ripple/timestamps.rb +27 -0
  67. data/lib/ripple/translation.rb +14 -0
  68. data/lib/ripple/validations.rb +67 -0
  69. data/lib/ripple/validations/associated_validator.rb +43 -0
  70. data/ripple.gemspec +46 -0
  71. data/seomoz-ripple.gemspec +46 -0
  72. data/spec/fixtures/config.yml +8 -0
  73. data/spec/integration/ripple/associations_spec.rb +220 -0
  74. data/spec/integration/ripple/conflict_resolution_spec.rb +293 -0
  75. data/spec/integration/ripple/nested_attributes_spec.rb +264 -0
  76. data/spec/integration/ripple/persistence_spec.rb +57 -0
  77. data/spec/integration/ripple/search_associations_spec.rb +42 -0
  78. data/spec/ripple/associations/many_embedded_proxy_spec.rb +122 -0
  79. data/spec/ripple/associations/many_linked_proxy_spec.rb +191 -0
  80. data/spec/ripple/associations/many_reference_proxy_spec.rb +170 -0
  81. data/spec/ripple/associations/many_stored_key_proxy_spec.rb +158 -0
  82. data/spec/ripple/associations/one_embedded_proxy_spec.rb +125 -0
  83. data/spec/ripple/associations/one_key_proxy_spec.rb +82 -0
  84. data/spec/ripple/associations/one_linked_proxy_spec.rb +91 -0
  85. data/spec/ripple/associations/one_stored_key_proxy_spec.rb +72 -0
  86. data/spec/ripple/associations/proxy_spec.rb +84 -0
  87. data/spec/ripple/associations_spec.rb +129 -0
  88. data/spec/ripple/attribute_methods/dirty_spec.rb +80 -0
  89. data/spec/ripple/attribute_methods_spec.rb +230 -0
  90. data/spec/ripple/bucket_access_spec.rb +25 -0
  91. data/spec/ripple/callbacks_spec.rb +176 -0
  92. data/spec/ripple/conflict/resolver_spec.rb +42 -0
  93. data/spec/ripple/conversion_spec.rb +22 -0
  94. data/spec/ripple/core_ext_spec.rb +103 -0
  95. data/spec/ripple/document/link_spec.rb +67 -0
  96. data/spec/ripple/document_spec.rb +96 -0
  97. data/spec/ripple/embedded_document/finders_spec.rb +29 -0
  98. data/spec/ripple/embedded_document/persistence_spec.rb +80 -0
  99. data/spec/ripple/embedded_document_spec.rb +84 -0
  100. data/spec/ripple/finders_spec.rb +217 -0
  101. data/spec/ripple/inspection_spec.rb +51 -0
  102. data/spec/ripple/key_spec.rb +30 -0
  103. data/spec/ripple/observable_spec.rb +121 -0
  104. data/spec/ripple/persistence_spec.rb +326 -0
  105. data/spec/ripple/properties_spec.rb +262 -0
  106. data/spec/ripple/ripple_spec.rb +71 -0
  107. data/spec/ripple/serialization_spec.rb +51 -0
  108. data/spec/ripple/timestamps_spec.rb +76 -0
  109. data/spec/ripple/validations/associated_validator_spec.rb +77 -0
  110. data/spec/ripple/validations_spec.rb +104 -0
  111. data/spec/spec_helper.rb +26 -0
  112. data/spec/support/associations.rb +1 -0
  113. data/spec/support/associations/proxies.rb +17 -0
  114. data/spec/support/integration_setup.rb +11 -0
  115. data/spec/support/mocks.rb +4 -0
  116. data/spec/support/models.rb +4 -0
  117. data/spec/support/models/address.rb +12 -0
  118. data/spec/support/models/box.rb +13 -0
  119. data/spec/support/models/car.rb +16 -0
  120. data/spec/support/models/cardboard_box.rb +3 -0
  121. data/spec/support/models/clock.rb +12 -0
  122. data/spec/support/models/clock_observer.rb +3 -0
  123. data/spec/support/models/company.rb +23 -0
  124. data/spec/support/models/customer.rb +4 -0
  125. data/spec/support/models/driver.rb +6 -0
  126. data/spec/support/models/email.rb +4 -0
  127. data/spec/support/models/engine.rb +5 -0
  128. data/spec/support/models/family.rb +16 -0
  129. data/spec/support/models/favorite.rb +4 -0
  130. data/spec/support/models/invoice.rb +7 -0
  131. data/spec/support/models/late_invoice.rb +3 -0
  132. data/spec/support/models/ninja.rb +9 -0
  133. data/spec/support/models/note.rb +5 -0
  134. data/spec/support/models/page.rb +4 -0
  135. data/spec/support/models/paid_invoice.rb +4 -0
  136. data/spec/support/models/passenger.rb +6 -0
  137. data/spec/support/models/profile.rb +10 -0
  138. data/spec/support/models/seat.rb +5 -0
  139. data/spec/support/models/subscription.rb +27 -0
  140. data/spec/support/models/tasks.rb +14 -0
  141. data/spec/support/models/team.rb +11 -0
  142. data/spec/support/models/transactions.rb +17 -0
  143. data/spec/support/models/tree.rb +4 -0
  144. data/spec/support/models/user.rb +10 -0
  145. data/spec/support/models/wheel.rb +6 -0
  146. data/spec/support/models/widget.rb +22 -0
  147. data/spec/support/test_server.rb +18 -0
  148. data/spec/support/test_server.yml.example +2 -0
  149. metadata +362 -0
@@ -0,0 +1,20 @@
1
+ require 'rails/generators/ripple_generator'
2
+
3
+ module Ripple
4
+ module Generators
5
+ class ModelGenerator < NamedBase
6
+ desc 'Creates a ripple model'
7
+ argument :attributes, :type => :array, :default => [], :banner => 'field:type field:type'
8
+ class_option :parent, :type => :string, :desc => "The parent class for the generated model"
9
+ class_option :embedded, :type => :boolean, :desc => "Make an embedded document model.", :default => false
10
+ class_option :embedded_in, :type => :string, :desc => "Specify the enclosing model for the embedded document. Implies --embedded."
11
+ check_class_collision
12
+
13
+ def create_model_file
14
+ template 'model.rb', "app/models/#{file_path}.rb"
15
+ end
16
+
17
+ hook_for :test_framework
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ class <%= class_name %><%= " < #{options[:parent].classify}" if options[:parent] %>
2
+ <% unless options[:parent] -%>
3
+ include Ripple::<%= "Embedded" if options[:embedded] || options[:embedded_in] %>Document
4
+ <% if options[:embedded_in] -%>embedded_in :<%= options[:embedded_in].underscore %><% end -%>
5
+ <% end -%>
6
+
7
+ <% attributes.reject{|attr| attr.reference?}.each do |attribute| -%>
8
+ property :<%= attribute.name %>, <%= attribute.type_class %>
9
+ <% end -%>
10
+ end
@@ -0,0 +1,16 @@
1
+ require 'rails/generators/ripple_generator'
2
+
3
+ module Ripple
4
+ module Generators
5
+ class ObserverGenerator < NamedBase
6
+ desc 'Creates an observer for Ripple documents'
7
+ check_class_collision :suffix => "Observer"
8
+
9
+ def create_observer_file
10
+ template 'observer.rb', File.join("app/models", class_path, "#{file_name}_observer.rb")
11
+ end
12
+
13
+ hook_for :test_framework
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %>Observer < ActiveModel::Observer
3
+ end
4
+ <% end -%>
@@ -0,0 +1,46 @@
1
+ require 'riak/test_server'
2
+
3
+ module Ripple
4
+ module TestServer
5
+ extend self
6
+
7
+ # Tweak this to change how your test server is configured
8
+ def test_server_config
9
+ {
10
+ :app_config => {
11
+ :riak_kv => {
12
+ :js_source_dir => Ripple.config.delete(:js_source_dir),
13
+ :map_cache_size => 0, # 0.14
14
+ :vnode_cache_entries => 0 # 0.13
15
+ },
16
+ :riak_core => { :web_port => Ripple.config[:http_port] || 8098 }
17
+ },
18
+ :bin_dir => Ripple.config.delete(:bin_dir),
19
+ :temp_dir => Rails.root + "tmp/riak_test_server"
20
+ }
21
+ end
22
+
23
+ # Prepares the subprocess Riak node for the test suite
24
+ def setup
25
+ unless @test_server
26
+ begin
27
+ _server = @test_server = Riak::TestServer.new(test_server_config)
28
+ @test_server.prepare!
29
+ @test_server.start
30
+ at_exit { _server.cleanup }
31
+ rescue => e
32
+ warn "Can't run tests with Riak::TestServer. Specify the location of your Riak installation in the config/ripple.yml #{Rails.env} environment."
33
+ warn e.inspect
34
+ @test_server = nil
35
+ end
36
+ end
37
+ end
38
+
39
+ # Clear the data after each test run
40
+ def clear
41
+ @test_server.recycle if @test_server
42
+ end
43
+ end
44
+ end
45
+
46
+ Ripple::TestServer.setup
@@ -0,0 +1,39 @@
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?(Rails.root + "features/support")
10
+ template 'test_server.rb', 'features/support/ripple.rb'
11
+ insert_into_file 'features/support/ripple.rb', "\n\nAfter do\n Ripple::TestServer.clear\nend", :after => "Ripple::TestServer.setup"
12
+ end
13
+ end
14
+
15
+ # RSpec
16
+ def create_rspec_file
17
+ if File.file?(Rails.root + 'spec/spec_helper.rb')
18
+ template 'test_server.rb', 'spec/support/ripple.rb'
19
+ inject_into_file 'spec/spec_helper.rb', :after => /R[Ss]pec\.configure do \|config\|/ do
20
+ "\n config.after(:each) do\n Ripple::TestServer.clear\n end\n"
21
+ end
22
+ end
23
+ end
24
+
25
+ # Test::Unit
26
+ def create_test_unit_file
27
+ if File.file?(Rails.root + 'test/test_helper.rb')
28
+ template 'test_server.rb', 'test/ripple_test_helper.rb'
29
+ inject_into_file "test/test_helper.rb", :before => "class ActiveSupport::TestCase" do
30
+ "# Setup in-memory test server for Riak\nrequire File.expand_path('../ripple_test_helper.rb', __FILE__)\n\n"
31
+ end
32
+ inject_into_class "test/test_helper.rb", ActiveSupport::TestCase do
33
+ " teardown { Ripple::TestServer.clear }\n\n"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,78 @@
1
+ require "rails/generators/named_base"
2
+ require "rails/generators/active_model"
3
+
4
+ class RippleGenerator < ::Rails::Generators::Base
5
+ def create_ripple
6
+ invoke "ripple:configuration"
7
+ invoke "ripple:js"
8
+ invoke "ripple:test"
9
+ end
10
+ end
11
+
12
+ module Ripple
13
+ # ActiveModel generators for use in a Rails project.
14
+ module Generators
15
+ # @private
16
+ class Base < ::Rails::Generators::Base
17
+ def self.source_root
18
+ @_ripple_source_root ||=
19
+ File.expand_path("../#{base_name}/#{generator_name}/templates", __FILE__)
20
+ end
21
+ end
22
+
23
+ class NamedBase < ::Rails::Generators::NamedBase
24
+ def self.source_root
25
+ @_ripple_source_root ||=
26
+ File.expand_path("../#{base_name}/#{generator_name}/templates", __FILE__)
27
+ end
28
+ end
29
+
30
+ # Generator for a {Ripple::Document} model
31
+ class ActiveModel < ::Rails::Generators::ActiveModel
32
+ def self.all(klass)
33
+ "#{klass}.list"
34
+ end
35
+
36
+ def self.find(klass, params=nil)
37
+ "#{klass}.find(#{params})"
38
+ end
39
+
40
+ def self.build(klass, params=nil)
41
+ if params
42
+ "#{klass}.new(#{params})"
43
+ else
44
+ "#{klass}.new"
45
+ end
46
+ end
47
+
48
+ def save
49
+ "#{name}.save"
50
+ end
51
+
52
+ def update_attributes(params=nil)
53
+ "#{name}.update_attributes(#{params})"
54
+ end
55
+
56
+ def errors
57
+ "#{name}.errors"
58
+ end
59
+
60
+ def destroy
61
+ "#{name}.destroy"
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ # @private
68
+ module Rails
69
+ module Generators
70
+ class GeneratedAttribute #:nodoc:
71
+ def type_class
72
+ return "Time" if type.to_s == "datetime"
73
+ return "String" if type.to_s == "text"
74
+ return type.to_s.camelcase
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,79 @@
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
+ self.config = config_hash || {}
58
+ rescue Errno::ENOENT
59
+ raise Ripple::MissingConfiguration.new(config_file)
60
+ end
61
+ alias_method :load_config, :load_configuration
62
+
63
+ private
64
+ def client_config
65
+ config.slice(*Riak::Client::VALID_OPTIONS)
66
+ end
67
+ end
68
+
69
+ # Exception raised when the path passed to
70
+ # {Ripple::load_configuration} does not point to a existing file.
71
+ class MissingConfiguration < StandardError
72
+ include Translation
73
+ def initialize(file_path)
74
+ super(t("missing_configuration", :file => file_path))
75
+ end
76
+ end
77
+ end
78
+
79
+ require 'ripple/railtie' if defined? Rails::Railtie
@@ -0,0 +1,356 @@
1
+ require 'active_support/concern'
2
+ require 'riak/walk_spec'
3
+ require 'ripple/translation'
4
+ require 'ripple/associations/proxy'
5
+ require 'ripple/associations/instantiators'
6
+ require 'ripple/associations/linked'
7
+ require 'ripple/associations/embedded'
8
+ require 'ripple/associations/many'
9
+ require 'ripple/associations/one'
10
+ require 'ripple/associations/linked'
11
+ require 'ripple/associations/one_embedded_proxy'
12
+ require 'ripple/associations/many_embedded_proxy'
13
+ require 'ripple/associations/one_linked_proxy'
14
+ require 'ripple/associations/many_linked_proxy'
15
+ require 'ripple/associations/many_stored_key_proxy'
16
+ require 'ripple/associations/one_key_proxy'
17
+ require 'ripple/associations/one_stored_key_proxy'
18
+ require 'ripple/associations/many_reference_proxy'
19
+
20
+ module Ripple
21
+ # Adds associations via links and embedding to {Ripple::Document}
22
+ # models. Examples:
23
+ #
24
+ # # Documents can contain embedded documents, and link to other standalone documents
25
+ # # via associations using the many and one class methods.
26
+ # class Person
27
+ # include Ripple::Document
28
+ # property :name, String
29
+ # many :addresses
30
+ # many :friends, :class_name => "Person"
31
+ # one :account
32
+ # end
33
+ #
34
+ # # Account and Address are embeddable documents
35
+ # class Account
36
+ # include Ripple::EmbeddedDocument
37
+ # property :paid_until, Time
38
+ # embedded_in :person # Adds "person" method to get parent document
39
+ # end
40
+ #
41
+ # class Address
42
+ # include Ripple::EmbeddedDocument
43
+ # property :street, String
44
+ # property :city, String
45
+ # property :state, String
46
+ # property :zip, String
47
+ # end
48
+ #
49
+ # person = Person.find("adamhunter")
50
+ # person.friends << Person.find("seancribbs") # Links to people/seancribbs with tag "friend"
51
+ # person.addresses << Address.new(:street => "100 Main Street") # Adds an embedded address
52
+ # person.account.paid_until = 3.months.from_now
53
+ module Associations
54
+ extend ActiveSupport::Concern
55
+
56
+ module ClassMethods
57
+ include Translation
58
+ # @private
59
+ def inherited(subclass)
60
+ super
61
+ subclass.associations.merge!(associations)
62
+ end
63
+
64
+ # Associations defined on the document
65
+ def associations
66
+ @associations ||= {}.with_indifferent_access
67
+ end
68
+
69
+ # Associations of embedded documents
70
+ def embedded_associations
71
+ associations.values.select(&:embedded?)
72
+ end
73
+
74
+ # Associations of linked documents
75
+ def linked_associations
76
+ associations.values.select(&:linked?)
77
+ end
78
+
79
+ # Associations of stored_key documents
80
+ def stored_key_associations
81
+ associations.values.select(&:stored_key?)
82
+ end
83
+
84
+ # Creates a singular association
85
+ def one(name, options={})
86
+ configure_for_key_correspondence if options[:using] === :key
87
+ create_association(:one, name, options)
88
+ end
89
+
90
+ # Creates a plural association
91
+ def many(name, options={})
92
+ raise ArgumentError, t('many_key_association') if options[:using] === :key
93
+ create_association(:many, name, options)
94
+ end
95
+
96
+ def configure_for_key_correspondence
97
+ include Ripple::Associations::KeyDelegator
98
+ end
99
+
100
+ private
101
+ def create_association(type, name, options={})
102
+ association = associations[name] = Association.new(type, name, options)
103
+ association.setup_on(self)
104
+
105
+ define_method(name) do
106
+ get_proxy(association)
107
+ end
108
+
109
+ define_method("#{name}=") do |value|
110
+ get_proxy(association).replace(value)
111
+ value
112
+ end
113
+
114
+ unless association.many?
115
+ define_method("#{name}?") do
116
+ get_proxy(association).present?
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ module InstanceMethods
123
+ # @private
124
+ def get_proxy(association)
125
+ unless proxy = instance_variable_get(association.ivar)
126
+ proxy = association.proxy_class.new(self, association)
127
+ instance_variable_set(association.ivar, proxy)
128
+ end
129
+ proxy
130
+ end
131
+
132
+ # @private
133
+ def reset_associations
134
+ self.class.associations.each do |name, assoc_object|
135
+ send(name).reset
136
+ end
137
+ end
138
+
139
+ # Adds embedded documents to the attributes
140
+ # @private
141
+ def attributes_for_persistence
142
+ self.class.embedded_associations.inject(super) do |attrs, association|
143
+ documents = instance_variable_get(association.ivar)
144
+ # We must explicitly check #nil? (rather than just saying `if documents`)
145
+ # because documents can be an association proxy that is proxying nil.
146
+ # In this case ruby treats documents as true because it is not _really_ nil,
147
+ # but #nil? will tell us if it is proxying nil.
148
+
149
+ unless documents.nil?
150
+ attrs[association.name] = documents.is_a?(Array) ? documents.map(&:attributes_for_persistence) : documents.attributes_for_persistence
151
+ end
152
+ attrs
153
+ end
154
+ end
155
+
156
+ def propagate_callbacks_to_embedded_associations(name, kind)
157
+ self.class.embedded_associations.each do |association|
158
+ documents = instance_variable_get(association.ivar)
159
+ # We must explicitly check #nil? (rather than just saying `if documents`)
160
+ # because documents can be an association proxy that is proxying nil.
161
+ # In this case ruby treats documents as true because it is not _really_ nil,
162
+ # but #nil? will tell us if it is proxying nil.
163
+ next if documents.nil?
164
+
165
+ Array(documents).each do |doc|
166
+ doc.send("_#{name}_callbacks").each do |callback|
167
+ next unless callback.kind == kind
168
+ doc.send(callback.filter)
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ # Propagates callbacks (save/create/update/destroy) to embedded associated documents.
175
+ # This is necessary so that when a parent is saved, the embedded child's before_save
176
+ # hooks are run as well.
177
+ # @private
178
+ def run_callbacks(name, *args, &block)
179
+ # validation is already propagated to embedded documents via the
180
+ # AssociatedValidator. We don't need to duplicate the propgation here.
181
+ return super if name == :validation
182
+
183
+ propagate_callbacks_to_embedded_associations(name, :before)
184
+ return_value = super
185
+ propagate_callbacks_to_embedded_associations(name, :after)
186
+ return_value
187
+ end
188
+ end
189
+ end
190
+
191
+ # The "reflection" for an association - metadata about how it is
192
+ # configured.
193
+ class Association
194
+ include Ripple::Translation
195
+ attr_reader :type, :name, :options
196
+
197
+ # association options :using, :class_name, :class, :extend,
198
+ # options that may be added :validate
199
+
200
+ def initialize(type, name, options={})
201
+ @type, @name, @options = type, name, options.to_options
202
+ end
203
+
204
+ # @return String The class name of the associated object(s)
205
+ def class_name
206
+ @class_name ||= case
207
+ when @options[:class_name]
208
+ @options[:class_name]
209
+ when @options[:class]
210
+ @options[:class].to_s
211
+ when many?
212
+ @name.to_s.classify
213
+ else
214
+ @name.to_s.camelize
215
+ end
216
+ end
217
+
218
+ # @return [Class] The class of the associated object(s)
219
+ def klass
220
+ @klass ||= options[:class] || class_name.constantize
221
+ end
222
+
223
+ # @return [true,false] Is the cardinality of the association > 1
224
+ def many?
225
+ @type == :many
226
+ end
227
+
228
+ # @return [true,false] Is the cardinality of the association == 1
229
+ def one?
230
+ @type == :one
231
+ end
232
+
233
+ # @return [true,false] Is the associated class an EmbeddedDocument
234
+ def embedded?
235
+ klass.embeddable?
236
+ end
237
+
238
+ # TODO: Polymorphic not supported
239
+ # @return [true,false] Does the association support more than one associated class
240
+ def polymorphic?
241
+ false
242
+ end
243
+
244
+ # @return [true,false] Does the association use links
245
+ def linked?
246
+ using == :linked
247
+ end
248
+
249
+ # @return [true,false] Does the association use stored_key
250
+ def stored_key?
251
+ using == :stored_key
252
+ end
253
+
254
+ # @return [String] the instance variable in the owner where the association will be stored
255
+ def ivar
256
+ "@_#{name}"
257
+ end
258
+
259
+ # @return [Class] the association proxy class
260
+ def proxy_class
261
+ @proxy_class ||= proxy_class_name.constantize
262
+ end
263
+
264
+ # @return [String] the class name of the association proxy
265
+ def proxy_class_name
266
+ klass_name = (many? ? 'Many' : 'One') + using.to_s.camelize + ('Polymorphic' if polymorphic?).to_s + 'Proxy'
267
+ "Ripple::Associations::#{klass_name}"
268
+ end
269
+
270
+ # @return [Proc] a filter proc to be used with Enumerable#select for collecting links that belong to this association (only when #linked? is true)
271
+ def link_filter
272
+ linked? ? lambda {|link| link.tag == link_tag } : lambda {|_| false }
273
+ end
274
+
275
+ # @return [String,nil] when #linked? is true, the tag for outgoing links
276
+ def link_tag
277
+ linked? ? Array(link_spec).first.tag : nil
278
+ end
279
+
280
+ def bucket_name
281
+ polymorphic? ? '_' : klass.bucket_name
282
+ end
283
+
284
+ # @return [Riak::WalkSpec] when #linked? is true, a specification for which links to follow to retrieve the associated documents
285
+ def link_spec
286
+ # TODO: support transitive linked associations
287
+ if linked?
288
+ tag = name.to_s
289
+ Riak::WalkSpec.new(:tag => tag, :bucket => bucket_name)
290
+ else
291
+ nil
292
+ end
293
+ end
294
+
295
+ # @return [Symbol] which method is used for representing the association - currently only supports :embedded and :linked
296
+ def using
297
+ @using ||= options[:using] || (embedded? ? :embedded : :linked)
298
+ end
299
+
300
+ # @raise [ArgumentError] if the value does not match the class of the association
301
+ def verify_type!(value, owner)
302
+ unless type_matches?(value)
303
+ raise ArgumentError.new(t('invalid_association_value',
304
+ :name => name,
305
+ :owner => owner.inspect,
306
+ :klass => polymorphic? ? "<polymorphic>" : klass.name,
307
+ :value => value.inspect))
308
+ end
309
+ end
310
+
311
+ # @private
312
+ def type_matches?(value)
313
+ case
314
+ when polymorphic?
315
+ one? || Array === value
316
+ when many?
317
+ Array === value && value.all? {|d| (embedded? && Hash === d) || klass === d }
318
+ when one?
319
+ value.nil? || (embedded? && Hash === value) || value.kind_of?(klass)
320
+ end
321
+ end
322
+
323
+ def uses_search?
324
+ (options[:using] == :reference)
325
+ end
326
+
327
+ def setup_on(target)
328
+ define_callbacks_on(target)
329
+ if uses_search?
330
+ klass.before_save do |o|
331
+ unless o.class.bucket.is_indexed?
332
+ o.class.bucket.enable_index!
333
+ end
334
+ end
335
+ end
336
+ end
337
+
338
+ def define_callbacks_on(klass)
339
+ _association = self
340
+
341
+ klass.before_save do
342
+ if _association.linked? && !@_in_save_loaded_documents_callback
343
+ @_in_save_loaded_documents_callback = true
344
+
345
+ begin
346
+ send(_association.name).loaded_documents.each do |document|
347
+ document.save if document.new? || document.changed?
348
+ end
349
+ ensure
350
+ remove_instance_variable(:@_in_save_loaded_documents_callback)
351
+ end
352
+ end
353
+ end
354
+ end
355
+ end
356
+ end