seomoz-ripple 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (149) hide show
  1. data/Gemfile +20 -0
  2. data/Guardfile +15 -0
  3. data/Rakefile +88 -0
  4. data/lib/rails/generators/ripple/configuration/configuration_generator.rb +13 -0
  5. data/lib/rails/generators/ripple/configuration/templates/ripple.yml +24 -0
  6. data/lib/rails/generators/ripple/js/js_generator.rb +13 -0
  7. data/lib/rails/generators/ripple/js/templates/js/contrib.js +63 -0
  8. data/lib/rails/generators/ripple/js/templates/js/iso8601.js +76 -0
  9. data/lib/rails/generators/ripple/js/templates/js/ripple.js +132 -0
  10. data/lib/rails/generators/ripple/model/model_generator.rb +20 -0
  11. data/lib/rails/generators/ripple/model/templates/model.rb +10 -0
  12. data/lib/rails/generators/ripple/observer/observer_generator.rb +16 -0
  13. data/lib/rails/generators/ripple/observer/templates/observer.rb +4 -0
  14. data/lib/rails/generators/ripple/test/templates/test_server.rb +46 -0
  15. data/lib/rails/generators/ripple/test/test_generator.rb +39 -0
  16. data/lib/rails/generators/ripple_generator.rb +78 -0
  17. data/lib/ripple.rb +79 -0
  18. data/lib/ripple/associations.rb +356 -0
  19. data/lib/ripple/associations/embedded.rb +35 -0
  20. data/lib/ripple/associations/instantiators.rb +26 -0
  21. data/lib/ripple/associations/linked.rb +65 -0
  22. data/lib/ripple/associations/many.rb +38 -0
  23. data/lib/ripple/associations/many_embedded_proxy.rb +38 -0
  24. data/lib/ripple/associations/many_linked_proxy.rb +66 -0
  25. data/lib/ripple/associations/many_reference_proxy.rb +93 -0
  26. data/lib/ripple/associations/many_stored_key_proxy.rb +76 -0
  27. data/lib/ripple/associations/one.rb +20 -0
  28. data/lib/ripple/associations/one_embedded_proxy.rb +35 -0
  29. data/lib/ripple/associations/one_key_proxy.rb +58 -0
  30. data/lib/ripple/associations/one_linked_proxy.rb +22 -0
  31. data/lib/ripple/associations/one_stored_key_proxy.rb +43 -0
  32. data/lib/ripple/associations/proxy.rb +118 -0
  33. data/lib/ripple/attribute_methods.rb +118 -0
  34. data/lib/ripple/attribute_methods/dirty.rb +50 -0
  35. data/lib/ripple/attribute_methods/query.rb +34 -0
  36. data/lib/ripple/attribute_methods/read.rb +26 -0
  37. data/lib/ripple/attribute_methods/write.rb +25 -0
  38. data/lib/ripple/callbacks.rb +74 -0
  39. data/lib/ripple/conflict/basic_resolver.rb +82 -0
  40. data/lib/ripple/conflict/document_hooks.rb +20 -0
  41. data/lib/ripple/conflict/resolver.rb +71 -0
  42. data/lib/ripple/conflict/test_helper.rb +33 -0
  43. data/lib/ripple/conversion.rb +28 -0
  44. data/lib/ripple/core_ext.rb +2 -0
  45. data/lib/ripple/core_ext/casting.rb +148 -0
  46. data/lib/ripple/core_ext/object.rb +8 -0
  47. data/lib/ripple/document.rb +104 -0
  48. data/lib/ripple/document/bucket_access.rb +25 -0
  49. data/lib/ripple/document/finders.rb +131 -0
  50. data/lib/ripple/document/key.rb +43 -0
  51. data/lib/ripple/document/link.rb +30 -0
  52. data/lib/ripple/document/persistence.rb +115 -0
  53. data/lib/ripple/embedded_document.rb +64 -0
  54. data/lib/ripple/embedded_document/around_callbacks.rb +18 -0
  55. data/lib/ripple/embedded_document/finders.rb +26 -0
  56. data/lib/ripple/embedded_document/persistence.rb +77 -0
  57. data/lib/ripple/i18n.rb +2 -0
  58. data/lib/ripple/inspection.rb +32 -0
  59. data/lib/ripple/locale/en.yml +21 -0
  60. data/lib/ripple/nested_attributes.rb +265 -0
  61. data/lib/ripple/observable.rb +28 -0
  62. data/lib/ripple/properties.rb +73 -0
  63. data/lib/ripple/property_type_mismatch.rb +12 -0
  64. data/lib/ripple/railtie.rb +13 -0
  65. data/lib/ripple/serialization.rb +84 -0
  66. data/lib/ripple/timestamps.rb +27 -0
  67. data/lib/ripple/translation.rb +14 -0
  68. data/lib/ripple/validations.rb +67 -0
  69. data/lib/ripple/validations/associated_validator.rb +43 -0
  70. data/ripple.gemspec +46 -0
  71. data/seomoz-ripple.gemspec +46 -0
  72. data/spec/fixtures/config.yml +8 -0
  73. data/spec/integration/ripple/associations_spec.rb +220 -0
  74. data/spec/integration/ripple/conflict_resolution_spec.rb +293 -0
  75. data/spec/integration/ripple/nested_attributes_spec.rb +264 -0
  76. data/spec/integration/ripple/persistence_spec.rb +57 -0
  77. data/spec/integration/ripple/search_associations_spec.rb +42 -0
  78. data/spec/ripple/associations/many_embedded_proxy_spec.rb +122 -0
  79. data/spec/ripple/associations/many_linked_proxy_spec.rb +191 -0
  80. data/spec/ripple/associations/many_reference_proxy_spec.rb +170 -0
  81. data/spec/ripple/associations/many_stored_key_proxy_spec.rb +158 -0
  82. data/spec/ripple/associations/one_embedded_proxy_spec.rb +125 -0
  83. data/spec/ripple/associations/one_key_proxy_spec.rb +82 -0
  84. data/spec/ripple/associations/one_linked_proxy_spec.rb +91 -0
  85. data/spec/ripple/associations/one_stored_key_proxy_spec.rb +72 -0
  86. data/spec/ripple/associations/proxy_spec.rb +84 -0
  87. data/spec/ripple/associations_spec.rb +129 -0
  88. data/spec/ripple/attribute_methods/dirty_spec.rb +80 -0
  89. data/spec/ripple/attribute_methods_spec.rb +230 -0
  90. data/spec/ripple/bucket_access_spec.rb +25 -0
  91. data/spec/ripple/callbacks_spec.rb +176 -0
  92. data/spec/ripple/conflict/resolver_spec.rb +42 -0
  93. data/spec/ripple/conversion_spec.rb +22 -0
  94. data/spec/ripple/core_ext_spec.rb +103 -0
  95. data/spec/ripple/document/link_spec.rb +67 -0
  96. data/spec/ripple/document_spec.rb +96 -0
  97. data/spec/ripple/embedded_document/finders_spec.rb +29 -0
  98. data/spec/ripple/embedded_document/persistence_spec.rb +80 -0
  99. data/spec/ripple/embedded_document_spec.rb +84 -0
  100. data/spec/ripple/finders_spec.rb +217 -0
  101. data/spec/ripple/inspection_spec.rb +51 -0
  102. data/spec/ripple/key_spec.rb +30 -0
  103. data/spec/ripple/observable_spec.rb +121 -0
  104. data/spec/ripple/persistence_spec.rb +326 -0
  105. data/spec/ripple/properties_spec.rb +262 -0
  106. data/spec/ripple/ripple_spec.rb +71 -0
  107. data/spec/ripple/serialization_spec.rb +51 -0
  108. data/spec/ripple/timestamps_spec.rb +76 -0
  109. data/spec/ripple/validations/associated_validator_spec.rb +77 -0
  110. data/spec/ripple/validations_spec.rb +104 -0
  111. data/spec/spec_helper.rb +26 -0
  112. data/spec/support/associations.rb +1 -0
  113. data/spec/support/associations/proxies.rb +17 -0
  114. data/spec/support/integration_setup.rb +11 -0
  115. data/spec/support/mocks.rb +4 -0
  116. data/spec/support/models.rb +4 -0
  117. data/spec/support/models/address.rb +12 -0
  118. data/spec/support/models/box.rb +13 -0
  119. data/spec/support/models/car.rb +16 -0
  120. data/spec/support/models/cardboard_box.rb +3 -0
  121. data/spec/support/models/clock.rb +12 -0
  122. data/spec/support/models/clock_observer.rb +3 -0
  123. data/spec/support/models/company.rb +23 -0
  124. data/spec/support/models/customer.rb +4 -0
  125. data/spec/support/models/driver.rb +6 -0
  126. data/spec/support/models/email.rb +4 -0
  127. data/spec/support/models/engine.rb +5 -0
  128. data/spec/support/models/family.rb +16 -0
  129. data/spec/support/models/favorite.rb +4 -0
  130. data/spec/support/models/invoice.rb +7 -0
  131. data/spec/support/models/late_invoice.rb +3 -0
  132. data/spec/support/models/ninja.rb +9 -0
  133. data/spec/support/models/note.rb +5 -0
  134. data/spec/support/models/page.rb +4 -0
  135. data/spec/support/models/paid_invoice.rb +4 -0
  136. data/spec/support/models/passenger.rb +6 -0
  137. data/spec/support/models/profile.rb +10 -0
  138. data/spec/support/models/seat.rb +5 -0
  139. data/spec/support/models/subscription.rb +27 -0
  140. data/spec/support/models/tasks.rb +14 -0
  141. data/spec/support/models/team.rb +11 -0
  142. data/spec/support/models/transactions.rb +17 -0
  143. data/spec/support/models/tree.rb +4 -0
  144. data/spec/support/models/user.rb +10 -0
  145. data/spec/support/models/wheel.rb +6 -0
  146. data/spec/support/models/widget.rb +22 -0
  147. data/spec/support/test_server.rb +18 -0
  148. data/spec/support/test_server.yml.example +2 -0
  149. metadata +362 -0
@@ -0,0 +1,8 @@
1
+ unless respond_to?(:define_singleton_method)
2
+ class Object
3
+ def define_singleton_method(name, &block)
4
+ singleton_class = class << self; self; end
5
+ singleton_class.send(:define_method, name, &block)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,104 @@
1
+ require 'active_support/concern'
2
+ require 'active_model/naming'
3
+ require 'ripple/conflict/document_hooks'
4
+ require 'ripple/document/bucket_access'
5
+ require 'ripple/document/key'
6
+ require 'ripple/document/persistence'
7
+ require 'ripple/document/finders'
8
+ require 'ripple/document/link'
9
+ require 'ripple/properties'
10
+ require 'ripple/attribute_methods'
11
+ require 'ripple/timestamps'
12
+ require 'ripple/validations'
13
+ require 'ripple/associations'
14
+ require 'ripple/callbacks'
15
+ require 'ripple/observable'
16
+ require 'ripple/conversion'
17
+ require 'ripple/inspection'
18
+ require 'ripple/nested_attributes'
19
+ require 'ripple/serialization'
20
+
21
+ module Ripple
22
+ # Represents a model stored in Riak, serialized in JSON object (document).
23
+ # Ripple::Document models aim to be fully ActiveModel compatible, with a keen
24
+ # eye toward features that developers expect from ActiveRecord, DataMapper and MongoMapper.
25
+ #
26
+ # Example:
27
+ #
28
+ # class Email
29
+ # include Ripple::Document
30
+ # property :from, String, :presence => true
31
+ # property :to, String, :presence => true
32
+ # property :sent, Time, :default => proc { Time.now }
33
+ # property :body, String
34
+ # end
35
+ #
36
+ # email = Email.find("37458abc752f8413e") # GET /riak/emails/37458abc752f8413e
37
+ # email.from = "someone@nowhere.net"
38
+ # email.save # PUT /riak/emails/37458abc752f8413e
39
+ #
40
+ # reply = Email.new
41
+ # reply.from = "justin@bashoooo.com"
42
+ # reply.to = "sean@geeemail.com"
43
+ # reply.body = "Riak is a good fit for scalable Ruby apps."
44
+ # reply.save # POST /riak/emails (Riak-assigned key)
45
+ #
46
+ module Document
47
+ extend ActiveSupport::Concern
48
+
49
+ included do
50
+ extend ActiveModel::Naming
51
+ extend BucketAccess
52
+ include Ripple::Document::Key
53
+ include Ripple::Document::Persistence
54
+ extend Ripple::Properties
55
+ include Ripple::AttributeMethods
56
+ include Ripple::Timestamps
57
+ include Ripple::Validations
58
+ include Ripple::Associations
59
+ include Ripple::Callbacks
60
+ include Ripple::Observable
61
+ include Ripple::Conversion
62
+ include Ripple::Document::Finders
63
+ include Ripple::Inspection
64
+ include Ripple::NestedAttributes
65
+ include Ripple::Serialization
66
+ include Ripple::Conflict::DocumentHooks
67
+ end
68
+
69
+ module ClassMethods
70
+ def embeddable?
71
+ false
72
+ end
73
+ end
74
+
75
+ module InstanceMethods
76
+ def _root_document
77
+ self
78
+ end
79
+
80
+ # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same key.
81
+ def ==(comparison_object)
82
+ comparison_object.equal?(self) ||
83
+ (comparison_object.class < Document && (comparison_object.instance_of?(self.class) || comparison_object.class.bucket.name == self.class.bucket.name) &&
84
+ !new? && comparison_object.key == key && !comparison_object.new?)
85
+ end
86
+
87
+ def eql?(other)
88
+ return true if other.equal?(self)
89
+
90
+ (other.class.equal?(self.class)) &&
91
+ !other.new? && !new? &&
92
+ (other.key == key)
93
+ end
94
+
95
+ def hash
96
+ if new?
97
+ super # every new document should be treated as a different doc
98
+ else
99
+ [self.class, key].hash
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,25 @@
1
+ require 'ripple'
2
+
3
+ module Ripple
4
+ module Document
5
+ # Similar to ActiveRecord's tables or MongoMapper's collections, we
6
+ # provide a sane default bucket in which to store your documents.
7
+ module BucketAccess
8
+ # @return [String] The bucket name assigned to the document class. Subclasses will inherit their bucket name from their parent class unless they redefine it.
9
+ def bucket_name
10
+ superclass.respond_to?(:bucket_name) ? superclass.bucket_name : model_name.plural
11
+ end
12
+
13
+ # @return [Riak::Bucket] The bucket assigned to this class.
14
+ def bucket
15
+ Ripple.client.bucket(bucket_name)
16
+ end
17
+
18
+ # Set the bucket name for this class and its subclasses.
19
+ # @param [String] value the new bucket name
20
+ def bucket_name=(value)
21
+ (class << self; self; end).send(:define_method, :bucket_name){ value }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,131 @@
1
+ require 'ripple/translation'
2
+ require 'active_support/concern'
3
+ require 'active_support/inflector'
4
+ require 'active_support/core_ext/hash/except'
5
+ require 'active_support/core_ext/hash/slice'
6
+ require 'ripple/conflict/resolver'
7
+
8
+ module Ripple
9
+
10
+ # Raised by <tt>find!</tt> when a document cannot be found with the given key.
11
+ # begin
12
+ # Example.find!('badkey')
13
+ # rescue Ripple::DocumentNotFound
14
+ # puts 'No Document here!'
15
+ # end
16
+ class DocumentNotFound < StandardError
17
+ include Translation
18
+ def initialize(keys, found)
19
+ if keys.empty?
20
+ super(t("document_not_found.no_key"))
21
+ elsif keys.size == 1
22
+ super(t("document_not_found.one_key", :key => keys.first))
23
+ else
24
+ missing = keys - found.compact.map(&:key)
25
+ super(t("document_not_found.many_keys", :keys => missing.join(', ')))
26
+ end
27
+ end
28
+ end
29
+
30
+ module Document
31
+ module Finders
32
+ extend ActiveSupport::Concern
33
+
34
+ module ClassMethods
35
+ # Retrieve single or multiple documents from Riak.
36
+ # @overload find(key)
37
+ # Find a single document.
38
+ # @param [String] key the key of a document to find
39
+ # @return [Document] the found document, or nil
40
+ # @overload find(key1, key2, ...)
41
+ # Find a list of documents.
42
+ # @param [String] key1 the key of a document to find
43
+ # @param [String] key2 the key of a document to find
44
+ # @return [Array<Document>] a list of found documents, including nil for missing documents
45
+ # @overload find(keylist)
46
+ # Find a list of documents.
47
+ # @param [Array<String>] keylist an array of keys to find
48
+ # @return [Array<Document>] a list of found documents, including nil for missing documents
49
+ def find(*args)
50
+ if args.first.is_a?(Array)
51
+ args.flatten.map {|key| find_one(key) }
52
+ else
53
+ args.flatten!
54
+ return nil if args.empty? || args.all?(&:blank?)
55
+ return find_one(args.first) if args.size == 1
56
+ args.map {|key| find_one(key) }
57
+ end
58
+ end
59
+
60
+ # Retrieve single or multiple documents from Riak
61
+ # but raise Ripple::DocumentNotFound if a key can
62
+ # not be found in the bucket.
63
+ def find!(*args)
64
+ found = find(*args)
65
+ raise DocumentNotFound.new(args, found) if !found || Array(found).include?(nil)
66
+ found
67
+ end
68
+
69
+ # Find the first object using the first key in the
70
+ # bucket's keys using find. You should not expect to
71
+ # actually get the first object you added to the bucket.
72
+ # This is just a convenience method.
73
+ def first
74
+ find(bucket.keys.first)
75
+ end
76
+
77
+ # Find the first object using the first key in the
78
+ # bucket's keys using find!
79
+ def first!
80
+ find!(bucket.keys.first)
81
+ end
82
+
83
+ # Find all documents in the Document's bucket and return them.
84
+ # @overload list()
85
+ # Get all documents and return them in an array.
86
+ # @param [Hash] options options to be passed to the
87
+ # underlying {Bucket#keys} method.
88
+ # @return [Array<Document>] all found documents in the bucket
89
+ # @overload list() {|doc| ... }
90
+ # Stream all documents in the bucket through the block.
91
+ # @yield [Document] doc a found document
92
+ # @note This operation is incredibly expensive and should not
93
+ # be used in production applications.
94
+ def list
95
+ if block_given?
96
+ bucket.keys do |keys|
97
+ keys.each do |key|
98
+ obj = find_one(key)
99
+ yield obj if obj
100
+ end
101
+ end
102
+ []
103
+ else
104
+ bucket.keys.inject([]) do |acc, k|
105
+ obj = find_one(k)
106
+ obj ? acc << obj : acc
107
+ end
108
+ end
109
+ end
110
+
111
+ private
112
+ def find_one(key)
113
+ instantiate(bucket.get(key, quorums.slice(:r)))
114
+ rescue Riak::FailedRequest => fr
115
+ raise fr unless fr.not_found?
116
+ end
117
+
118
+ def instantiate(robject)
119
+ klass = robject.data['_type'].constantize rescue self
120
+ klass.new.tap do |doc|
121
+ doc.key = robject.key
122
+ doc.__send__(:raw_attributes=, robject.data.except("_type")) if robject.data
123
+ doc.instance_variable_set(:@new, false)
124
+ doc.instance_variable_set(:@robject, robject)
125
+ doc.changed_attributes.clear
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,43 @@
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
+ class_eval <<-CODE
13
+ def key
14
+ #{prop}.to_s
15
+ end
16
+ def key=(value)
17
+ self.#{prop} = value
18
+ end
19
+ def key_attr
20
+ :#{prop}
21
+ end
22
+ CODE
23
+ end
24
+ end
25
+
26
+ module InstanceMethods
27
+ # Reads the key for this Document.
28
+ def key
29
+ @key
30
+ end
31
+
32
+ # Sets the key for this Document.
33
+ def key=(value)
34
+ @key = value.to_s
35
+ end
36
+
37
+ def key_attr
38
+ :key
39
+ end
40
+ end
41
+ end
42
+ end
43
+ 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,115 @@
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(attrs={}, &block)
12
+ new(attrs, &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
+ module InstanceMethods
30
+ # @private
31
+ def initialize
32
+ super
33
+ @new = true
34
+ end
35
+
36
+ # Determines whether this is a new document.
37
+ def new?
38
+ @new || false
39
+ end
40
+
41
+ # Updates a single attribute and then saves the document
42
+ # NOTE: THIS SKIPS VALIDATIONS! Use with caution.
43
+ # @return [true,false] whether the document succeeded in saving
44
+ def update_attribute(attribute, value)
45
+ send("#{attribute}=", value)
46
+ save(:validate => false)
47
+ end
48
+
49
+ # Writes new attributes and then saves the document
50
+ # @return [true,false] whether the document succeeded in saving
51
+ def update_attributes(attrs)
52
+ self.attributes = attrs
53
+ save
54
+ end
55
+
56
+ # Saves the document in Riak.
57
+ # @return [true,false] whether the document succeeded in saving
58
+ def save(*args)
59
+ really_save(*args)
60
+ end
61
+
62
+ def really_save(*args)
63
+ update_robject
64
+ robject.store(self.class.quorums.slice(:w,:dw))
65
+ self.key = robject.key
66
+ @new = false
67
+ true
68
+ end
69
+
70
+ # Reloads the document from Riak
71
+ # @return self
72
+ def reload
73
+ return self if new?
74
+ robject.reload(:force => true)
75
+ self.__send__(:raw_attributes=, @robject.data.except("_type"))
76
+ reset_associations
77
+ self
78
+ end
79
+
80
+ # Deletes the document from Riak and freezes this instance
81
+ def destroy
82
+ robject.delete(self.class.quorums.slice(:rw)) unless new?
83
+ freeze
84
+ true
85
+ rescue Riak::FailedRequest
86
+ false
87
+ end
88
+
89
+ # Freezes the document, preventing further modification.
90
+ def freeze
91
+ @attributes.freeze; super
92
+ end
93
+
94
+ attr_writer :robject
95
+
96
+ def robject
97
+ @robject ||= Riak::RObject.new(self.class.bucket, key).tap do |obj|
98
+ obj.content_type = "application/json"
99
+ end
100
+ end
101
+
102
+ def update_robject
103
+ robject.key = key if robject.key != key
104
+ robject.content_type = 'application/json'
105
+ robject.data = attributes_for_persistence
106
+ end
107
+
108
+ private
109
+ def attributes_for_persistence
110
+ raw_attributes.merge("_type" => self.class.name)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end