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.
- data/Rakefile +74 -0
- data/lib/riak.rb +49 -0
- data/lib/riak/bucket.rb +176 -0
- data/lib/riak/cache_store.rb +82 -0
- data/lib/riak/client.rb +139 -0
- data/lib/riak/client/curb_backend.rb +82 -0
- data/lib/riak/client/http_backend.rb +209 -0
- data/lib/riak/client/net_http_backend.rb +49 -0
- data/lib/riak/failed_request.rb +37 -0
- data/lib/riak/i18n.rb +20 -0
- data/lib/riak/invalid_response.rb +25 -0
- data/lib/riak/link.rb +73 -0
- data/lib/riak/locale/en.yml +37 -0
- data/lib/riak/map_reduce.rb +248 -0
- data/lib/riak/map_reduce_error.rb +20 -0
- data/lib/riak/robject.rb +267 -0
- data/lib/riak/util/escape.rb +12 -0
- data/lib/riak/util/fiber1.8.rb +48 -0
- data/lib/riak/util/headers.rb +44 -0
- data/lib/riak/util/multipart.rb +52 -0
- data/lib/riak/util/translation.rb +29 -0
- data/lib/riak/walk_spec.rb +117 -0
- data/spec/fixtures/cat.jpg +0 -0
- data/spec/fixtures/multipart-blank.txt +7 -0
- data/spec/fixtures/multipart-with-body.txt +16 -0
- data/spec/integration/riak/cache_store_spec.rb +129 -0
- data/spec/riak/bucket_spec.rb +247 -0
- data/spec/riak/client_spec.rb +174 -0
- data/spec/riak/curb_backend_spec.rb +53 -0
- data/spec/riak/escape_spec.rb +21 -0
- data/spec/riak/headers_spec.rb +34 -0
- data/spec/riak/http_backend_spec.rb +131 -0
- data/spec/riak/link_spec.rb +82 -0
- data/spec/riak/map_reduce_spec.rb +352 -0
- data/spec/riak/multipart_spec.rb +36 -0
- data/spec/riak/net_http_backend_spec.rb +28 -0
- data/spec/riak/object_spec.rb +538 -0
- data/spec/riak/walk_spec_spec.rb +208 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/http_backend_implementation_examples.rb +215 -0
- data/spec/support/mock_server.rb +61 -0
- data/spec/support/mocks.rb +3 -0
- 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
|
data/lib/riak/robject.rb
ADDED
@@ -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
|