riak-client 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/Rakefile +74 -0
  2. data/lib/riak.rb +49 -0
  3. data/lib/riak/bucket.rb +176 -0
  4. data/lib/riak/cache_store.rb +82 -0
  5. data/lib/riak/client.rb +139 -0
  6. data/lib/riak/client/curb_backend.rb +82 -0
  7. data/lib/riak/client/http_backend.rb +209 -0
  8. data/lib/riak/client/net_http_backend.rb +49 -0
  9. data/lib/riak/failed_request.rb +37 -0
  10. data/lib/riak/i18n.rb +20 -0
  11. data/lib/riak/invalid_response.rb +25 -0
  12. data/lib/riak/link.rb +73 -0
  13. data/lib/riak/locale/en.yml +37 -0
  14. data/lib/riak/map_reduce.rb +248 -0
  15. data/lib/riak/map_reduce_error.rb +20 -0
  16. data/lib/riak/robject.rb +267 -0
  17. data/lib/riak/util/escape.rb +12 -0
  18. data/lib/riak/util/fiber1.8.rb +48 -0
  19. data/lib/riak/util/headers.rb +44 -0
  20. data/lib/riak/util/multipart.rb +52 -0
  21. data/lib/riak/util/translation.rb +29 -0
  22. data/lib/riak/walk_spec.rb +117 -0
  23. data/spec/fixtures/cat.jpg +0 -0
  24. data/spec/fixtures/multipart-blank.txt +7 -0
  25. data/spec/fixtures/multipart-with-body.txt +16 -0
  26. data/spec/integration/riak/cache_store_spec.rb +129 -0
  27. data/spec/riak/bucket_spec.rb +247 -0
  28. data/spec/riak/client_spec.rb +174 -0
  29. data/spec/riak/curb_backend_spec.rb +53 -0
  30. data/spec/riak/escape_spec.rb +21 -0
  31. data/spec/riak/headers_spec.rb +34 -0
  32. data/spec/riak/http_backend_spec.rb +131 -0
  33. data/spec/riak/link_spec.rb +82 -0
  34. data/spec/riak/map_reduce_spec.rb +352 -0
  35. data/spec/riak/multipart_spec.rb +36 -0
  36. data/spec/riak/net_http_backend_spec.rb +28 -0
  37. data/spec/riak/object_spec.rb +538 -0
  38. data/spec/riak/walk_spec_spec.rb +208 -0
  39. data/spec/spec_helper.rb +30 -0
  40. data/spec/support/http_backend_implementation_examples.rb +215 -0
  41. data/spec/support/mock_server.rb +61 -0
  42. data/spec/support/mocks.rb +3 -0
  43. metadata +187 -0
@@ -0,0 +1,37 @@
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)"
@@ -0,0 +1,248 @@
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
@@ -0,0 +1,20 @@
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
+ # Raised when an error occurred in the Javascript map-reduce chain.
18
+ # The message will be the body of the JSON error response.
19
+ class MapReduceError < StandardError; end
20
+ end
@@ -0,0 +1,267 @@
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.delete(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} #{url} [#{@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
+ # Generates a URL representing the object according to the client, bucket and key.
241
+ # If the key is blank, the bucket URL will be returned (where the object will be
242
+ # submitted to when stored).
243
+ def url
244
+ segments = [ @bucket.client.prefix, escape(@bucket.name)]
245
+ segments << escape(@key) if @key
246
+ @bucket.client.http.path(*segments).to_s
247
+ end
248
+
249
+ private
250
+ def extract_header(response, name, attribute=nil)
251
+ if response[:headers][name].present?
252
+ value = response[:headers][name].try(:first)
253
+ value = yield value if block_given?
254
+ send("#{attribute}=", value) if attribute
255
+ end
256
+ end
257
+
258
+ def map_walk_group(group)
259
+ group.map do |obj|
260
+ if obj[:headers] && obj[:body] && obj[:headers]['location']
261
+ bucket, key = $1, $2 if obj[:headers]['location'].first =~ %r{/.*/(.*)/(.*)$}
262
+ RObject.new(@bucket.client.bucket(bucket, :keys => false), key).load(obj)
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end