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,35 @@
1
+ require 'active_support/concern'
2
+
3
+ module Ripple
4
+ module Document
5
+ module Key
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ # Defines the key to be derived from a property.
10
+ # @param [String,Symbol] prop the property to derive the key from
11
+ def key_on(prop)
12
+ prop = prop.to_sym
13
+
14
+ define_method(:key) { send(prop).to_s }
15
+ define_method(:key=) { |v| send(:"#{prop}=", v) }
16
+ define_method(:key_attr) { prop }
17
+ end
18
+ end
19
+
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
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ require 'riak/link'
2
+
3
+ module Ripple
4
+ module Document
5
+ # A Link that is tied to a particular document and tag.
6
+ # The key is fetched from the document lazily when needed.
7
+ class Link < Riak::Link
8
+ attr_reader :document
9
+ private :document
10
+
11
+ def initialize(document, tag)
12
+ @document = document
13
+ super(document.class.bucket_name, nil, tag)
14
+ end
15
+
16
+ def key
17
+ document.key
18
+ end
19
+
20
+ def hash
21
+ document.hash
22
+ end
23
+ end
24
+
25
+ def to_link(tag)
26
+ Link.new(self, tag)
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,130 @@
1
+ require 'active_support/concern'
2
+
3
+ module Ripple
4
+ module Document
5
+ module Persistence
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+
10
+ # Instantiates a new record, applies attributes from a block, and saves it
11
+ def create(*args, &block)
12
+ new(*args, &block).tap {|s| s.save }
13
+ end
14
+
15
+ # Destroys all records one at a time.
16
+ # Place holder while :delete to bucket is being developed.
17
+ def destroy_all
18
+ list(&:destroy)
19
+ end
20
+
21
+ attr_writer :quorums
22
+ alias_method "set_quorums", "quorums="
23
+
24
+ def quorums
25
+ @quorums ||= {}
26
+ end
27
+ end
28
+
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"
115
+ end
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
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,63 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/hash/except'
3
+ require 'ripple/translation'
4
+ require 'ripple/embedded_document/around_callbacks'
5
+ require 'ripple/embedded_document/finders'
6
+ require 'ripple/embedded_document/persistence'
7
+ require 'ripple/properties'
8
+ require 'ripple/attribute_methods'
9
+ require 'ripple/timestamps'
10
+ require 'ripple/validations'
11
+ require 'ripple/associations'
12
+ require 'ripple/callbacks'
13
+ require 'ripple/conversion'
14
+ require 'ripple/inspection'
15
+ require 'ripple/nested_attributes'
16
+ require 'ripple/serialization'
17
+
18
+ module Ripple
19
+ # Represents a document model that is composed into or stored in a parent
20
+ # Document. Embedded documents may also embed other documents, have
21
+ # callbacks and validations, but are solely dependent on the parent Document.
22
+ module EmbeddedDocument
23
+ extend ActiveSupport::Concern
24
+ include Translation
25
+
26
+ included do
27
+ extend ActiveModel::Naming
28
+ include Persistence
29
+ extend Ripple::Properties
30
+ include Ripple::AttributeMethods
31
+ include Ripple::Indexes
32
+ include Ripple::Timestamps
33
+ include Ripple::Validations
34
+ include Ripple::Associations
35
+ include Ripple::Callbacks
36
+ include Ripple::EmbeddedDocument::AroundCallbacks
37
+ include Ripple::Conversion
38
+ include Finders
39
+ include Ripple::Inspection
40
+ include Ripple::NestedAttributes
41
+ include Ripple::Serialization
42
+ end
43
+
44
+ module ClassMethods
45
+ def embeddable?
46
+ true
47
+ end
48
+ end
49
+
50
+ def ==(other)
51
+ self.class == other.class &&
52
+ _parent_document == other._parent_document &&
53
+ serializable_hash == other.serializable_hash
54
+ end
55
+ alias eql? ==
56
+
57
+ def hash
58
+ hash = self.class.hash ^ _parent_document.class.hash ^ serializable_hash.to_s.hash
59
+ hash ^= _parent_document.key.hash if _parent_document.respond_to?(:key)
60
+ hash
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,18 @@
1
+ require 'ripple/callbacks'
2
+
3
+ module Ripple
4
+ module EmbeddedDocument
5
+ module AroundCallbacks
6
+ extend ActiveSupport::Concern
7
+ extend Translation
8
+
9
+ included do
10
+ Ripple::Callbacks::CALLBACK_TYPES.each do |type|
11
+ define_singleton_method "around_#{type}" do |*args|
12
+ raise NotImplementedError.new(t("around_callbacks_not_supported", :type => type))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/inflector'
3
+
4
+ module Ripple
5
+ module EmbeddedDocument
6
+ # @private
7
+ module Finders
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+ def instantiate(attrs)
12
+ begin
13
+ klass = attrs['_type'].present? ? attrs.delete('_type').constantize : self
14
+ rescue NameError
15
+ klass = self
16
+ end
17
+ klass.new.tap do |object|
18
+ object.raw_attributes = attrs
19
+ object.changed_attributes.clear
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,75 @@
1
+ require 'active_support/concern'
2
+ require 'ripple/translation'
3
+
4
+ module Ripple
5
+ # Exception raised when save is called on an EmbeddedDocument that
6
+ # is not attached to a root Document.
7
+ class NoRootDocument < StandardError
8
+ include Translation
9
+ def initialize(doc, method)
10
+ super(t("no_root_document", :doc => doc.inspect, :method => method))
11
+ end
12
+ end
13
+
14
+ module EmbeddedDocument
15
+ # Adds methods to {Ripple::EmbeddedDocument} that delegate storage
16
+ # operations to the parent document.
17
+ module Persistence
18
+ extend ActiveSupport::Concern
19
+
20
+ module ClassMethods
21
+ # Creates a method that points to the parent document.
22
+ def embedded_in(parent)
23
+ define_method(parent) { @_parent_document }
24
+ end
25
+ end
26
+
27
+ # The parent document to this embedded document. This may be a
28
+ # {Ripple::Document} or another {Ripple::EmbeddedDocument}.
29
+ attr_accessor :_parent_document
30
+
31
+ # Whether the root document is unsaved.
32
+ def new?
33
+ if _root_document
34
+ _root_document.new?
35
+ else
36
+ true
37
+ end
38
+ end
39
+
40
+ # Sets this embedded documents attributes and saves the root document.
41
+ def update_attributes(attrs)
42
+ self.attributes = attrs
43
+ save
44
+ end
45
+
46
+ # Updates this embedded document's attribute and saves the
47
+ # root document, skipping validations.
48
+ def update_attribute(attribute, value)
49
+ send("#{attribute}=", value)
50
+ save(:validate => false)
51
+ end
52
+
53
+ # Saves this embedded document by delegating to the root document.
54
+ def save(*args)
55
+ if _root_document
56
+ run_save_callbacks do
57
+ _root_document.save(*args)
58
+ end
59
+ else
60
+ raise NoRootDocument.new(self, :save)
61
+ end
62
+ end
63
+
64
+ # @private
65
+ def attributes_for_persistence
66
+ raw_attributes.merge("_type" => self.class.name)
67
+ end
68
+
69
+ # The root {Ripple::Document} to which this embedded document belongs.
70
+ def _root_document
71
+ @_parent_document.try(:_root_document)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,5 @@
1
+ require 'active_support/i18n'
2
+
3
+ Dir.glob(File.expand_path("../locale/*.yml", __FILE__)).each do |locale_file|
4
+ I18n.load_path << locale_file
5
+ end
@@ -0,0 +1,151 @@
1
+ require 'ripple/translation'
2
+ require 'active_support/concern'
3
+
4
+ module Ripple
5
+ # Adds secondary-indexes to {Document} properties.
6
+ module Indexes
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+
11
+ def inherited(subclass)
12
+ super
13
+ subclass.indexes = indexes.dup
14
+ end
15
+
16
+ # Indexes defined on the document.
17
+ def indexes
18
+ @indexes ||= {}.with_indifferent_access
19
+ end
20
+
21
+ def indexes=(idx)
22
+ @indexes = idx
23
+ end
24
+
25
+ def property(key, type, options={})
26
+ if indexed = options.delete(:index)
27
+ indexes[key] = Index.new(key, type, indexed)
28
+ end
29
+ super
30
+ end
31
+
32
+ def index(key, type, &block)
33
+ if block_given?
34
+ indexes[key] = Index.new(key, type, &block)
35
+ else
36
+ indexes[key] = Index.new(key, type)
37
+ end
38
+ end
39
+ end
40
+
41
+ # Returns indexes in a form suitable for persisting to Riak.
42
+ # @return [Hash] indexes for this document
43
+ def indexes_for_persistence(prefix = '')
44
+ Hash.new {|h,k| h[k] = Set.new }.tap do |indexes|
45
+ # Add embedded associations' indexes
46
+ self.class.embedded_associations.each do |association|
47
+ documents = instance_variable_get(association.ivar)
48
+ unless documents.nil?
49
+ Array(documents).each do |doc|
50
+ embedded_indexes = doc.indexes_for_persistence("#{prefix}#{association.name}_")
51
+ indexes.merge!(embedded_indexes) do |_,original,new|
52
+ original.merge new
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # Add this document's indexes
59
+ self.class.indexes.each do |key, index|
60
+ if index.block
61
+ index_value = index.to_index_value instance_exec(&index.block)
62
+ else
63
+ index_value = index.to_index_value send(key)
64
+ end
65
+ index_value = Set[index_value] unless index_value.is_a?(Enumerable) && !index_value.is_a?(String)
66
+ indexes[prefix + index.index_key].merge index_value
67
+ end
68
+ end
69
+ end
70
+
71
+ # Modifies the persistence chain to set indexes on the internal
72
+ # {Riak::RObject} before saving.
73
+ module DocumentMethods
74
+ extend ActiveSupport::Concern
75
+ def update_robject
76
+ robject.indexes = indexes_for_persistence
77
+ super
78
+ end
79
+
80
+ module ClassMethods
81
+ # Search for a document using an indexed column
82
+ # @param [Symbol] name of the index
83
+ # @param [String, Integer, Range] query to search for
84
+ def find_by_index(index_name, query)
85
+ if ["$bucket", "$key"].include?(index_name.to_s)
86
+ self.find(Ripple.client.get_index(self.bucket.name, index_name.to_s, query))
87
+ else
88
+ idx = self.indexes[index_name]
89
+ raise ArgumentError, t('index_undefined', :property => index_name, :type => self.name) if idx.nil?
90
+ self.find(Ripple.client.get_index(self.bucket.name, idx.index_key, query))
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ # Represents a Secondary Index on a Document
98
+ class Index
99
+ include Translation
100
+ attr_reader :key, :type, :block
101
+
102
+ # Creates an index for a Document
103
+ # @param [Symbol] key the attribute key
104
+ # @param [Class] property_type the type of the associated property
105
+ # @param ['bin', 'int', String, Integer] index_type if given, the
106
+ # type of index
107
+ # @yield a block that returns the value of the index
108
+ def initialize(key, property_type, index_type=true, &block)
109
+ @key, @type, @index, @block = key, property_type, index_type, block
110
+ end
111
+
112
+
113
+ # The key under which a value will be indexed
114
+ def index_key
115
+ "#{key}_#{index_type}"
116
+ end
117
+
118
+ # Converts an attribute to a value appropriate for storing in a
119
+ # secondary index.
120
+ # @param [Object] value a value of type {#type}
121
+ # @return [String, Integer, Set] a value appropriate for storing
122
+ # in a secondary index
123
+ def to_index_value(value)
124
+ value.to_ripple_index(index_type)
125
+ end
126
+
127
+ # @return ["bin", "int", nil] the type of index used for this property
128
+ # @raise [ArgumentError] if the type cannot be automatically determined
129
+ def index_type
130
+ @index_type ||= case @index
131
+ when /^bin|int$/
132
+ @index
133
+ when Class
134
+ determine_index_type(@index)
135
+ else
136
+ determine_index_type(@type)
137
+ end
138
+ end
139
+
140
+ private
141
+ def determine_index_type(itype)
142
+ if String == itype || itype < String
143
+ 'bin'
144
+ elsif [Integer, Time, Date, ActiveSupport::TimeWithZone].any? {|t| t == itype || itype < t }
145
+ 'int'
146
+ else
147
+ raise ArgumentError, t('index_type_unknown', :property => @key, :type => itype.name)
148
+ end
149
+ end
150
+ end
151
+ end