ripple 1.0.0.beta → 1.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. data/.gitignore +10 -6
  2. data/Gemfile +6 -15
  3. data/Gemfile.rails30 +3 -0
  4. data/Gemfile.rails31 +3 -0
  5. data/Gemfile.rails32 +3 -0
  6. data/Guardfile +3 -1
  7. data/LICENSE +16 -0
  8. data/README.markdown +173 -0
  9. data/RELEASE_NOTES.textile +286 -0
  10. data/Rakefile +19 -0
  11. data/lib/rails/generators/ripple/configuration/templates/ripple.yml +1 -0
  12. data/lib/rails/generators/ripple/model/model_generator.rb +1 -1
  13. data/lib/rails/generators/ripple/model/templates/{model.rb → model.rb.erb} +0 -0
  14. data/lib/rails/generators/ripple/observer/observer_generator.rb +1 -1
  15. data/lib/rails/generators/ripple/observer/templates/{observer.rb → observer.rb.erb} +0 -2
  16. data/lib/rails/generators/ripple/test/templates/cucumber.rb.erb +7 -0
  17. data/lib/rails/generators/ripple/test/test_generator.rb +17 -13
  18. data/lib/rails/generators/ripple_generator.rb +1 -0
  19. data/lib/ripple/associations.rb +65 -55
  20. data/lib/ripple/associations/embedded.rb +1 -1
  21. data/lib/ripple/associations/linked.rb +1 -1
  22. data/lib/ripple/associations/many.rb +1 -1
  23. data/lib/ripple/associations/many_embedded_proxy.rb +3 -2
  24. data/lib/ripple/associations/many_linked_proxy.rb +1 -1
  25. data/lib/ripple/associations/many_reference_proxy.rb +7 -5
  26. data/lib/ripple/associations/proxy.rb +2 -2
  27. data/lib/ripple/attribute_methods.rb +69 -61
  28. data/lib/ripple/attribute_methods/dirty.rb +2 -2
  29. data/lib/ripple/attribute_methods/read.rb +4 -2
  30. data/lib/ripple/callbacks.rb +23 -26
  31. data/lib/ripple/conflict/basic_resolver.rb +6 -2
  32. data/lib/ripple/conflict/document_hooks.rb +26 -0
  33. data/lib/ripple/conflict/resolver.rb +10 -2
  34. data/lib/ripple/conflict/test_helper.rb +3 -2
  35. data/lib/ripple/conversion.rb +1 -0
  36. data/lib/ripple/core_ext.rb +1 -0
  37. data/lib/ripple/core_ext/casting.rb +2 -0
  38. data/lib/ripple/core_ext/indexes.rb +89 -0
  39. data/lib/ripple/document.rb +23 -22
  40. data/lib/ripple/document/key.rb +12 -14
  41. data/lib/ripple/document/persistence.rb +99 -84
  42. data/lib/ripple/embedded_document.rb +9 -10
  43. data/lib/ripple/embedded_document/persistence.rb +42 -44
  44. data/lib/ripple/i18n.rb +4 -1
  45. data/lib/ripple/indexes.rb +151 -0
  46. data/lib/ripple/locale/en.yml +4 -0
  47. data/lib/ripple/locale/fr.yml +24 -0
  48. data/lib/ripple/nested_attributes.rb +92 -90
  49. data/lib/ripple/properties.rb +2 -1
  50. data/lib/ripple/railtie.rb +9 -0
  51. data/lib/ripple/railties/ripple.rake +32 -15
  52. data/lib/ripple/serialization.rb +50 -52
  53. data/lib/ripple/test_server.rb +1 -2
  54. data/lib/ripple/timestamps.rb +6 -8
  55. data/lib/ripple/validations.rb +19 -21
  56. data/lib/ripple/version.rb +1 -1
  57. data/ripple.gemspec +6 -5
  58. data/spec/generators/ripple/configuration_generator_spec.rb +9 -0
  59. data/spec/generators/ripple/js_generator_spec.rb +14 -0
  60. data/spec/generators/ripple/model_generator_spec.rb +64 -0
  61. data/spec/generators/ripple/observer_generator_spec.rb +20 -0
  62. data/spec/generators/ripple/test_generator_spec.rb +116 -0
  63. data/spec/generators/ripple_generator_spec.rb +11 -0
  64. data/spec/integration/ripple/conflict_resolution_spec.rb +35 -4
  65. data/spec/integration/ripple/indexes_spec.rb +47 -0
  66. data/spec/ripple/associations/many_embedded_proxy_spec.rb +50 -60
  67. data/spec/ripple/associations/many_linked_proxy_spec.rb +2 -2
  68. data/spec/ripple/associations/many_reference_proxy_spec.rb +1 -1
  69. data/spec/ripple/associations_spec.rb +16 -7
  70. data/spec/ripple/attribute_methods_spec.rb +43 -2
  71. data/spec/ripple/callbacks_spec.rb +120 -101
  72. data/spec/ripple/conversion_spec.rb +5 -13
  73. data/spec/ripple/core_ext_spec.rb +93 -15
  74. data/spec/ripple/finders_spec.rb +0 -2
  75. data/spec/ripple/indexes_spec.rb +111 -0
  76. data/spec/ripple/observable_spec.rb +1 -2
  77. data/spec/ripple/persistence_spec.rb +55 -32
  78. data/spec/ripple/properties_spec.rb +1 -1
  79. data/spec/ripple/ripple_spec.rb +5 -5
  80. data/spec/ripple/timestamps_spec.rb +9 -2
  81. data/spec/ripple/validations_spec.rb +50 -52
  82. data/spec/spec_helper.rb +9 -2
  83. data/spec/support/generator_setup.rb +26 -0
  84. data/spec/support/models.rb +1 -0
  85. data/spec/support/models/box.rb +1 -0
  86. data/spec/support/models/clock.rb +1 -1
  87. data/spec/support/models/indexer.rb +26 -0
  88. data/spec/support/models/post.rb +3 -2
  89. data/spec/support/models/widget.rb +2 -0
  90. data/spec/support/search.rb +2 -2
  91. data/spec/support/test_server.rb +23 -11
  92. data/spec/support/test_server.yml.example +1 -1
  93. metadata +159 -135
  94. data/spec/support/mocks.rb +0 -4
@@ -33,8 +33,8 @@ module Ripple
33
33
  end
34
34
 
35
35
  # @private
36
- def initialize(attrs={})
37
- super(attrs)
36
+ def initialize(*args)
37
+ super
38
38
  changed_attributes.clear
39
39
  end
40
40
 
@@ -5,8 +5,10 @@ module Ripple
5
5
  module Read
6
6
  extend ActiveSupport::Concern
7
7
 
8
- included do
9
- attribute_method_suffix ""
8
+ if ActiveSupport::VERSION::STRING < '3.2'
9
+ included do
10
+ attribute_method_suffix ''
11
+ end
10
12
  end
11
13
 
12
14
  def [](attr_name)
@@ -38,36 +38,33 @@ module Ripple
38
38
  end
39
39
 
40
40
  # @private
41
- module InstanceMethods
42
- # @private
43
- def really_save(*args, &block)
44
- run_save_callbacks do
45
- super
46
- end
41
+ def really_save(*args, &block)
42
+ run_save_callbacks do
43
+ super
47
44
  end
48
-
49
- def run_save_callbacks
50
- state = new? ? :create : :update
51
- run_callbacks(:save) do
52
- run_callbacks(state) do
53
- yield
54
- end
45
+ end
46
+
47
+ def run_save_callbacks
48
+ state = new? ? :create : :update
49
+ run_callbacks(:save) do
50
+ run_callbacks(state) do
51
+ yield
55
52
  end
56
53
  end
57
-
58
- # @private
59
- def destroy(*args, &block)
60
- run_callbacks(:destroy) do
61
- super
62
- end
54
+ end
55
+
56
+ # @private
57
+ def destroy!(*args, &block)
58
+ run_callbacks(:destroy) do
59
+ super
63
60
  end
64
-
65
- # @private
66
- def valid?(*args, &block)
67
- @_on_validate = new? ? :create : :update
68
- run_callbacks(:validation) do
69
- super
70
- end
61
+ end
62
+
63
+ # @private
64
+ def valid?(*args, &block)
65
+ @_on_validate = new? ? :create : :update
66
+ run_callbacks(:validation) do
67
+ super
71
68
  end
72
69
  end
73
70
  end
@@ -28,6 +28,10 @@ module Ripple
28
28
 
29
29
  private
30
30
 
31
+ def undeleted_siblings
32
+ @undeleted_siblings ||= siblings.reject(&:deleted?)
33
+ end
34
+
31
35
  def process_properties
32
36
  model_class.properties.each do |name, property|
33
37
  document.send(:"#{name}=", resolved_property_value_for(property))
@@ -53,7 +57,7 @@ module Ripple
53
57
  end
54
58
 
55
59
  def resolved_property_value_for(property)
56
- uniq_values = siblings.map(&property.key).uniq
60
+ uniq_values = undeleted_siblings.map(&property.key).uniq
57
61
 
58
62
  value = if uniq_values.size == 1
59
63
  uniq_values.first
@@ -69,7 +73,7 @@ module Ripple
69
73
 
70
74
  def resolved_association_value_for(association, proxy_value_method)
71
75
  # the association proxy doesn't uniquify well, so we have to use the target or links directly
72
- uniq_values = siblings.map { |s| s.send(association.name).__send__(proxy_value_method) }.uniq
76
+ uniq_values = undeleted_siblings.map { |s| s.send(association.name).__send__(proxy_value_method) }.uniq
73
77
 
74
78
  return uniq_values.first if uniq_values.size == 1
75
79
  remaining_conflicts << association.name
@@ -4,13 +4,39 @@ module Ripple
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  module ClassMethods
7
+ # @return [Proc] the registered conflict handler
7
8
  attr_reader :on_conflict_block
8
9
 
10
+ # Registers a conflict handler for this model.
11
+ #
12
+ # @param [Array<Symbol>] expected_conflicts the list of properties and associations
13
+ # you expect to be in conflict.
14
+ # @yield the conflict handler block
15
+ # @yieldparam [Array<Ripple::Document>] siblings the sibling documents
16
+ # @yieldparam [Array<Symbol>] conflicts the properties and associations that could not
17
+ # be resolved by ripple's basic resolution logic.
18
+ #
19
+ # The block is instance_eval'd in the context of a partially resolved model instance.
20
+ # Thus, you should apply your resolution logic directly to self. Before calling
21
+ # your block, Ripple attempts some basic resolution on your behalf:
22
+ #
23
+ # * Any property or association for which all siblings agree will be set to the common value.
24
+ # * created_at will be set to the minimum value.
25
+ # * updated_at will be set to the maximum value.
26
+ # * All other properties and associations will be set to the default: nil or the default
27
+ # value for a property, nil for a one association, and an empty array for a many association.
28
+ #
29
+ # Note that any conflict you do not resolve is a potential source of data loss (since ripple
30
+ # sets it to a default such as nil). It is recommended (but not required) that you pass the list
31
+ # of expected conflicts, as that informs ripple of what conflicts your block handles. If it detects
32
+ # conflicts for any other properties or associations, a NotImplementedError will be raised.
9
33
  def on_conflict(*expected_conflicts, &block)
10
34
  @expected_conflicts = expected_conflicts
11
35
  @on_conflict_block = block
12
36
  end
13
37
 
38
+ # @return [Array<Symbol>] list of properties and associations that are expected
39
+ # to be in conflict.
14
40
  def expected_conflicts
15
41
  @expected_conflicts ||= []
16
42
  end
@@ -11,7 +11,7 @@ module Ripple
11
11
 
12
12
  def self.to_proc
13
13
  @to_proc ||= lambda do |robject|
14
- possible_model_classes = robject.siblings.map { |s| s.data['_type'] }.uniq
14
+ possible_model_classes = robject.siblings.map { |s| s.data && s.data['_type'] }.compact.uniq
15
15
  return nil unless possible_model_classes.size == 1
16
16
 
17
17
  resolver = new(robject, possible_model_classes.first.constantize)
@@ -36,7 +36,15 @@ module Ripple
36
36
  end
37
37
 
38
38
  def siblings
39
- @siblings ||= @robject.siblings.map { |s| @model_class.send(:instantiate, s) }
39
+ @siblings ||= @robject.siblings.map do |s|
40
+ @model_class.send(:instantiate, s).tap do |record|
41
+ # TODO: make the deleted conditional explicit by putting logic in
42
+ # RObject to know it has loaded a deleted sibling.
43
+ # Here we assume it is deleted if the data is nil because
44
+ # that's the only way we know of that the data can be nil.
45
+ record.instance_variable_set(:@deleted, true) if s.data.nil?
46
+ end
47
+ end
40
48
  end
41
49
 
42
50
  def document
@@ -9,14 +9,15 @@ module Ripple
9
9
 
10
10
  begin
11
11
  klass, key = main_record.class, main_record.key
12
+ raise "#{klass.bucket.name} allow_mult property is false!" unless klass.bucket.allow_mult
12
13
  records = modifiers.map { |_| klass.find!(key) }
13
14
 
14
15
  records.zip(modifiers).each do |(record, modifier)|
15
- # necessary to get conflict, so riak thinks they are being saved by different clients
16
+ # necessary to get conflict on 0.14 and earlier, so riak thinks they are being saved by different clients
16
17
  Ripple.client.client_id += 1
17
18
 
18
19
  modifier.call(record)
19
- record.save!
20
+ record.save! unless record.deleted?
20
21
  end
21
22
 
22
23
  robject = klass.bucket.get(key)
@@ -3,6 +3,7 @@ require 'active_model/conversion'
3
3
  module Ripple
4
4
  # Provides ActionPack compatibility for {Ripple::Document} models.
5
5
  module Conversion
6
+ extend ActiveSupport::Concern
6
7
  include ActiveModel::Conversion
7
8
 
8
9
  # True if this is a new document
@@ -1,2 +1,3 @@
1
1
  require 'ripple/core_ext/casting'
2
2
  require 'ripple/core_ext/object'
3
+ require 'ripple/core_ext/indexes'
@@ -71,6 +71,8 @@ BooleanCast = Module.new do
71
71
  !value.zero?
72
72
  when TrueClass, FalseClass
73
73
  value
74
+ when /^\s*0/
75
+ false
74
76
  when /^\s*t/i
75
77
  true
76
78
  when /^\s*f/i
@@ -0,0 +1,89 @@
1
+ require 'tzinfo'
2
+ require 'active_support/core_ext/date/conversions'
3
+ require 'active_support/core_ext/date/zones'
4
+ require 'active_support/core_ext/date_time/conversions'
5
+ require 'active_support/core_ext/date_time/zones'
6
+ require 'active_support/core_ext/time/conversions'
7
+ require 'active_support/core_ext/time/zones'
8
+ require 'active_support/core_ext/string/conversions'
9
+ require 'ripple/property_type_mismatch'
10
+ require 'set'
11
+
12
+ # @private
13
+ class Object
14
+ def to_ripple_index(type)
15
+ case type
16
+ when 'bin'
17
+ to_s
18
+ when 'int'
19
+ to_i
20
+ end
21
+ end
22
+ end
23
+
24
+ # @private
25
+ class Time
26
+ def to_ripple_index(type)
27
+ case type
28
+ when 'bin'
29
+ utc.send(Ripple.date_format)
30
+ when 'int'
31
+ # Use millisecond-precision
32
+ (utc.to_f * 1000).round
33
+ end
34
+ end
35
+ end
36
+
37
+ # @private
38
+ class Date
39
+ def to_ripple_index(type)
40
+ case type
41
+ when 'bin'
42
+ to_s(Ripple.date_format)
43
+ when 'int'
44
+ to_time(:utc).to_ripple_index(type)
45
+ end
46
+ end
47
+ end
48
+
49
+ # @private
50
+ class DateTime
51
+ def to_ripple_index(type)
52
+ case type
53
+ when 'bin'
54
+ utc.to_s(Ripple.date_format)
55
+ when 'int'
56
+ (utc.to_f * 1000).round
57
+ end
58
+ end
59
+ end
60
+
61
+ # @private
62
+ module ActiveSupport
63
+ class TimeWithZone
64
+ def to_ripple_index(type)
65
+ utc.to_ripple_index(type)
66
+ end
67
+ end
68
+ end
69
+
70
+ # @private
71
+ module Enumerable
72
+ def to_ripple_index(type)
73
+ Set.new(map {|v| v.to_ripple_index(type) })
74
+ end
75
+ end
76
+
77
+ if String < Enumerable
78
+ # Fix for 1.8, in which String is Enumerable
79
+ class String
80
+ def to_ripple_index(type)
81
+ case type
82
+ when 'bin'
83
+ to_s
84
+ when 'int'
85
+ to_i
86
+ end
87
+ end
88
+ end
89
+ end
@@ -8,6 +8,7 @@ require 'ripple/document/finders'
8
8
  require 'ripple/document/link'
9
9
  require 'ripple/properties'
10
10
  require 'ripple/attribute_methods'
11
+ require 'ripple/indexes'
11
12
  require 'ripple/timestamps'
12
13
  require 'ripple/validations'
13
14
  require 'ripple/associations'
@@ -55,6 +56,8 @@ module Ripple
55
56
  include Ripple::Document::Finders
56
57
  include Ripple::AttributeMethods
57
58
  include Ripple::Timestamps
59
+ include Ripple::Indexes
60
+ include Ripple::Indexes::DocumentMethods
58
61
  include Ripple::Validations
59
62
  include Ripple::Associations
60
63
  include Ripple::Callbacks
@@ -72,32 +75,30 @@ module Ripple
72
75
  end
73
76
  end
74
77
 
75
- module InstanceMethods
76
- def _root_document
77
- self
78
- end
78
+ def _root_document
79
+ self
80
+ end
79
81
 
80
- # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same key.
81
- def ==(comparison_object)
82
- comparison_object.equal?(self) ||
83
- (comparison_object.class < Document && (comparison_object.instance_of?(self.class) || comparison_object.class.bucket.name == self.class.bucket.name) &&
84
- !new? && comparison_object.key == key && !comparison_object.new?)
85
- end
82
+ # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same key.
83
+ def ==(comparison_object)
84
+ comparison_object.equal?(self) ||
85
+ (comparison_object.class < Document && (comparison_object.instance_of?(self.class) || comparison_object.class.bucket.name == self.class.bucket.name) &&
86
+ !new? && comparison_object.key == key && !comparison_object.new?)
87
+ end
86
88
 
87
- def eql?(other)
88
- return true if other.equal?(self)
89
+ def eql?(other)
90
+ return true if other.equal?(self)
89
91
 
90
- (other.class.equal?(self.class)) &&
91
- !other.new? && !new? &&
92
- (other.key == key)
93
- end
92
+ (other.class.equal?(self.class)) &&
93
+ !other.new? && !new? &&
94
+ (other.key == key)
95
+ end
94
96
 
95
- def hash
96
- if new?
97
- super # every new document should be treated as a different doc
98
- else
99
- [self.class, key].hash
100
- end
97
+ def hash
98
+ if new?
99
+ super # every new document should be treated as a different doc
100
+ else
101
+ [self.class, key].hash
101
102
  end
102
103
  end
103
104
  end
@@ -17,20 +17,18 @@ module Ripple
17
17
  end
18
18
  end
19
19
 
20
- module InstanceMethods
21
- # Reads the key for this Document.
22
- def key
23
- @key
24
- end
25
-
26
- # Sets the key for this Document.
27
- def key=(value)
28
- @key = value.to_s
29
- end
30
-
31
- def key_attr
32
- :key
33
- end
20
+ # Reads the key for this Document.
21
+ def key
22
+ @key
23
+ end
24
+
25
+ # Sets the key for this Document.
26
+ def key=(value)
27
+ @key = value.to_s
28
+ end
29
+
30
+ def key_attr
31
+ :key
34
32
  end
35
33
  end
36
34
  end
@@ -8,8 +8,8 @@ module Ripple
8
8
  module ClassMethods
9
9
 
10
10
  # Instantiates a new record, applies attributes from a block, and saves it
11
- def create(attrs={}, &block)
12
- new(attrs, &block).tap {|s| s.save }
11
+ def create(*args, &block)
12
+ new(*args, &block).tap {|s| s.save }
13
13
  end
14
14
 
15
15
  # Destroys all records one at a time.
@@ -26,90 +26,105 @@ module Ripple
26
26
  end
27
27
  end
28
28
 
29
- module InstanceMethods
30
- # @private
31
- def initialize
32
- super
33
- @new = true
34
- end
35
-
36
- # Determines whether this is a new document.
37
- def new?
38
- @new || false
39
- end
40
-
41
- # Updates a single attribute and then saves the document
42
- # NOTE: THIS SKIPS VALIDATIONS! Use with caution.
43
- # @return [true,false] whether the document succeeded in saving
44
- def update_attribute(attribute, value)
45
- send("#{attribute}=", value)
46
- save(:validate => false)
47
- end
48
-
49
- # Writes new attributes and then saves the document
50
- # @return [true,false] whether the document succeeded in saving
51
- def update_attributes(attrs)
52
- self.attributes = attrs
53
- save
54
- end
55
-
56
- # Saves the document in Riak.
57
- # @return [true,false] whether the document succeeded in saving
58
- def save(*args)
59
- really_save(*args)
60
- end
61
-
62
- def really_save(*args)
63
- update_robject
64
- robject.store(self.class.quorums.slice(:w,:dw))
65
- self.key = robject.key
66
- @new = false
67
- true
68
- end
69
-
70
- # Reloads the document from Riak
71
- # @return self
72
- def reload
73
- return self if new?
74
- @robject = @robject.reload(:force => true)
75
- self.__send__(:raw_attributes=, @robject.data.except("_type"))
76
- reset_associations
77
- self
78
- end
79
-
80
- # Deletes the document from Riak and freezes this instance
81
- def destroy
82
- robject.delete(self.class.quorums.slice(:rw)) unless new?
83
- freeze
84
- true
85
- rescue Riak::FailedRequest
86
- false
87
- end
88
-
89
- # Freezes the document, preventing further modification.
90
- def freeze
91
- @attributes.freeze; super
92
- end
93
-
94
- attr_writer :robject
95
-
96
- def robject
97
- @robject ||= Riak::RObject.new(self.class.bucket, key).tap do |obj|
98
- obj.content_type = "application/json"
99
- end
100
- end
101
-
102
- def update_robject
103
- robject.key = key if robject.key != key
104
- robject.content_type = 'application/json'
105
- robject.data = attributes_for_persistence
106
- end
107
-
108
- private
109
- def attributes_for_persistence
110
- raw_attributes.merge("_type" => self.class.name)
29
+ # @private
30
+ def initialize
31
+ super
32
+ @new = true
33
+ @deleted = false
34
+ end
35
+
36
+ # Determines whether this document has been deleted or not.
37
+ def deleted?
38
+ @deleted
39
+ end
40
+
41
+ # Determines whether this is a new document.
42
+ def new?
43
+ @new || false
44
+ end
45
+
46
+ # Updates a single attribute and then saves the document
47
+ # NOTE: THIS SKIPS VALIDATIONS! Use with caution.
48
+ # @return [true,false] whether the document succeeded in saving
49
+ def update_attribute(attribute, value)
50
+ send("#{attribute}=", value)
51
+ save(:validate => false)
52
+ end
53
+
54
+ # Writes new attributes and then saves the document
55
+ # @return [true,false] whether the document succeeded in saving
56
+ def update_attributes(attrs)
57
+ self.attributes = attrs
58
+ save
59
+ end
60
+
61
+ # Saves the document in Riak.
62
+ # @return [true,false] whether the document succeeded in saving
63
+ def save(*args)
64
+ really_save(*args)
65
+ end
66
+
67
+ def really_save(*args)
68
+ update_robject
69
+ robject.store(self.class.quorums.slice(:w,:dw))
70
+ self.key = robject.key
71
+ @new = false
72
+ true
73
+ end
74
+
75
+ # Reloads the document from Riak
76
+ # @return self
77
+ def reload
78
+ return self if new?
79
+ @robject = @robject.reload(:force => true)
80
+ self.__send__(:raw_attributes=, @robject.data.except("_type"))
81
+ reset_associations
82
+ self
83
+ end
84
+
85
+ # Deletes the document from Riak and freezes this instance
86
+ def destroy!
87
+ robject.delete(self.class.quorums.slice(:rw)) unless new?
88
+ @deleted = true
89
+ freeze
90
+ end
91
+
92
+ def destroy
93
+ destroy!
94
+ true
95
+ rescue Riak::FailedRequest
96
+ false
97
+ end
98
+
99
+ # Freeze the attributes hash instead of the record itself to avoid
100
+ # errors when calling methods on frozen records.
101
+ def freeze
102
+ @attributes.freeze
103
+ end
104
+
105
+ # Returns +true+ if the attributes hash has been frozen.
106
+ def frozen?
107
+ @attributes.frozen?
108
+ end
109
+
110
+ attr_writer :robject
111
+
112
+ def robject
113
+ @robject ||= Riak::RObject.new(self.class.bucket, key).tap do |obj|
114
+ obj.content_type = "application/json"
111
115
  end
112
116
  end
117
+
118
+ def update_robject
119
+ robject.key = key if robject.key != key
120
+ robject.content_type = 'application/json'
121
+ robject.data = attributes_for_persistence
122
+ end
123
+
124
+ private
125
+ def attributes_for_persistence
126
+ raw_attributes.merge("_type" => self.class.name)
127
+ end
113
128
  end
114
129
  end
115
130
  end