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.
- 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
|