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