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