riak-client 0.7.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 (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