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.
Files changed (77) hide show
  1. data/LICENSE +17 -0
  2. data/README.md +182 -0
  3. data/RELEASE_NOTES.md +284 -0
  4. data/better-ripple.gemspec +55 -0
  5. data/lib/rails/generators/ripple/configuration/configuration_generator.rb +13 -0
  6. data/lib/rails/generators/ripple/configuration/templates/ripple.yml +25 -0
  7. data/lib/rails/generators/ripple/js/js_generator.rb +13 -0
  8. data/lib/rails/generators/ripple/js/templates/js/contrib.js +63 -0
  9. data/lib/rails/generators/ripple/js/templates/js/iso8601.js +76 -0
  10. data/lib/rails/generators/ripple/js/templates/js/ripple.js +132 -0
  11. data/lib/rails/generators/ripple/model/model_generator.rb +20 -0
  12. data/lib/rails/generators/ripple/model/templates/model.rb.erb +10 -0
  13. data/lib/rails/generators/ripple/observer/observer_generator.rb +16 -0
  14. data/lib/rails/generators/ripple/observer/templates/observer.rb.erb +2 -0
  15. data/lib/rails/generators/ripple/test/templates/cucumber.rb.erb +7 -0
  16. data/lib/rails/generators/ripple/test/test_generator.rb +44 -0
  17. data/lib/rails/generators/ripple_generator.rb +79 -0
  18. data/lib/ripple.rb +86 -0
  19. data/lib/ripple/associations.rb +380 -0
  20. data/lib/ripple/associations/embedded.rb +35 -0
  21. data/lib/ripple/associations/instantiators.rb +26 -0
  22. data/lib/ripple/associations/linked.rb +65 -0
  23. data/lib/ripple/associations/many.rb +38 -0
  24. data/lib/ripple/associations/many_embedded_proxy.rb +39 -0
  25. data/lib/ripple/associations/many_linked_proxy.rb +66 -0
  26. data/lib/ripple/associations/many_reference_proxy.rb +95 -0
  27. data/lib/ripple/associations/many_stored_key_proxy.rb +76 -0
  28. data/lib/ripple/associations/one.rb +20 -0
  29. data/lib/ripple/associations/one_embedded_proxy.rb +35 -0
  30. data/lib/ripple/associations/one_key_proxy.rb +58 -0
  31. data/lib/ripple/associations/one_linked_proxy.rb +26 -0
  32. data/lib/ripple/associations/one_stored_key_proxy.rb +43 -0
  33. data/lib/ripple/associations/proxy.rb +118 -0
  34. data/lib/ripple/attribute_methods.rb +132 -0
  35. data/lib/ripple/attribute_methods/dirty.rb +59 -0
  36. data/lib/ripple/attribute_methods/query.rb +34 -0
  37. data/lib/ripple/attribute_methods/read.rb +28 -0
  38. data/lib/ripple/attribute_methods/write.rb +25 -0
  39. data/lib/ripple/callbacks.rb +71 -0
  40. data/lib/ripple/conflict/basic_resolver.rb +86 -0
  41. data/lib/ripple/conflict/document_hooks.rb +46 -0
  42. data/lib/ripple/conflict/resolver.rb +79 -0
  43. data/lib/ripple/conflict/test_helper.rb +34 -0
  44. data/lib/ripple/conversion.rb +29 -0
  45. data/lib/ripple/core_ext.rb +3 -0
  46. data/lib/ripple/core_ext/casting.rb +151 -0
  47. data/lib/ripple/core_ext/indexes.rb +89 -0
  48. data/lib/ripple/core_ext/object.rb +8 -0
  49. data/lib/ripple/document.rb +105 -0
  50. data/lib/ripple/document/bucket_access.rb +25 -0
  51. data/lib/ripple/document/finders.rb +131 -0
  52. data/lib/ripple/document/key.rb +35 -0
  53. data/lib/ripple/document/link.rb +30 -0
  54. data/lib/ripple/document/persistence.rb +130 -0
  55. data/lib/ripple/embedded_document.rb +63 -0
  56. data/lib/ripple/embedded_document/around_callbacks.rb +18 -0
  57. data/lib/ripple/embedded_document/finders.rb +26 -0
  58. data/lib/ripple/embedded_document/persistence.rb +75 -0
  59. data/lib/ripple/i18n.rb +5 -0
  60. data/lib/ripple/indexes.rb +151 -0
  61. data/lib/ripple/inspection.rb +32 -0
  62. data/lib/ripple/locale/en.yml +26 -0
  63. data/lib/ripple/locale/fr.yml +24 -0
  64. data/lib/ripple/nested_attributes.rb +275 -0
  65. data/lib/ripple/observable.rb +28 -0
  66. data/lib/ripple/properties.rb +74 -0
  67. data/lib/ripple/property_type_mismatch.rb +12 -0
  68. data/lib/ripple/railtie.rb +26 -0
  69. data/lib/ripple/railties/ripple.rake +103 -0
  70. data/lib/ripple/serialization.rb +82 -0
  71. data/lib/ripple/test_server.rb +35 -0
  72. data/lib/ripple/timestamps.rb +25 -0
  73. data/lib/ripple/translation.rb +18 -0
  74. data/lib/ripple/validations.rb +65 -0
  75. data/lib/ripple/validations/associated_validator.rb +43 -0
  76. data/lib/ripple/version.rb +3 -0
  77. metadata +310 -0
@@ -0,0 +1,34 @@
1
+ require 'active_support/concern'
2
+
3
+ module Ripple
4
+ module AttributeMethods
5
+ module Query
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attribute_method_suffix "?"
10
+ end
11
+
12
+ private
13
+ # Based on code from ActiveRecord
14
+ def attribute?(attr_name)
15
+ unless value = attribute(attr_name)
16
+ false
17
+ else
18
+ prop = self.class.properties[attr_name]
19
+ if prop.nil?
20
+ if Numeric === value || value !~ /[^0-9]/
21
+ !value.to_i.zero?
22
+ else
23
+ Boolean.ripple_cast(value) || value.present?
24
+ end
25
+ elsif prop.type <= Numeric
26
+ !value.zero?
27
+ else
28
+ value.present?
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ require 'active_support/concern'
2
+
3
+ module Ripple
4
+ module AttributeMethods
5
+ module Read
6
+ extend ActiveSupport::Concern
7
+
8
+ if ActiveSupport::VERSION::STRING < '3.2'
9
+ included do
10
+ attribute_method_suffix ''
11
+ end
12
+ end
13
+
14
+ def [](attr_name)
15
+ attribute(attr_name)
16
+ end
17
+
18
+ private
19
+ def attribute(attr_name)
20
+ if @attributes.include?(attr_name)
21
+ @attributes[attr_name]
22
+ else
23
+ nil
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_support/concern'
2
+
3
+ module Ripple
4
+ module AttributeMethods
5
+ module Write
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attribute_method_suffix "="
10
+ end
11
+
12
+ def []=(attr_name, value)
13
+ __send__(:attribute=, attr_name, value)
14
+ end
15
+
16
+ private
17
+ def attribute=(attr_name, value)
18
+ if prop = self.class.properties[attr_name]
19
+ value = prop.type_cast(value)
20
+ end
21
+ @attributes[attr_name] = value
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,71 @@
1
+ require 'active_support/concern'
2
+ require 'active_model/callbacks'
3
+
4
+ module Ripple
5
+ # Adds lifecycle callbacks to {Ripple::Document} models, in the typical
6
+ # ActiveModel fashion.
7
+ module Callbacks
8
+ extend ActiveSupport::Concern
9
+
10
+ CALLBACK_TYPES = [:create, :update, :save, :destroy, :validation]
11
+
12
+ included do
13
+ extend ActiveModel::Callbacks
14
+ define_model_callbacks *(CALLBACK_TYPES - [:validation])
15
+ define_callbacks :validation, :terminator => "result == false", :scope => [:kind, :name]
16
+ end
17
+
18
+ module ClassMethods
19
+ # Defines a callback to be run before validations.
20
+ def before_validation(*args, &block)
21
+ options = args.last
22
+ if options.is_a?(Hash) && options[:on]
23
+ options[:if] = Array(options[:if])
24
+ options[:if] << "@_on_validate == :#{options[:on]}"
25
+ end
26
+ set_callback(:validation, :before, *args, &block)
27
+ end
28
+
29
+ # Defines a callback to be run after validations.
30
+ def after_validation(*args, &block)
31
+ options = args.extract_options!
32
+ options[:prepend] = true
33
+ options[:if] = Array(options[:if])
34
+ options[:if] << "!halted && value != false"
35
+ options[:if] << "@_on_validate == :#{options[:on]}" if options[:on]
36
+ set_callback(:validation, :after, *(args << options), &block)
37
+ end
38
+ end
39
+
40
+ # @private
41
+ def really_save(*args, &block)
42
+ run_save_callbacks do
43
+ super
44
+ 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
52
+ end
53
+ end
54
+ end
55
+
56
+ # @private
57
+ def destroy!(*args, &block)
58
+ run_callbacks(:destroy) do
59
+ super
60
+ end
61
+ end
62
+
63
+ # @private
64
+ def valid?(*args, &block)
65
+ @_on_validate = new? ? :create : :update
66
+ run_callbacks(:validation) do
67
+ super
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,86 @@
1
+ module Ripple
2
+ module Conflict
3
+ class BasicResolver
4
+ delegate :model_class, :document, :siblings, :to => :@main_resolver
5
+
6
+ def initialize(main_resolver)
7
+ @main_resolver = main_resolver
8
+ end
9
+
10
+ def remaining_conflicts
11
+ @remaining_conflicts ||= []
12
+ end
13
+
14
+ def unexpected_conflicts
15
+ # if the user didn't specify the conflict they expect,
16
+ # then don't consider any conflicts unexpected
17
+ return [] if model_class.expected_conflicts.blank?
18
+
19
+ remaining_conflicts - model_class.expected_conflicts
20
+ end
21
+
22
+ def perform
23
+ process_properties
24
+ process_embedded_associations
25
+ process_linked_associations
26
+ process_stored_key_associations
27
+ end
28
+
29
+ private
30
+
31
+ def undeleted_siblings
32
+ @undeleted_siblings ||= siblings.reject(&:deleted?)
33
+ end
34
+
35
+ def process_properties
36
+ model_class.properties.each do |name, property|
37
+ document.send(:"#{name}=", resolved_property_value_for(property))
38
+ end
39
+ end
40
+
41
+ def process_embedded_associations
42
+ model_class.embedded_associations.each do |assoc|
43
+ document.send(:"#{assoc.name}=", resolved_association_value_for(assoc, :load_target))
44
+ end
45
+ end
46
+
47
+ def process_linked_associations
48
+ model_class.linked_associations.each do |assoc|
49
+ document.send(assoc.name).replace_links(resolved_association_value_for(assoc, :links))
50
+ end
51
+ end
52
+
53
+ def process_stored_key_associations
54
+ model_class.stored_key_associations.each do |assoc|
55
+ document.send(assoc.name).reset_owner_keys if (assoc.type == :many)
56
+ end
57
+ end
58
+
59
+ def resolved_property_value_for(property)
60
+ uniq_values = undeleted_siblings.map(&property.key).uniq
61
+
62
+ value = if uniq_values.size == 1
63
+ uniq_values.first
64
+ elsif property.key == :updated_at
65
+ uniq_values.compact.max
66
+ elsif property.key == :created_at
67
+ uniq_values.compact.min
68
+ else
69
+ remaining_conflicts << property.key
70
+ property.default
71
+ end
72
+ end
73
+
74
+ def resolved_association_value_for(association, proxy_value_method)
75
+ # the association proxy doesn't uniquify well, so we have to use the target or links directly
76
+ uniq_values = undeleted_siblings.map { |s| s.send(association.name).__send__(proxy_value_method) }.uniq
77
+
78
+ return uniq_values.first if uniq_values.size == 1
79
+ remaining_conflicts << association.name
80
+
81
+ association.many? ? [] : nil # default value
82
+ end
83
+ end
84
+ end
85
+ end
86
+
@@ -0,0 +1,46 @@
1
+ module Ripple
2
+ module Conflict
3
+ module DocumentHooks
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ # @return [Proc] the registered conflict handler
8
+ attr_reader :on_conflict_block
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.
33
+ def on_conflict(*expected_conflicts, &block)
34
+ @expected_conflicts = expected_conflicts
35
+ @on_conflict_block = block
36
+ end
37
+
38
+ # @return [Array<Symbol>] list of properties and associations that are expected
39
+ # to be in conflict.
40
+ def expected_conflicts
41
+ @expected_conflicts ||= []
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,79 @@
1
+ require 'ripple/conflict/basic_resolver'
2
+
3
+ module Ripple
4
+ module Conflict
5
+ class Resolver
6
+ include Translation
7
+
8
+ attr_reader :document, :model_class
9
+
10
+ delegate :expected_conflicts, :on_conflict_block, :to => :model_class
11
+
12
+ def self.to_proc
13
+ @to_proc ||= lambda do |robject|
14
+ possible_model_classes = robject.siblings.map { |s| s.data && s.data['_type'] }.compact.uniq
15
+ return nil unless possible_model_classes.size == 1
16
+
17
+ resolver = new(robject, possible_model_classes.first.constantize)
18
+ resolver.resolve
19
+ resolver.document.robject
20
+ end
21
+ end
22
+
23
+ Riak::RObject.on_conflict(&self)
24
+
25
+ def initialize(robject, model_class)
26
+ @robject = robject
27
+ @model_class = model_class
28
+ end
29
+
30
+ def resolve
31
+ assert_conflict_block
32
+ basic_resolver.perform
33
+ assert_no_unexpected_conflicts
34
+ document.instance_exec(siblings, basic_resolver.remaining_conflicts, &on_conflict_block)
35
+ document.update_robject
36
+ end
37
+
38
+ def siblings
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
48
+ end
49
+
50
+ def document
51
+ # pick a sibling robject to use as the basis of the document to resolve
52
+ # which one doesn't really matter.
53
+ @document ||= @model_class.send(:instantiate, @robject.siblings.first.dup)
54
+ end
55
+
56
+ private
57
+
58
+ def basic_resolver
59
+ @basic_resolver ||= BasicResolver.new(self)
60
+ end
61
+
62
+ def assert_conflict_block
63
+ return if on_conflict_block
64
+
65
+ raise NotImplementedError, t('conflict_handler_not_implemented', :document => document)
66
+ end
67
+
68
+ def assert_no_unexpected_conflicts
69
+ return unless basic_resolver.unexpected_conflicts.any?
70
+
71
+ raise NotImplementedError, t('unexpected_conflicts',
72
+ :conflicts => basic_resolver.unexpected_conflicts.inspect,
73
+ :document => document.inspect
74
+ )
75
+ end
76
+ end
77
+ end
78
+ end
79
+
@@ -0,0 +1,34 @@
1
+ module Ripple
2
+ module Conflict
3
+ module TestHelper
4
+ def create_conflict(main_record, *modifiers)
5
+ # We have to disable all on conflict resolvers while we create conflict
6
+ # so that they don't auto-resolve it.
7
+ orig_hooks = Riak::RObject.on_conflict_hooks.dup
8
+ Riak::RObject.on_conflict_hooks.clear
9
+
10
+ begin
11
+ klass, key = main_record.class, main_record.key
12
+ raise "#{klass.bucket.name} allow_mult property is false!" unless klass.bucket.allow_mult
13
+ records = modifiers.map { |_| klass.find!(key) }
14
+
15
+ records.zip(modifiers).each do |(record, modifier)|
16
+ # necessary to get conflict on 0.14 and earlier, so riak thinks they are being saved by different clients
17
+ Ripple.client.client_id += 1
18
+
19
+ modifier.call(record)
20
+ record.save! unless record.deleted?
21
+ end
22
+
23
+ robject = klass.bucket.get(key)
24
+ raise "#{robject} is not in conflict as expected." unless robject.conflict?
25
+ ensure
26
+ orig_hooks.each do |hook|
27
+ Riak::RObject.on_conflict(&hook)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,29 @@
1
+ require 'active_model/conversion'
2
+
3
+ module Ripple
4
+ # Provides ActionPack compatibility for {Ripple::Document} models.
5
+ module Conversion
6
+ extend ActiveSupport::Concern
7
+ include ActiveModel::Conversion
8
+
9
+ # True if this is a new document
10
+ def new_record?
11
+ new?
12
+ end
13
+
14
+ # True if this is not a new document
15
+ def persisted?
16
+ !new?
17
+ end
18
+
19
+ # Converts to a view key
20
+ def to_key
21
+ new? ? nil : [key]
22
+ end
23
+
24
+ # Converts to a URL parameter
25
+ def to_param
26
+ key
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ require 'ripple/core_ext/casting'
2
+ require 'ripple/core_ext/object'
3
+ require 'ripple/core_ext/indexes'