ripple 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. data/Rakefile +61 -48
  2. data/lib/ripple.rb +5 -1
  3. data/lib/ripple/core_ext/casting.rb +3 -0
  4. data/lib/ripple/document.rb +6 -2
  5. data/lib/ripple/document/associations.rb +154 -0
  6. data/lib/ripple/document/{persistence/callbacks.rb → associations/embedded.rb} +20 -24
  7. data/lib/ripple/document/associations/instantiators.rb +41 -0
  8. data/lib/{riak/util/translation.rb → ripple/document/associations/linked.rb} +14 -11
  9. data/lib/ripple/document/associations/many.rb +52 -0
  10. data/lib/ripple/document/associations/many_embedded_proxy.rb +49 -0
  11. data/lib/{riak/i18n.rb → ripple/document/associations/one.rb} +18 -2
  12. data/lib/ripple/document/associations/one_embedded_proxy.rb +41 -0
  13. data/lib/ripple/document/associations/proxy.rb +125 -0
  14. data/lib/ripple/document/attribute_methods.rb +8 -1
  15. data/lib/ripple/document/attribute_methods/read.rb +4 -0
  16. data/lib/ripple/document/attribute_methods/write.rb +4 -0
  17. data/lib/ripple/document/bucket_access.rb +1 -1
  18. data/lib/ripple/document/callbacks.rb +75 -0
  19. data/lib/ripple/document/finders.rb +50 -3
  20. data/lib/ripple/document/persistence.rb +14 -6
  21. data/lib/ripple/document/validations.rb +35 -7
  22. data/lib/ripple/document/validations/associated_validator.rb +37 -0
  23. data/lib/ripple/embedded_document.rb +8 -2
  24. data/lib/{riak/map_reduce_error.rb → ripple/embedded_document/conversion.rb} +19 -5
  25. data/lib/{riak/invalid_response.rb → ripple/embedded_document/finders.rb} +17 -8
  26. data/lib/ripple/embedded_document/persistence.rb +75 -13
  27. data/lib/ripple/locale/en.yml +7 -1
  28. data/{spec/riak/net_http_backend_spec.rb → lib/ripple/railtie.rb} +17 -13
  29. data/spec/fixtures/config.yml +3 -0
  30. data/spec/integration/ripple/associations_spec.rb +81 -0
  31. data/spec/integration/ripple/persistence_spec.rb +54 -0
  32. data/spec/ripple/associations/many_embedded_proxy_spec.rb +124 -0
  33. data/spec/ripple/associations/one_embedded_proxy_spec.rb +130 -0
  34. data/spec/ripple/associations/proxy_spec.rb +78 -0
  35. data/spec/ripple/associations_spec.rb +111 -0
  36. data/spec/ripple/attribute_methods_spec.rb +37 -16
  37. data/spec/ripple/bucket_access_spec.rb +3 -14
  38. data/spec/ripple/callbacks_spec.rb +53 -9
  39. data/spec/ripple/document_spec.rb +22 -6
  40. data/spec/ripple/embedded_document/conversion_spec.rb +35 -0
  41. data/spec/{riak/headers_spec.rb → ripple/embedded_document/finders_spec.rb} +17 -14
  42. data/spec/ripple/embedded_document/persistence_spec.rb +86 -0
  43. data/spec/ripple/embedded_document_spec.rb +1 -26
  44. data/spec/ripple/finders_spec.rb +66 -30
  45. data/spec/ripple/persistence_spec.rb +33 -21
  46. data/spec/ripple/properties_spec.rb +1 -7
  47. data/spec/ripple/ripple_spec.rb +10 -0
  48. data/spec/ripple/timestamps_spec.rb +12 -19
  49. data/spec/ripple/validations_spec.rb +48 -6
  50. data/spec/spec_helper.rb +4 -10
  51. data/spec/support/associations/proxies.rb +16 -0
  52. data/spec/support/integration.rb +4 -0
  53. data/spec/support/mocks.rb +3 -0
  54. data/spec/support/models/address.rb +8 -0
  55. data/spec/support/models/box.rb +6 -0
  56. data/spec/support/models/cardboard_box.rb +3 -0
  57. data/spec/support/models/clock.rb +6 -0
  58. data/spec/support/models/customer.rb +4 -0
  59. data/spec/support/models/email.rb +4 -0
  60. data/spec/support/models/family.rb +14 -0
  61. data/spec/support/models/favorite.rb +4 -0
  62. data/spec/support/models/invoice.rb +6 -0
  63. data/spec/support/models/late_invoice.rb +3 -0
  64. data/spec/support/models/note.rb +4 -0
  65. data/spec/support/models/page.rb +4 -0
  66. data/spec/support/models/paid_invoice.rb +4 -0
  67. data/spec/support/models/tree.rb +3 -0
  68. data/spec/support/models/user.rb +6 -0
  69. data/spec/support/models/widget.rb +6 -0
  70. metadata +111 -138
  71. data/.document +0 -5
  72. data/.gitignore +0 -26
  73. data/CONTRIBUTORS.textile +0 -5
  74. data/LICENSE +0 -13
  75. data/README.textile +0 -128
  76. data/RELEASE_NOTES.textile +0 -68
  77. data/VERSION +0 -1
  78. data/lib/riak.rb +0 -46
  79. data/lib/riak/bucket.rb +0 -157
  80. data/lib/riak/client.rb +0 -139
  81. data/lib/riak/client/curb_backend.rb +0 -82
  82. data/lib/riak/client/http_backend.rb +0 -209
  83. data/lib/riak/client/net_http_backend.rb +0 -49
  84. data/lib/riak/failed_request.rb +0 -37
  85. data/lib/riak/link.rb +0 -73
  86. data/lib/riak/locale/en.yml +0 -37
  87. data/lib/riak/map_reduce.rb +0 -248
  88. data/lib/riak/robject.rb +0 -258
  89. data/lib/riak/util/escape.rb +0 -12
  90. data/lib/riak/util/fiber1.8.rb +0 -48
  91. data/lib/riak/util/headers.rb +0 -44
  92. data/lib/riak/util/multipart.rb +0 -52
  93. data/lib/riak/walk_spec.rb +0 -117
  94. data/ripple.gemspec +0 -169
  95. data/spec/fixtures/cat.jpg +0 -0
  96. data/spec/fixtures/multipart-blank.txt +0 -7
  97. data/spec/fixtures/multipart-with-body.txt +0 -16
  98. data/spec/riak/bucket_spec.rb +0 -230
  99. data/spec/riak/client_spec.rb +0 -174
  100. data/spec/riak/curb_backend_spec.rb +0 -50
  101. data/spec/riak/escape_spec.rb +0 -17
  102. data/spec/riak/http_backend_spec.rb +0 -131
  103. data/spec/riak/link_spec.rb +0 -82
  104. data/spec/riak/map_reduce_spec.rb +0 -352
  105. data/spec/riak/multipart_spec.rb +0 -36
  106. data/spec/riak/object_spec.rb +0 -532
  107. data/spec/riak/walk_spec_spec.rb +0 -208
  108. data/spec/spec.opts +0 -1
  109. data/spec/support/http_backend_implementation_examples.rb +0 -215
  110. data/spec/support/mock_server.rb +0 -58
@@ -1,37 +0,0 @@
1
- # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
- require 'riak'
15
-
16
- module Riak
17
- # Exception raised when the expected response code from Riak
18
- # fails to match the actual response code.
19
- class FailedRequest < StandardError
20
- include Util::Translation
21
- # @return [Symbol] the HTTP method, one of :head, :get, :post, :put, :delete
22
- attr_reader :method
23
- # @return [Fixnum] the expected response code
24
- attr_reader :expected
25
- # @return [Fixnum] the received response code
26
- attr_reader :code
27
- # @return [Hash] the response headers
28
- attr_reader :headers
29
- # @return [String] the response body, if present
30
- attr_reader :body
31
-
32
- def initialize(method, expected_code, received_code, headers, body)
33
- @method, @expected, @code, @headers, @body = method, expected_code, received_code, headers, body
34
- super t("failed_request", :expected => @expected.inspect, :code => @code, :body => @body)
35
- end
36
- end
37
- end
@@ -1,73 +0,0 @@
1
- # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
- require 'riak'
15
-
16
- module Riak
17
- # Represents a link from one object to another in Riak
18
- class Link
19
- include Util::Translation
20
- # @return [String] the URL (relative or absolute) of the related resource
21
- attr_accessor :url
22
-
23
- # @return [String] the relationship ("rel") of the other resource to this one
24
- attr_accessor :rel
25
- alias :tag :rel
26
- alias :tag= :rel=
27
-
28
- # @param [String] header_string the string value of the Link: HTTP header from a Riak response
29
- # @return [Array<Link>] an array of Riak::Link structs parsed from the header
30
- def self.parse(header_string)
31
- header_string.scan(%r{<([^>]+)>\s*;\s*(?:rel|riaktag)=\"([^\"]+)\"}).map do |match|
32
- new(match[0], match[1])
33
- end
34
- end
35
-
36
- def initialize(url, rel)
37
- @url, @rel = url, rel
38
- end
39
-
40
- # @return [String] bucket_name, if the Link url is a known Riak link ("/riak/<bucket>/<key>")
41
- def bucket
42
- URI.unescape($1) if url =~ %r{^/[^/]+/([^/]+)/?}
43
- end
44
-
45
- # @return [String] key, if the Link url is a known Riak link ("/riak/<bucket>/<key>")
46
- def key
47
- URI.unescape($1) if url =~ %r{^/[^/]+/[^/]+/([^/]+)/?}
48
- end
49
-
50
- def inspect; to_s; end
51
-
52
- def to_s
53
- %Q[<#{@url}>; riaktag="#{@rel}"]
54
- end
55
-
56
- def hash
57
- self.to_s.hash
58
- end
59
-
60
- def eql?(other)
61
- self == other
62
- end
63
-
64
- def ==(other)
65
- other.is_a?(Link) && url == other.url && rel == other.rel
66
- end
67
-
68
- def to_walk_spec
69
- raise t("bucket_link_conversion") if @rel == "up" || key.nil?
70
- WalkSpec.new(:bucket => bucket, :tag => @rel)
71
- end
72
- end
73
- end
@@ -1,37 +0,0 @@
1
- # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
- en:
15
- riak:
16
- client_type: "invalid argument {{client}} is not a Riak::Client"
17
- string_type: "invalid_argument {{string}} is not a String"
18
- loading_bucket: "while loading bucket '{{name}}'"
19
- failed_request: "Expected {{expected}} from Riak but received {{code}}. {{body}}"
20
- hash_type: "invalid argument {{hash}} is not a Hash"
21
- path_and_body_required: "You must supply both a resource path and a body."
22
- request_body_type: "Request body must be a string or IO."
23
- resource_path_short: "Resource path too short"
24
- missing_host_and_port: "You must specify a host and port, or use the defaults of 127.0.0.1:8098"
25
- invalid_client_id: "Invalid client ID, must be a string or between 0 and {{max_id}}"
26
- hostname_invalid: "host must be a valid hostname"
27
- port_invalid: "port must be an integer between 0 and 65535"
28
- install_curb: "curb library not found! Please `gem install curb` for better performance."
29
- bucket_link_conversion: "Can't convert a bucket link to a walk spec"
30
- invalid_phase_type: "type must be :map, :reduce, or :link"
31
- module_function_pair_required: "function must have two elements when an array"
32
- stored_function_invalid: "function must have :bucket and :key when a hash"
33
- walk_spec_invalid_unless_link: "WalkSpec is only valid for a function when the type is :link"
34
- invalid_function_value: "invalid value for function: {{value}}"
35
- content_type_undefined: "content_type is not defined!"
36
- too_few_arguments: "too few arguments: {{params}}"
37
- wrong_argument_count_walk_spec: "wrong number of arguments (one Hash or bucket,tag,keep required)"
@@ -1,248 +0,0 @@
1
- # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
- require 'riak'
15
-
16
- module Riak
17
- # Class for invoking map-reduce jobs using the HTTP interface.
18
- class MapReduce
19
- include Util::Translation
20
- # @return [Array<[bucket,key]>,String] The bucket/keys for input to the job, or the bucket (all keys).
21
- # @see #add
22
- attr_accessor :inputs
23
-
24
- # @return [Array<Phase>] The map and reduce phases that will be executed
25
- # @see #map
26
- # @see #reduce
27
- # @see #link
28
- attr_accessor :query
29
-
30
- # Creates a new map-reduce job.
31
- # @param [Client] client the Riak::Client interface
32
- # @yield [self] helpful for initializing the job
33
- def initialize(client)
34
- @client, @inputs, @query = client, [], []
35
- yield self if block_given?
36
- end
37
-
38
- # Add or replace inputs for the job.
39
- # @overload add(bucket)
40
- # Run the job across all keys in the bucket. This will replace any other inputs previously added.
41
- # @param [String, Bucket] bucket the bucket to run the job on
42
- # @overload add(bucket,key)
43
- # Add a bucket/key pair to the job.
44
- # @param [String,Bucket] bucket the bucket of the object
45
- # @param [String] key the key of the object
46
- # @overload add(object)
47
- # Add an object to the job (by its bucket/key)
48
- # @param [RObject] object the object to add to the inputs
49
- # @overload add(bucket, key, keydata)
50
- # @param [String,Bucket] bucket the bucket of the object
51
- # @param [String] key the key of the object
52
- # @param [String] keydata extra data to pass along with the object to the job
53
- # @return [MapReduce] self
54
- def add(*params)
55
- params = params.dup.flatten
56
- case params.size
57
- when 1
58
- p = params.first
59
- case p
60
- when Bucket
61
- @inputs = p.name
62
- when RObject
63
- @inputs << [p.bucket.name, p.key]
64
- when String
65
- @inputs = p
66
- end
67
- when 2..3
68
- bucket = params.shift
69
- bucket = bucket.name if Bucket === bucket
70
- @inputs << params.unshift(bucket)
71
- end
72
- self
73
- end
74
- alias :<< :add
75
- alias :include :add
76
-
77
- # Add a map phase to the job.
78
- # @overload map(function)
79
- # @param [String, Array] function a Javascript function that represents the phase, or an Erlang [module,function] pair
80
- # @overload map(function?, options)
81
- # @param [String, Array] function a Javascript function that represents the phase, or an Erlang [module, function] pair
82
- # @param [Hash] options extra options for the phase (see {Phase#initialize})
83
- # @return [MapReduce] self
84
- # @see Phase#initialize
85
- def map(*params)
86
- options = params.extract_options!
87
- @query << Phase.new({:type => :map, :function => params.shift}.merge(options))
88
- self
89
- end
90
-
91
- # Add a reduce phase to the job.
92
- # @overload reduce(function)
93
- # @param [String, Array] function a Javascript function that represents the phase, or an Erlang [module,function] pair
94
- # @overload reduce(function?, options)
95
- # @param [String, Array] function a Javascript function that represents the phase, or an Erlang [module, function] pair
96
- # @param [Hash] options extra options for the phase (see {Phase#initialize})
97
- # @return [MapReduce] self
98
- # @see Phase#initialize
99
- def reduce(*params)
100
- options = params.extract_options!
101
- @query << Phase.new({:type => :reduce, :function => params.shift}.merge(options))
102
- self
103
- end
104
-
105
- # Add a link phase to the job. Link phases follow links attached to objects automatically (a special case of map).
106
- # @overload link(walk_spec, options={})
107
- # @param [WalkSpec] walk_spec a WalkSpec that represents the types of links to follow
108
- # @param [Hash] options extra options for the phase (see {Phase#initialize})
109
- # @overload link(bucket, tag, keep, options={})
110
- # @param [String, nil] bucket the bucket to limit links to
111
- # @param [String, nil] tag the tag to limit links to
112
- # @param [Boolean] keep whether to keep results of this phase (overrides the phase options)
113
- # @param [Hash] options extra options for the phase (see {Phase#initialize})
114
- # @overload link(options)
115
- # @param [Hash] options options for both the walk spec and link phase
116
- # @see WalkSpec#initialize
117
- # @return [MapReduce] self
118
- # @see Phase#initialize
119
- def link(*params)
120
- options = params.extract_options!
121
- walk_spec_options = options.slice!(:type, :function, :language, :arg) unless params.first
122
- walk_spec = WalkSpec.normalize(params.shift || walk_spec_options).first
123
- @query << Phase.new({:type => :link, :function => walk_spec}.merge(options))
124
- self
125
- end
126
-
127
- # Sets the timeout for the map-reduce job.
128
- # @param [Fixnum] value the job timeout, in milliseconds
129
- def timeout(value)
130
- @timeout = value
131
- end
132
-
133
- # Convert the job to JSON for submission over the HTTP interface.
134
- # @return [String] the JSON representation
135
- def to_json(options={})
136
- hash = {"inputs" => inputs, "query" => query.map(&:as_json)}
137
- hash['timeout'] = @timeout.to_i if @timeout
138
- ActiveSupport::JSON.encode(hash, options)
139
- end
140
-
141
- # Executes this map-reduce job.
142
- # @return [Array<Array>] similar to link-walking, each element is an array of results from a phase where "keep" is true. If there is only one "keep" phase, only the results from that phase will be returned.
143
- def run
144
- response = @client.http.post(200, @client.mapred, to_json, {"Content-Type" => "application/json", "Accept" => "application/json"})
145
- if response.try(:[], :headers).try(:[],'content-type').include?("application/json")
146
- ActiveSupport::JSON.decode(response[:body])
147
- else
148
- response
149
- end
150
- rescue FailedRequest => fr
151
- if fr.code == 500 && fr.headers['content-type'].include?("application/json")
152
- raise MapReduceError.new(fr.body)
153
- else
154
- raise fr
155
- end
156
- end
157
-
158
- # Represents an individual phase in a map-reduce pipeline. Generally you'll want to call
159
- # methods of {MapReduce} instead of using this directly.
160
- class Phase
161
- include Util::Translation
162
- # @return [Symbol] the type of phase - :map, :reduce, or :link
163
- attr_accessor :type
164
-
165
- # @return [String, Array<String, String>, Hash, WalkSpec] For :map and :reduce types, the Javascript function to run (as a string or hash with bucket/key), or the module + function in Erlang to run. For a :link type, a {Riak::WalkSpec} or an equivalent hash.
166
- attr_accessor :function
167
-
168
- # @return [String] the language of the phase's function - "javascript" or "erlang". Meaningless for :link type phases.
169
- attr_accessor :language
170
-
171
- # @return [Boolean] whether results of this phase will be returned
172
- attr_accessor :keep
173
-
174
- # @return [Array] any extra static arguments to pass to the phase
175
- attr_accessor :arg
176
-
177
- # Creates a phase in the map-reduce pipeline
178
- # @param [Hash] options options for the phase
179
- # @option options [Symbol] :type one of :map, :reduce, :link
180
- # @option options [String] :language ("javascript") "erlang" or "javascript"
181
- # @option options [String, Array, Hash] :function In the case of Javascript, a literal function in a string, or a hash with :bucket and :key. In the case of Erlang, an Array of [module, function]. For a :link phase, a hash including any of :bucket, :tag or a WalkSpec.
182
- # @option options [Boolean] :keep (false) whether to return the results of this phase
183
- # @option options [Array] :arg (nil) any extra static arguments to pass to the phase
184
- def initialize(options={})
185
- self.type = options[:type]
186
- self.language = options[:language] || "javascript"
187
- self.function = options[:function]
188
- self.keep = options[:keep] || false
189
- self.arg = options[:arg]
190
- end
191
-
192
- def type=(value)
193
- raise ArgumentError, t("invalid_phase_type") unless value.to_s =~ /^(map|reduce|link)$/i
194
- @type = value.to_s.downcase.to_sym
195
- end
196
-
197
- def function=(value)
198
- case value
199
- when Array
200
- raise ArgumentError, t("module_function_pair_required") unless value.size == 2
201
- @language = "erlang"
202
- when Hash
203
- raise ArgumentError, t("stored_function_invalid") unless type == :link || value.has_key?(:bucket) && value.has_key?(:key)
204
- @language = "javascript"
205
- when String
206
- @language = "javascript"
207
- when WalkSpec
208
- raise ArgumentError, t("walk_spec_invalid_unless_link") unless type == :link
209
- else
210
- raise ArgumentError, t("invalid_function_value", :value => value.inspect)
211
- end
212
- @function = value
213
- end
214
-
215
- # Converts the phase to JSON for use while invoking a job.
216
- # @return [String] a JSON representation of the phase
217
- def to_json(options=nil)
218
- ActiveSupport::JSON.encode(as_json, options)
219
- end
220
-
221
- # Converts the phase to its JSON-compatible representation for job invocation.
222
- # @return [Hash] a Hash-equivalent of the phase
223
- def as_json(options=nil)
224
- obj = case type
225
- when :map, :reduce
226
- defaults = {"language" => language, "keep" => keep}
227
- case function
228
- when Hash
229
- defaults.merge(function)
230
- when String
231
- if function =~ /\s*function/
232
- defaults.merge("source" => function)
233
- else
234
- defaults.merge("name" => function)
235
- end
236
- when Array
237
- defaults.merge("module" => function[0], "function" => function[1])
238
- end
239
- when :link
240
- spec = WalkSpec.normalize(function).first
241
- {"bucket" => spec.bucket, "tag" => spec.tag, "keep" => spec.keep || keep}
242
- end
243
- obj["arg"] = arg if arg
244
- { type => obj }
245
- end
246
- end
247
- end
248
- end
@@ -1,258 +0,0 @@
1
- # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
- require 'riak'
15
- require 'set'
16
-
17
- module Riak
18
- # Parent class of all object types supported by ripple. {Riak::RObject} represents
19
- # the data and metadata stored in a bucket/key pair in the Riak database.
20
- class RObject
21
- include Util
22
- include Util::Translation
23
- include Util::Escape
24
-
25
- # @return [Bucket] the bucket in which this object is contained
26
- attr_accessor :bucket
27
-
28
- # @return [String] the key of this object within its bucket
29
- attr_accessor :key
30
-
31
- # @return [String] the MIME content type of the object
32
- attr_accessor :content_type
33
-
34
- # @return [String] the Riak vector clock for the object
35
- attr_accessor :vclock
36
- alias_attribute :vector_clock, :vclock
37
-
38
- # @return [Object] the data stored in Riak at this object's key. Varies in format by content-type, defaulting to String from the response body.
39
- attr_accessor :data
40
-
41
- # @return [Set<Link>] an Set of {Riak::Link} objects for relationships between this object and other resources
42
- attr_accessor :links
43
-
44
- # @return [String] the ETag header from the most recent HTTP response, useful for caching and reloading
45
- attr_accessor :etag
46
-
47
- # @return [Time] the Last-Modified header from the most recent HTTP response, useful for caching and reloading
48
- attr_accessor :last_modified
49
-
50
- # @return [Hash] a hash of any X-Riak-Meta-* headers that were in the HTTP response, keyed on the trailing portion
51
- attr_accessor :meta
52
-
53
- # Create a new object manually
54
- # @param [Bucket] bucket the bucket in which the object exists
55
- # @param [String] key the key at which the object resides. If nil, a key will be assigned when the object is saved.
56
- # @see Bucket#get
57
- def initialize(bucket, key=nil)
58
- @bucket, @key = bucket, key
59
- @links, @meta = Set.new, {}
60
- yield self if block_given?
61
- end
62
-
63
- # Load object data from an HTTP response
64
- # @param [Hash] response a response from {Riak::Client::HTTPBackend}
65
- def load(response)
66
- extract_header(response, "location", :key) {|v| URI.unescape(v.split("/").last) }
67
- extract_header(response, "content-type", :content_type)
68
- extract_header(response, "x-riak-vclock", :vclock)
69
- extract_header(response, "link", :links) {|v| Set.new(Link.parse(v)) }
70
- extract_header(response, "etag", :etag)
71
- extract_header(response, "last-modified", :last_modified) {|v| Time.httpdate(v) }
72
- @meta = response[:headers].inject({}) do |h,(k,v)|
73
- if k =~ /x-riak-meta-(.*)/
74
- h[$1] = v
75
- end
76
- h
77
- end
78
- @conflict = response[:code].try(:to_i) == 300 && content_type =~ /multipart\/mixed/
79
- @siblings = nil
80
- @data = deserialize(response[:body]) if response[:body].present?
81
- self
82
- end
83
-
84
- # HTTP header hash that will be sent along when storing the object
85
- # @return [Hash] hash of HTTP Headers
86
- def store_headers
87
- {}.tap do |hash|
88
- hash["Content-Type"] = @content_type
89
- hash["X-Riak-Vclock"] = @vclock if @vclock
90
- unless @links.blank?
91
- hash["Link"] = @links.reject {|l| l.rel == "up" }.map(&:to_s).join(", ")
92
- end
93
- unless @meta.blank?
94
- @meta.each do |k,v|
95
- hash["X-Riak-Meta-#{k}"] = v.to_s
96
- end
97
- end
98
- end
99
- end
100
-
101
- # HTTP header hash that will be sent along when reloading the object
102
- # @return [Hash] hash of HTTP headers
103
- def reload_headers
104
- {}.tap do |h|
105
- h['If-None-Match'] = @etag if @etag.present?
106
- h['If-Modified-Since'] = @last_modified.httpdate if @last_modified.present?
107
- end
108
- end
109
-
110
- # Store the object in Riak
111
- # @param [Hash] options query parameters
112
- # @option options [Fixnum] :r the "r" parameter (Read quorum for the implicit read performed when validating the store operation)
113
- # @option options [Fixnum] :w the "w" parameter (Write quorum)
114
- # @option options [Fixnum] :dw the "dw" parameter (Durable-write quorum)
115
- # @option options [Boolean] :returnbody (true) whether to return the result of a successful write in the body of the response. Set to false for fire-and-forget updates, set to true to immediately have access to the object's stored representation.
116
- # @return [Riak::RObject] self
117
- # @raise [ArgumentError] if the content_type is not defined
118
- def store(options={})
119
- raise ArgumentError, t("content_type_undefined") unless @content_type.present?
120
- params = {:returnbody => true}.merge(options)
121
- method, codes, path = @key.present? ? [:put, [200,204,300], "#{escape(@bucket.name)}/#{escape(@key)}"] : [:post, 201, escape(@bucket.name)]
122
- response = @bucket.client.http.send(method, codes, @bucket.client.prefix, path, params, serialize(data), store_headers)
123
- load(response)
124
- end
125
-
126
- # Reload the object from Riak. Will use conditional GETs when possible.
127
- # @param [Hash] options query parameters
128
- # @option options [Fixnum] :r the "r" parameter (Read quorum)
129
- # @option options [Boolean] :force will force a reload request if the vclock is not present, useful for reloading the object after a store (not passed in the query params)
130
- # @return [Riak::RObject] self
131
- def reload(options={})
132
- force = options.delete(:force)
133
- return self unless @key && (@vclock || force)
134
- codes = @bucket.allow_mult ? [200,300,304] : [200,304]
135
- response = @bucket.client.http.get(codes, @bucket.client.prefix, escape(@bucket.name), escape(@key), options, reload_headers)
136
- load(response) unless response[:code] == 304
137
- self
138
- end
139
-
140
- alias :fetch :reload
141
-
142
- # Delete the object from Riak and freeze this instance. Will work whether or not the object actually
143
- # exists in the Riak database.
144
- def delete
145
- return if key.blank?
146
- @bucket.client.http.delete([204,404], @bucket.client.prefix, escape(@bucket.name), escape(@key))
147
- freeze
148
- end
149
-
150
- # Returns sibling objects when in conflict.
151
- # @return [Array<RObject>] an array of conflicting sibling objects for this key
152
- # @return [self] this object when not in conflict
153
- def siblings
154
- return self unless conflict?
155
- @siblings ||= Multipart.parse(data, Multipart.extract_boundary(content_type)).map do |part|
156
- RObject.new(self.bucket, self.key) do |sibling|
157
- sibling.load(part)
158
- sibling.vclock = vclock
159
- end
160
- end
161
- end
162
-
163
- # @return [true,false] Whether this object has conflicting sibling objects (divergent vclocks)
164
- def conflict?
165
- @conflict.present?
166
- end
167
-
168
- # Serializes the internal object data for sending to Riak. Differs based on the content-type.
169
- # This method is called internally when storing the object.
170
- # Automatically serialized formats:
171
- # * JSON (application/json)
172
- # * YAML (text/yaml)
173
- # * Marshal (application/octet-stream if meta['ruby-serialization'] == "Marshal")
174
- # @param [Object] payload the data to serialize
175
- def serialize(payload)
176
- return payload if IO === payload
177
- case @content_type
178
- when /json/
179
- ActiveSupport::JSON.encode(payload)
180
- when /yaml/
181
- YAML.dump(payload)
182
- when "application/octet-stream"
183
- if @meta['ruby-serialization'] == "Marshal"
184
- Marshal.dump(payload)
185
- else
186
- payload.to_s
187
- end
188
- else
189
- payload.to_s
190
- end
191
- end
192
-
193
- # Deserializes the internal object data from a Riak response. Differs based on the content-type.
194
- # This method is called internally when loading the object.
195
- # Automatically deserialized formats:
196
- # * JSON (application/json)
197
- # * YAML (text/yaml)
198
- # * Marshal (application/octet-stream if meta['ruby-serialization'] == "Marshal")
199
- # @param [String] body the serialized response body
200
- def deserialize(body)
201
- case @content_type
202
- when /json/
203
- ActiveSupport::JSON.decode(body)
204
- when /yaml/
205
- YAML.load(body)
206
- when "application/octet-stream"
207
- if @meta['ruby-serialization'] == "Marshal"
208
- Marshal.load(body)
209
- else
210
- body
211
- end
212
- else
213
- body
214
- end
215
- end
216
-
217
- # @return [String] A representation suitable for IRB and debugging output
218
- def inspect
219
- "#<#{self.class.name} #{@bucket.client.http.path(@bucket.client.prefix, escape(@bucket.name), escape(@key)).to_s} [#{@content_type}]:#{@data.inspect}>"
220
- end
221
-
222
- # Walks links from this object to other objects in Riak.
223
- def walk(*params)
224
- specs = WalkSpec.normalize(*params)
225
- response = @bucket.client.http.get(200, @bucket.client.prefix, escape(@bucket.name), escape(@key), specs.join("/"))
226
- if boundary = Multipart.extract_boundary(response[:headers]['content-type'].first)
227
- Multipart.parse(response[:body], boundary).map do |group|
228
- map_walk_group(group)
229
- end
230
- else
231
- []
232
- end
233
- end
234
-
235
- # Converts the object to a link suitable for linking other objects to it
236
- def to_link(tag=nil)
237
- Link.new(@bucket.client.http.path(@bucket.client.prefix, escape(@bucket.name), escape(@key)).path, tag)
238
- end
239
-
240
- private
241
- def extract_header(response, name, attribute=nil)
242
- if response[:headers][name].present?
243
- value = response[:headers][name].try(:first)
244
- value = yield value if block_given?
245
- send("#{attribute}=", value) if attribute
246
- end
247
- end
248
-
249
- def map_walk_group(group)
250
- group.map do |obj|
251
- if obj[:headers] && obj[:body] && obj[:headers]['location']
252
- bucket, key = $1, $2 if obj[:headers]['location'].first =~ %r{/.*/(.*)/(.*)$}
253
- RObject.new(@bucket.client.bucket(bucket, :keys => false), key).load(obj)
254
- end
255
- end
256
- end
257
- end
258
- end