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