better-ripple 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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'