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,151 @@
1
+ require 'tzinfo'
2
+ require 'active_support/json'
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'active_support/core_ext/object/to_json'
5
+ require 'active_support/core_ext/date/conversions'
6
+ require 'active_support/core_ext/date/zones'
7
+ require 'active_support/core_ext/date_time/conversions'
8
+ require 'active_support/core_ext/date_time/zones'
9
+ require 'active_support/core_ext/time/conversions'
10
+ require 'active_support/core_ext/time/zones'
11
+ require 'active_support/core_ext/string/conversions'
12
+ require 'ripple/property_type_mismatch'
13
+ require 'set'
14
+
15
+ # @private
16
+ class Object
17
+ def self.ripple_cast(value)
18
+ value
19
+ end
20
+ end
21
+
22
+ # @private
23
+ class Symbol
24
+ def self.ripple_cast(value)
25
+ return nil if value.blank?
26
+ value.respond_to?(:to_s) && value.to_s.intern or raise Ripple::PropertyTypeMismatch.new(self, value)
27
+ end
28
+ end
29
+
30
+ # @private
31
+ class Numeric
32
+ def self.ripple_cast(value)
33
+ return nil if value.blank?
34
+ raise Ripple::PropertyTypeMismatch.new(self,value) unless value.respond_to?(:to_i) && value.respond_to?(:to_f)
35
+ float_value = value.to_f
36
+ int_value = value.to_i
37
+ float_value == int_value ? int_value : float_value
38
+ end
39
+ end
40
+
41
+ # @private
42
+ class Integer
43
+ def self.ripple_cast(value)
44
+ return nil if value.nil? || (String === value && value.blank?)
45
+ !value.is_a?(Symbol) && value.respond_to?(:to_i) && value.to_i or raise Ripple::PropertyTypeMismatch.new(self, value)
46
+ end
47
+ end
48
+
49
+ # @private
50
+ class Float
51
+ def self.ripple_cast(value)
52
+ return nil if value.nil? || (String === value && value.blank?)
53
+ value.respond_to?(:to_f) && value.to_f or raise Ripple::PropertyTypeMismatch.new(self, value)
54
+ end
55
+ end
56
+
57
+ # @private
58
+ class String
59
+ def self.ripple_cast(value)
60
+ return nil if value.nil?
61
+ value.respond_to?(:to_s) && value.to_s or raise Ripple::PropertyTypeMismatch.new(self, value)
62
+ end
63
+ end
64
+
65
+ BooleanCast = Module.new do
66
+ def ripple_cast(value)
67
+ case value
68
+ when NilClass
69
+ nil
70
+ when Numeric
71
+ !value.zero?
72
+ when TrueClass, FalseClass
73
+ value
74
+ when /^\s*0/
75
+ false
76
+ when /^\s*t/i
77
+ true
78
+ when /^\s*f/i
79
+ false
80
+ else
81
+ value.present?
82
+ end
83
+ end
84
+ end
85
+
86
+ unless defined?(::Boolean)
87
+ # Stand-in for true/false property types.
88
+ module ::Boolean; end
89
+ end
90
+
91
+ ::Boolean.send(:extend, BooleanCast)
92
+ TrueClass.send(:extend, BooleanCast)
93
+ FalseClass.send(:extend, BooleanCast)
94
+
95
+ # @private
96
+ class Time
97
+ def as_json(options={})
98
+ self.utc.send(Ripple.date_format)
99
+ end
100
+
101
+ def self.ripple_cast(value)
102
+ return nil if value.blank?
103
+ value.respond_to?(:to_time) && value.to_time or raise Ripple::PropertyTypeMismatch.new(self, value)
104
+ end
105
+ end
106
+
107
+ # @private
108
+ class Date
109
+ def as_json(options={})
110
+ self.to_s(Ripple.date_format)
111
+ end
112
+
113
+ def self.ripple_cast(value)
114
+ return nil if value.blank?
115
+ value.respond_to?(:to_date) && value.to_date or raise Ripple::PropertyTypeMismatch.new(self, value)
116
+ end
117
+ end
118
+
119
+ # @private
120
+ class DateTime
121
+ def as_json(options={})
122
+ self.utc.to_s(Ripple.date_format)
123
+ end
124
+
125
+ def self.ripple_cast(value)
126
+ return nil if value.blank?
127
+ value.respond_to?(:to_datetime) && value.to_datetime or raise Ripple::PropertyTypeMismatch.new(self, value)
128
+ end
129
+ end
130
+
131
+ # @private
132
+ module ActiveSupport
133
+ class TimeWithZone
134
+ def as_json(options={})
135
+ self.utc.send(Ripple.date_format)
136
+ end
137
+ end
138
+ end
139
+
140
+ # @private
141
+ class Set
142
+ def as_json(options = {})
143
+ map { |e| e.as_json(options) }
144
+ end
145
+
146
+ def self.ripple_cast(value)
147
+ return nil if value.nil?
148
+ value.is_a?(Enumerable) && new(value) or raise Ripple::PropertyTypeMismatch.new(self, value)
149
+ end
150
+ end
151
+
@@ -0,0 +1,89 @@
1
+ require 'tzinfo'
2
+ require 'active_support/core_ext/date/conversions'
3
+ require 'active_support/core_ext/date/zones'
4
+ require 'active_support/core_ext/date_time/conversions'
5
+ require 'active_support/core_ext/date_time/zones'
6
+ require 'active_support/core_ext/time/conversions'
7
+ require 'active_support/core_ext/time/zones'
8
+ require 'active_support/core_ext/string/conversions'
9
+ require 'ripple/property_type_mismatch'
10
+ require 'set'
11
+
12
+ # @private
13
+ class Object
14
+ def to_ripple_index(type)
15
+ case type
16
+ when 'bin'
17
+ to_s
18
+ when 'int'
19
+ to_i
20
+ end
21
+ end
22
+ end
23
+
24
+ # @private
25
+ class Time
26
+ def to_ripple_index(type)
27
+ case type
28
+ when 'bin'
29
+ utc.send(Ripple.date_format)
30
+ when 'int'
31
+ # Use millisecond-precision
32
+ (utc.to_f * 1000).round
33
+ end
34
+ end
35
+ end
36
+
37
+ # @private
38
+ class Date
39
+ def to_ripple_index(type)
40
+ case type
41
+ when 'bin'
42
+ to_s(Ripple.date_format)
43
+ when 'int'
44
+ to_time(:utc).to_ripple_index(type)
45
+ end
46
+ end
47
+ end
48
+
49
+ # @private
50
+ class DateTime
51
+ def to_ripple_index(type)
52
+ case type
53
+ when 'bin'
54
+ utc.to_s(Ripple.date_format)
55
+ when 'int'
56
+ (utc.to_f * 1000).round
57
+ end
58
+ end
59
+ end
60
+
61
+ # @private
62
+ module ActiveSupport
63
+ class TimeWithZone
64
+ def to_ripple_index(type)
65
+ utc.to_ripple_index(type)
66
+ end
67
+ end
68
+ end
69
+
70
+ # @private
71
+ module Enumerable
72
+ def to_ripple_index(type)
73
+ Set.new(map {|v| v.to_ripple_index(type) })
74
+ end
75
+ end
76
+
77
+ if String < Enumerable
78
+ # Fix for 1.8, in which String is Enumerable
79
+ class String
80
+ def to_ripple_index(type)
81
+ case type
82
+ when 'bin'
83
+ to_s
84
+ when 'int'
85
+ to_i
86
+ end
87
+ end
88
+ end
89
+ end
@@ -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,105 @@
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/indexes'
12
+ require 'ripple/timestamps'
13
+ require 'ripple/validations'
14
+ require 'ripple/associations'
15
+ require 'ripple/callbacks'
16
+ require 'ripple/observable'
17
+ require 'ripple/conversion'
18
+ require 'ripple/inspection'
19
+ require 'ripple/nested_attributes'
20
+ require 'ripple/serialization'
21
+
22
+ module Ripple
23
+ # Represents a model stored in Riak, serialized in JSON object (document).
24
+ # Ripple::Document models aim to be fully ActiveModel compatible, with a keen
25
+ # eye toward features that developers expect from ActiveRecord, DataMapper and MongoMapper.
26
+ #
27
+ # Example:
28
+ #
29
+ # class Email
30
+ # include Ripple::Document
31
+ # property :from, String, :presence => true
32
+ # property :to, String, :presence => true
33
+ # property :sent, Time, :default => proc { Time.now }
34
+ # property :body, String
35
+ # end
36
+ #
37
+ # email = Email.find("37458abc752f8413e") # GET /riak/emails/37458abc752f8413e
38
+ # email.from = "someone@nowhere.net"
39
+ # email.save # PUT /riak/emails/37458abc752f8413e
40
+ #
41
+ # reply = Email.new
42
+ # reply.from = "justin@bashoooo.com"
43
+ # reply.to = "sean@geeemail.com"
44
+ # reply.body = "Riak is a good fit for scalable Ruby apps."
45
+ # reply.save # POST /riak/emails (Riak-assigned key)
46
+ #
47
+ module Document
48
+ extend ActiveSupport::Concern
49
+
50
+ included do
51
+ extend ActiveModel::Naming
52
+ extend BucketAccess
53
+ include Ripple::Document::Key
54
+ include Ripple::Document::Persistence
55
+ extend Ripple::Properties
56
+ include Ripple::Document::Finders
57
+ include Ripple::AttributeMethods
58
+ include Ripple::Timestamps
59
+ include Ripple::Indexes
60
+ include Ripple::Indexes::DocumentMethods
61
+ include Ripple::Validations
62
+ include Ripple::Associations
63
+ include Ripple::Callbacks
64
+ include Ripple::Observable
65
+ include Ripple::Conversion
66
+ include Ripple::Inspection
67
+ include Ripple::NestedAttributes
68
+ include Ripple::Serialization
69
+ include Ripple::Conflict::DocumentHooks
70
+ end
71
+
72
+ module ClassMethods
73
+ def embeddable?
74
+ false
75
+ end
76
+ end
77
+
78
+ def _root_document
79
+ self
80
+ end
81
+
82
+ # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same key.
83
+ def ==(comparison_object)
84
+ comparison_object.equal?(self) ||
85
+ (comparison_object.class < Document && (comparison_object.instance_of?(self.class) || comparison_object.class.bucket.name == self.class.bucket.name) &&
86
+ !new? && comparison_object.key == key && !comparison_object.new?)
87
+ end
88
+
89
+ def eql?(other)
90
+ return true if other.equal?(self)
91
+
92
+ (other.class.equal?(self.class)) &&
93
+ !other.new? && !new? &&
94
+ (other.key == key)
95
+ end
96
+
97
+ def hash
98
+ if new?
99
+ super # every new document should be treated as a different doc
100
+ else
101
+ [self.class, key].hash
102
+ end
103
+ end
104
+ end
105
+ 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