riak-client 0.8.3 → 0.9.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +10 -3
- data/lib/riak/bucket.rb +31 -46
- data/lib/riak/client.rb +18 -4
- data/lib/riak/client/excon_backend.rb +15 -1
- data/lib/riak/client/http_backend.rb +169 -196
- data/lib/riak/client/http_backend/configuration.rb +56 -0
- data/lib/riak/client/http_backend/object_methods.rb +101 -0
- data/lib/riak/client/http_backend/request_headers.rb +46 -0
- data/lib/riak/client/http_backend/transport_methods.rb +208 -0
- data/lib/riak/core_ext/blank.rb +14 -2
- data/lib/riak/link.rb +9 -4
- data/lib/riak/locale/en.yml +2 -0
- data/lib/riak/map_reduce.rb +37 -103
- data/lib/riak/map_reduce/filter_builder.rb +106 -0
- data/lib/riak/map_reduce/phase.rb +108 -0
- data/lib/riak/robject.rb +19 -96
- data/lib/riak/util/escape.rb +7 -0
- data/riak-client.gemspec +5 -5
- data/spec/riak/bucket_spec.rb +57 -111
- data/spec/riak/client_spec.rb +97 -78
- data/spec/riak/escape_spec.rb +7 -0
- data/spec/riak/http_backend/object_methods_spec.rb +218 -0
- data/spec/riak/http_backend_spec.rb +204 -65
- data/spec/riak/link_spec.rb +8 -0
- data/spec/riak/map_reduce_spec.rb +26 -151
- data/spec/riak/object_spec.rb +36 -350
- data/spec/riak/search_spec.rb +5 -16
- data/spec/spec_helper.rb +1 -1
- metadata +29 -8
data/lib/riak/link.rb
CHANGED
@@ -22,7 +22,7 @@ module Riak
|
|
22
22
|
# @return [String] the relationship tag (or "rel") of the other resource to this one
|
23
23
|
attr_accessor :tag
|
24
24
|
alias_method :rel, :tag
|
25
|
-
alias_method :rel
|
25
|
+
alias_method :'rel=', :'tag='
|
26
26
|
|
27
27
|
# @return [String] the bucket of the related resource
|
28
28
|
attr_accessor :bucket
|
@@ -30,6 +30,10 @@ module Riak
|
|
30
30
|
# @return [String] the key of the related resource
|
31
31
|
attr_accessor :key
|
32
32
|
|
33
|
+
%w{bucket key}.each do |m|
|
34
|
+
class_eval %{ def #{m}=(value); @url = nil; @#{m} = value; end }
|
35
|
+
end
|
36
|
+
|
33
37
|
# @param [String] header_string the string value of the Link: HTTP header from a Riak response
|
34
38
|
# @return [Array<Link>] an array of Riak::Link structs parsed from the header
|
35
39
|
def self.parse(header_string)
|
@@ -56,12 +60,13 @@ module Riak
|
|
56
60
|
|
57
61
|
# @return [String] the URL (relative or absolute) of the related resource
|
58
62
|
def url
|
59
|
-
"/riak/#{escape(bucket)}" + (key.blank? ? "" : "/#{escape(key)}")
|
63
|
+
@url ||= "/riak/#{escape(bucket)}" + (key.blank? ? "" : "/#{escape(key)}")
|
60
64
|
end
|
61
65
|
|
62
66
|
def url=(value)
|
63
|
-
@
|
64
|
-
@
|
67
|
+
@url = value
|
68
|
+
@bucket = unescape($1) if value =~ %r{^/[^/]+/([^/]+)/?}
|
69
|
+
@key = unescape($1) if value =~ %r{^/[^/]+/[^/]+/([^/]+)/?}
|
65
70
|
end
|
66
71
|
|
67
72
|
def inspect; to_s; end
|
data/lib/riak/locale/en.yml
CHANGED
@@ -18,6 +18,8 @@ en:
|
|
18
18
|
content_type_undefined: "content_type is not defined!"
|
19
19
|
empty_map_reduce_query: "Specify one or more query phases to your MapReduce."
|
20
20
|
failed_request: "Expected %{expected} from Riak but received %{code}. %{body}"
|
21
|
+
filter_needs_block: "Filter %{filter} expects a block."
|
22
|
+
filter_arity_mismatch: "Filter %{filter} expects %{expected} arguments but %{received} were given."
|
21
23
|
hash_type: "invalid argument %{hash} is not a Hash"
|
22
24
|
http_configuration: "The %{backend} HTTP backend cannot be used. Please check its requirements."
|
23
25
|
hostname_invalid: "host must be a valid hostname"
|
data/lib/riak/map_reduce.rb
CHANGED
@@ -19,7 +19,12 @@ module Riak
|
|
19
19
|
include Util::Translation
|
20
20
|
include Util::Escape
|
21
21
|
|
22
|
-
|
22
|
+
autoload :Phase, "riak/map_reduce/phase"
|
23
|
+
autoload :FilterBuilder, "riak/map_reduce/filter_builder"
|
24
|
+
|
25
|
+
# @return [Array<[bucket,key]>,String,Hash<:bucket,:filters>] The
|
26
|
+
# bucket/keys for input to the job, or the bucket (all
|
27
|
+
# keys), or a hash containing the bucket and key-filters.
|
23
28
|
# @see #add
|
24
29
|
attr_accessor :inputs
|
25
30
|
|
@@ -52,9 +57,17 @@ module Riak
|
|
52
57
|
# @param [String,Bucket] bucket the bucket of the object
|
53
58
|
# @param [String] key the key of the object
|
54
59
|
# @param [String] keydata extra data to pass along with the object to the job
|
60
|
+
# @overload add(bucket, filters)
|
61
|
+
# Run the job across all keys in the bucket, with the given
|
62
|
+
# key-filters. This will replace any other inputs previously
|
63
|
+
# added. (Requires Riak 0.14)
|
64
|
+
# @param [String,Bucket] bucket the bucket to filter keys from
|
65
|
+
# @param [Array<Array>] filters a list of key-filters to apply
|
66
|
+
# to the key list
|
55
67
|
# @return [MapReduce] self
|
56
68
|
def add(*params)
|
57
|
-
params = params.dup
|
69
|
+
params = params.dup
|
70
|
+
params = params.first if Array === params.first
|
58
71
|
case params.size
|
59
72
|
when 1
|
60
73
|
p = params.first
|
@@ -69,14 +82,28 @@ module Riak
|
|
69
82
|
when 2..3
|
70
83
|
bucket = params.shift
|
71
84
|
bucket = bucket.name if Bucket === bucket
|
72
|
-
|
73
|
-
|
85
|
+
if Array === params.first
|
86
|
+
@inputs = {:bucket => escape(bucket), :filters => params.first }
|
87
|
+
else
|
88
|
+
key = params.shift
|
89
|
+
@inputs << params.unshift(escape(key)).unshift(escape(bucket))
|
90
|
+
end
|
74
91
|
end
|
75
92
|
self
|
76
93
|
end
|
77
94
|
alias :<< :add
|
78
95
|
alias :include :add
|
79
96
|
|
97
|
+
# Adds a bucket and key-filters built by the given
|
98
|
+
# block. Equivalent to #add with a list of filters.
|
99
|
+
# @param [String] bucket the bucket to apply key-filters to
|
100
|
+
# @yield [] builder block - instance_eval'ed into a FilterBuilder
|
101
|
+
# @return [MapReduce] self
|
102
|
+
# @see MapReduce#add
|
103
|
+
def filter(bucket, &block)
|
104
|
+
add(bucket, FilterBuilder.new(&block).to_a)
|
105
|
+
end
|
106
|
+
|
80
107
|
# Add a map phase to the job.
|
81
108
|
# @overload map(function)
|
82
109
|
# @param [String, Array] function a Javascript function that represents the phase, or an Erlang [module,function] pair
|
@@ -144,16 +171,13 @@ module Riak
|
|
144
171
|
end
|
145
172
|
|
146
173
|
# Executes this map-reduce job.
|
147
|
-
# @return [Array<Array>] similar to link-walking, each element is
|
148
|
-
|
174
|
+
# @return [Array<Array>] similar to link-walking, each element is
|
175
|
+
# an array of results from a phase where "keep" is true. If there
|
176
|
+
# is only one "keep" phase, only the results from that phase will
|
177
|
+
# be returned.
|
178
|
+
def run(&block)
|
149
179
|
raise MapReduceError.new(t("empty_map_reduce_query")) if @query.empty?
|
150
|
-
|
151
|
-
begin
|
152
|
-
raise unless response[:headers]['content-type'].include?('application/json')
|
153
|
-
JSON.parse(response[:body])
|
154
|
-
rescue
|
155
|
-
response
|
156
|
-
end
|
180
|
+
@client.backend.mapred(self, &block)
|
157
181
|
rescue FailedRequest => fr
|
158
182
|
if fr.code == 500 && fr.headers['content-type'].include?("application/json")
|
159
183
|
raise MapReduceError.new(fr.body)
|
@@ -161,95 +185,5 @@ module Riak
|
|
161
185
|
raise fr
|
162
186
|
end
|
163
187
|
end
|
164
|
-
|
165
|
-
# Represents an individual phase in a map-reduce pipeline. Generally you'll want to call
|
166
|
-
# methods of MapReduce instead of using this directly.
|
167
|
-
class Phase
|
168
|
-
include Util::Translation
|
169
|
-
# @return [Symbol] the type of phase - :map, :reduce, or :link
|
170
|
-
attr_accessor :type
|
171
|
-
|
172
|
-
# @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.
|
173
|
-
attr_accessor :function
|
174
|
-
|
175
|
-
# @return [String] the language of the phase's function - "javascript" or "erlang". Meaningless for :link type phases.
|
176
|
-
attr_accessor :language
|
177
|
-
|
178
|
-
# @return [Boolean] whether results of this phase will be returned
|
179
|
-
attr_accessor :keep
|
180
|
-
|
181
|
-
# @return [Array] any extra static arguments to pass to the phase
|
182
|
-
attr_accessor :arg
|
183
|
-
|
184
|
-
# Creates a phase in the map-reduce pipeline
|
185
|
-
# @param [Hash] options options for the phase
|
186
|
-
# @option options [Symbol] :type one of :map, :reduce, :link
|
187
|
-
# @option options [String] :language ("javascript") "erlang" or "javascript"
|
188
|
-
# @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.
|
189
|
-
# @option options [Boolean] :keep (false) whether to return the results of this phase
|
190
|
-
# @option options [Array] :arg (nil) any extra static arguments to pass to the phase
|
191
|
-
def initialize(options={})
|
192
|
-
self.type = options[:type]
|
193
|
-
self.language = options[:language] || "javascript"
|
194
|
-
self.function = options[:function]
|
195
|
-
self.keep = options[:keep] || false
|
196
|
-
self.arg = options[:arg]
|
197
|
-
end
|
198
|
-
|
199
|
-
def type=(value)
|
200
|
-
raise ArgumentError, t("invalid_phase_type") unless value.to_s =~ /^(map|reduce|link)$/i
|
201
|
-
@type = value.to_s.downcase.to_sym
|
202
|
-
end
|
203
|
-
|
204
|
-
def function=(value)
|
205
|
-
case value
|
206
|
-
when Array
|
207
|
-
raise ArgumentError, t("module_function_pair_required") unless value.size == 2
|
208
|
-
@language = "erlang"
|
209
|
-
when Hash
|
210
|
-
raise ArgumentError, t("stored_function_invalid") unless type == :link || value.has_key?(:bucket) && value.has_key?(:key)
|
211
|
-
@language = "javascript"
|
212
|
-
when String
|
213
|
-
@language = "javascript"
|
214
|
-
when WalkSpec
|
215
|
-
raise ArgumentError, t("walk_spec_invalid_unless_link") unless type == :link
|
216
|
-
else
|
217
|
-
raise ArgumentError, t("invalid_function_value", :value => value.inspect)
|
218
|
-
end
|
219
|
-
@function = value
|
220
|
-
end
|
221
|
-
|
222
|
-
# Converts the phase to JSON for use while invoking a job.
|
223
|
-
# @return [String] a JSON representation of the phase
|
224
|
-
def to_json(*a)
|
225
|
-
as_json.to_json(*a)
|
226
|
-
end
|
227
|
-
|
228
|
-
# Converts the phase to its JSON-compatible representation for job invocation.
|
229
|
-
# @return [Hash] a Hash-equivalent of the phase
|
230
|
-
def as_json(options=nil)
|
231
|
-
obj = case type
|
232
|
-
when :map, :reduce
|
233
|
-
defaults = {"language" => language, "keep" => keep}
|
234
|
-
case function
|
235
|
-
when Hash
|
236
|
-
defaults.merge(function)
|
237
|
-
when String
|
238
|
-
if function =~ /\s*function/
|
239
|
-
defaults.merge("source" => function)
|
240
|
-
else
|
241
|
-
defaults.merge("name" => function)
|
242
|
-
end
|
243
|
-
when Array
|
244
|
-
defaults.merge("module" => function[0], "function" => function[1])
|
245
|
-
end
|
246
|
-
when :link
|
247
|
-
spec = WalkSpec.normalize(function).first
|
248
|
-
{"bucket" => spec.bucket, "tag" => spec.tag, "keep" => spec.keep || keep}
|
249
|
-
end
|
250
|
-
obj["arg"] = arg if arg
|
251
|
-
{ type => obj }
|
252
|
-
end
|
253
|
-
end
|
254
188
|
end
|
255
189
|
end
|
@@ -0,0 +1,106 @@
|
|
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 MapReduce
|
18
|
+
# Builds key-filter lists for MapReduce inputs in a DSL-like fashion.
|
19
|
+
class FilterBuilder
|
20
|
+
include Util::Translation
|
21
|
+
|
22
|
+
# Known filters available in riak_kv_mapred_filters, mapped to
|
23
|
+
# their arities. These are turned into instance methods.
|
24
|
+
# Example:
|
25
|
+
#
|
26
|
+
# FilterBuilder.new do
|
27
|
+
# string_to_int
|
28
|
+
# less_than 50
|
29
|
+
# end
|
30
|
+
FILTERS = {
|
31
|
+
:int_to_string => 0,
|
32
|
+
:string_to_int => 0,
|
33
|
+
:float_to_string => 0,
|
34
|
+
:string_to_float => 0,
|
35
|
+
:to_upper => 0,
|
36
|
+
:to_lower => 0,
|
37
|
+
:tokenize => 2,
|
38
|
+
:urldecode => 0,
|
39
|
+
:greater_than => 1,
|
40
|
+
:less_than => 1,
|
41
|
+
:greater_than_eq => 1,
|
42
|
+
:less_than_eq => 1,
|
43
|
+
:between => [2,3],
|
44
|
+
:matches => 1,
|
45
|
+
:neq => 1,
|
46
|
+
:eq => 1,
|
47
|
+
:set_member => 1,
|
48
|
+
:similar_to => 2,
|
49
|
+
:starts_with => 1,
|
50
|
+
:ends_with => 1
|
51
|
+
}
|
52
|
+
|
53
|
+
# Available logical operations for joining filter chains. These
|
54
|
+
# are turned into instance methods with leading underscores,
|
55
|
+
# with aliases to uppercase versions.
|
56
|
+
# Example:
|
57
|
+
#
|
58
|
+
# FilterBuilder.new do
|
59
|
+
# string_to_int
|
60
|
+
# AND do
|
61
|
+
# greater_than_eq 50
|
62
|
+
# neq 100
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
LOGICAL_OPERATIONS = %w{and or not}
|
66
|
+
|
67
|
+
FILTERS.each do |f,arity|
|
68
|
+
class_eval <<-CODE
|
69
|
+
def #{f}(*args)
|
70
|
+
raise ArgumentError.new(t("filter_arity_mismatch", :filter => :#{f}, :expected => #{arity.inspect}, :received => args.size)) unless Array(#{arity.inspect}).include?(args.size)
|
71
|
+
@filters << ([:#{f}] + args)
|
72
|
+
end
|
73
|
+
CODE
|
74
|
+
end
|
75
|
+
|
76
|
+
LOGICAL_OPERATIONS.each do |op|
|
77
|
+
class_eval <<-CODE
|
78
|
+
def _#{op}(&block)
|
79
|
+
raise ArgumentError.new(t('filter_needs_block', :filter => '#{op}')) unless block_given?
|
80
|
+
@filters << [:#{op}, self.class.new(&block).to_a]
|
81
|
+
end
|
82
|
+
alias :#{op.to_s.upcase} :_#{op}
|
83
|
+
CODE
|
84
|
+
end
|
85
|
+
|
86
|
+
# Creates a new FilterBuilder. Pass a block that will be
|
87
|
+
# instance_eval'ed to construct the sequence of filters.
|
88
|
+
def initialize(&block)
|
89
|
+
@filters = []
|
90
|
+
instance_eval(&block) if block_given?
|
91
|
+
end
|
92
|
+
|
93
|
+
# Wraps multi-step filters for use inside logical
|
94
|
+
# operations. Does not correspond to an actual filter.
|
95
|
+
def sequence(&block)
|
96
|
+
@filters << self.class.new(&block).to_a
|
97
|
+
end
|
98
|
+
alias :seq :sequence
|
99
|
+
|
100
|
+
# @return A list of filters for handing to the MapReduce inputs.
|
101
|
+
def to_a
|
102
|
+
@filters
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,108 @@
|
|
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 MapReduce
|
18
|
+
# Represents an individual phase in a map-reduce pipeline. Generally you'll want to call
|
19
|
+
# methods of MapReduce instead of using this directly.
|
20
|
+
class Phase
|
21
|
+
include Util::Translation
|
22
|
+
# @return [Symbol] the type of phase - :map, :reduce, or :link
|
23
|
+
attr_accessor :type
|
24
|
+
|
25
|
+
# @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.
|
26
|
+
attr_accessor :function
|
27
|
+
|
28
|
+
# @return [String] the language of the phase's function - "javascript" or "erlang". Meaningless for :link type phases.
|
29
|
+
attr_accessor :language
|
30
|
+
|
31
|
+
# @return [Boolean] whether results of this phase will be returned
|
32
|
+
attr_accessor :keep
|
33
|
+
|
34
|
+
# @return [Array] any extra static arguments to pass to the phase
|
35
|
+
attr_accessor :arg
|
36
|
+
|
37
|
+
# Creates a phase in the map-reduce pipeline
|
38
|
+
# @param [Hash] options options for the phase
|
39
|
+
# @option options [Symbol] :type one of :map, :reduce, :link
|
40
|
+
# @option options [String] :language ("javascript") "erlang" or "javascript"
|
41
|
+
# @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.
|
42
|
+
# @option options [Boolean] :keep (false) whether to return the results of this phase
|
43
|
+
# @option options [Array] :arg (nil) any extra static arguments to pass to the phase
|
44
|
+
def initialize(options={})
|
45
|
+
self.type = options[:type]
|
46
|
+
self.language = options[:language] || "javascript"
|
47
|
+
self.function = options[:function]
|
48
|
+
self.keep = options[:keep] || false
|
49
|
+
self.arg = options[:arg]
|
50
|
+
end
|
51
|
+
|
52
|
+
def type=(value)
|
53
|
+
raise ArgumentError, t("invalid_phase_type") unless value.to_s =~ /^(map|reduce|link)$/i
|
54
|
+
@type = value.to_s.downcase.to_sym
|
55
|
+
end
|
56
|
+
|
57
|
+
def function=(value)
|
58
|
+
case value
|
59
|
+
when Array
|
60
|
+
raise ArgumentError, t("module_function_pair_required") unless value.size == 2
|
61
|
+
@language = "erlang"
|
62
|
+
when Hash
|
63
|
+
raise ArgumentError, t("stored_function_invalid") unless type == :link || value.has_key?(:bucket) && value.has_key?(:key)
|
64
|
+
@language = "javascript"
|
65
|
+
when String
|
66
|
+
@language = "javascript"
|
67
|
+
when WalkSpec
|
68
|
+
raise ArgumentError, t("walk_spec_invalid_unless_link") unless type == :link
|
69
|
+
else
|
70
|
+
raise ArgumentError, t("invalid_function_value", :value => value.inspect)
|
71
|
+
end
|
72
|
+
@function = value
|
73
|
+
end
|
74
|
+
|
75
|
+
# Converts the phase to JSON for use while invoking a job.
|
76
|
+
# @return [String] a JSON representation of the phase
|
77
|
+
def to_json(*a)
|
78
|
+
as_json.to_json(*a)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Converts the phase to its JSON-compatible representation for job invocation.
|
82
|
+
# @return [Hash] a Hash-equivalent of the phase
|
83
|
+
def as_json(options=nil)
|
84
|
+
obj = case type
|
85
|
+
when :map, :reduce
|
86
|
+
defaults = {"language" => language, "keep" => keep}
|
87
|
+
case function
|
88
|
+
when Hash
|
89
|
+
defaults.merge(function)
|
90
|
+
when String
|
91
|
+
if function =~ /\s*function/
|
92
|
+
defaults.merge("source" => function)
|
93
|
+
else
|
94
|
+
defaults.merge("name" => function)
|
95
|
+
end
|
96
|
+
when Array
|
97
|
+
defaults.merge("module" => function[0], "function" => function[1])
|
98
|
+
end
|
99
|
+
when :link
|
100
|
+
spec = WalkSpec.normalize(function).first
|
101
|
+
{"bucket" => spec.bucket, "tag" => spec.tag, "keep" => spec.keep || keep}
|
102
|
+
end
|
103
|
+
obj["arg"] = arg if arg
|
104
|
+
{ type => obj }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/riak/robject.rb
CHANGED
@@ -13,14 +13,15 @@
|
|
13
13
|
# limitations under the License.
|
14
14
|
require 'riak'
|
15
15
|
require 'set'
|
16
|
+
require 'time'
|
16
17
|
|
17
18
|
module Riak
|
18
19
|
# Represents the data and metadata stored in a bucket/key pair in
|
19
|
-
# the Riak database, the base unit of data manipulation.
|
20
|
+
# the Riak database, the base unit of data manipulation.
|
20
21
|
class RObject
|
21
|
-
include Util
|
22
22
|
include Util::Translation
|
23
23
|
include Util::Escape
|
24
|
+
extend Util::Escape
|
24
25
|
|
25
26
|
# @return [Bucket] the bucket in which this object is contained
|
26
27
|
attr_accessor :bucket
|
@@ -57,7 +58,7 @@ module Riak
|
|
57
58
|
# @return [Array<RObject>] An array of RObject instances
|
58
59
|
def self.load_from_mapreduce(client, response)
|
59
60
|
response.map do |item|
|
60
|
-
RObject.new(client[
|
61
|
+
RObject.new(client[unescape(item['bucket'])], unescape(item['key'])).load_from_mapreduce(item)
|
61
62
|
end
|
62
63
|
end
|
63
64
|
|
@@ -72,27 +73,6 @@ module Riak
|
|
72
73
|
yield self if block_given?
|
73
74
|
end
|
74
75
|
|
75
|
-
# Load object data from an HTTP response
|
76
|
-
# @param [Hash] response a response from {Riak::Client::HTTPBackend}
|
77
|
-
def load(response)
|
78
|
-
extract_header(response, "location", :key) {|v| URI.unescape(v.split("/").last) }
|
79
|
-
extract_header(response, "content-type", :content_type)
|
80
|
-
extract_header(response, "x-riak-vclock", :vclock)
|
81
|
-
extract_header(response, "link", :links) {|v| Set.new(Link.parse(v)) }
|
82
|
-
extract_header(response, "etag", :etag)
|
83
|
-
extract_header(response, "last-modified", :last_modified) {|v| Time.httpdate(v) }
|
84
|
-
@meta = response[:headers].inject({}) do |h,(k,v)|
|
85
|
-
if k =~ /x-riak-meta-(.*)/
|
86
|
-
h[$1] = v
|
87
|
-
end
|
88
|
-
h
|
89
|
-
end
|
90
|
-
@conflict = (response[:code].to_i == 300 && content_type =~ /multipart\/mixed/) rescue false
|
91
|
-
@siblings = nil
|
92
|
-
self.raw_data = response[:body] if response[:body].present?
|
93
|
-
self
|
94
|
-
end
|
95
|
-
|
96
76
|
# Load object data from a map/reduce response item.
|
97
77
|
# This method is used by RObject::load_from_mapreduce to instantiate the necessary
|
98
78
|
# objects.
|
@@ -115,7 +95,7 @@ module Riak
|
|
115
95
|
self
|
116
96
|
end
|
117
97
|
|
118
|
-
# @return [Object] the unmarshaled form of {#raw_data} stored in riak at this object's key
|
98
|
+
# @return [Object] the unmarshaled form of {#raw_data} stored in riak at this object's key
|
119
99
|
def data
|
120
100
|
if @raw_data && !@data
|
121
101
|
@data = deserialize(@raw_data)
|
@@ -147,37 +127,6 @@ module Riak
|
|
147
127
|
@raw_data = new_raw_data
|
148
128
|
end
|
149
129
|
|
150
|
-
# HTTP header hash that will be sent along when storing the object
|
151
|
-
# @return [Hash] hash of HTTP Headers
|
152
|
-
def store_headers
|
153
|
-
{}.tap do |hash|
|
154
|
-
hash["Content-Type"] = @content_type
|
155
|
-
hash["X-Riak-Vclock"] = @vclock if @vclock
|
156
|
-
if @prevent_stale_writes && @etag.present?
|
157
|
-
hash["If-Match"] = @etag
|
158
|
-
elsif @prevent_stale_writes
|
159
|
-
hash["If-None-Match"] = "*"
|
160
|
-
end
|
161
|
-
unless @links.blank?
|
162
|
-
hash["Link"] = @links.reject {|l| l.rel == "up" }.map(&:to_s).join(", ")
|
163
|
-
end
|
164
|
-
unless @meta.blank?
|
165
|
-
@meta.each do |k,v|
|
166
|
-
hash["X-Riak-Meta-#{k}"] = v.to_s
|
167
|
-
end
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
# HTTP header hash that will be sent along when reloading the object
|
173
|
-
# @return [Hash] hash of HTTP headers
|
174
|
-
def reload_headers
|
175
|
-
{}.tap do |h|
|
176
|
-
h['If-None-Match'] = @etag if @etag.present?
|
177
|
-
h['If-Modified-Since'] = @last_modified.httpdate if @last_modified.present?
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
130
|
# Store the object in Riak
|
182
131
|
# @param [Hash] options query parameters
|
183
132
|
# @option options [Fixnum] :r the "r" parameter (Read quorum for the implicit read performed when validating the store operation)
|
@@ -189,23 +138,22 @@ module Riak
|
|
189
138
|
def store(options={})
|
190
139
|
raise ArgumentError, t("content_type_undefined") unless @content_type.present?
|
191
140
|
params = {:returnbody => true}.merge(options)
|
192
|
-
|
193
|
-
|
194
|
-
load(response)
|
141
|
+
@bucket.client.backend.store_object(self, params[:returnbody], params[:w], params[:dw])
|
142
|
+
self
|
195
143
|
end
|
196
144
|
|
197
145
|
# Reload the object from Riak. Will use conditional GETs when possible.
|
198
146
|
# @param [Hash] options query parameters
|
199
147
|
# @option options [Fixnum] :r the "r" parameter (Read quorum)
|
200
|
-
# @option options [Boolean] :force will force a reload request if
|
148
|
+
# @option options [Boolean] :force will force a reload request if
|
149
|
+
# the vclock is not present, useful for reloading the object after
|
150
|
+
# a store (not passed in the query params)
|
201
151
|
# @return [Riak::RObject] self
|
202
152
|
def reload(options={})
|
203
153
|
force = options.delete(:force)
|
204
154
|
return self unless @key && (@vclock || force)
|
205
|
-
|
206
|
-
|
207
|
-
load(response) unless response[:code] == 304
|
208
|
-
self
|
155
|
+
self.etag = self.last_modified = nil if force
|
156
|
+
bucket.client.backend.reload_object(self, options[:r])
|
209
157
|
end
|
210
158
|
|
211
159
|
alias :fetch :reload
|
@@ -218,17 +166,14 @@ module Riak
|
|
218
166
|
freeze
|
219
167
|
end
|
220
168
|
|
169
|
+
attr_writer :siblings, :conflict
|
170
|
+
|
221
171
|
# Returns sibling objects when in conflict.
|
222
172
|
# @return [Array<RObject>] an array of conflicting sibling objects for this key
|
223
173
|
# @return [self] this object when not in conflict
|
224
174
|
def siblings
|
225
175
|
return self unless conflict?
|
226
|
-
@siblings
|
227
|
-
RObject.new(self.bucket, self.key) do |sibling|
|
228
|
-
sibling.load(part)
|
229
|
-
sibling.vclock = vclock
|
230
|
-
end
|
231
|
-
end
|
176
|
+
@siblings
|
232
177
|
end
|
233
178
|
|
234
179
|
# @return [true,false] Whether this object has conflicting sibling objects (divergent vclocks)
|
@@ -286,21 +231,14 @@ module Riak
|
|
286
231
|
else
|
287
232
|
@raw_data && "(#{@raw_data.size} bytes)"
|
288
233
|
end
|
289
|
-
"#<#{self.class.name} #{
|
234
|
+
"#<#{self.class.name} {#{bucket.name}#{"," + @key if @key}} [#{@content_type}]:#{body}>"
|
290
235
|
end
|
291
236
|
|
292
237
|
# Walks links from this object to other objects in Riak.
|
293
238
|
# @param [Array<Hash,WalkSpec>] link specifications for the query
|
294
239
|
def walk(*params)
|
295
240
|
specs = WalkSpec.normalize(*params)
|
296
|
-
|
297
|
-
if boundary = Multipart.extract_boundary(response[:headers]['content-type'].first)
|
298
|
-
Multipart.parse(response[:body], boundary).map do |group|
|
299
|
-
map_walk_group(group)
|
300
|
-
end
|
301
|
-
else
|
302
|
-
[]
|
303
|
-
end
|
241
|
+
@bucket.client.http.link_walk(self, specs)
|
304
242
|
end
|
305
243
|
|
306
244
|
# Converts the object to a link suitable for linking other objects
|
@@ -322,14 +260,14 @@ module Riak
|
|
322
260
|
alias :vector_clock :vclock
|
323
261
|
alias :vector_clock= :vclock=
|
324
262
|
|
325
|
-
|
263
|
+
protected
|
326
264
|
def load_map_reduce_value(hash)
|
327
265
|
metadata = hash['metadata']
|
328
266
|
extract_if_present(metadata, 'X-Riak-VTag', :etag)
|
329
267
|
extract_if_present(metadata, 'content-type', :content_type)
|
330
268
|
extract_if_present(metadata, 'X-Riak-Last-Modified', :last_modified) { |v| Time.httpdate( v ) }
|
331
269
|
extract_if_present(metadata, 'Links', :links) do |links|
|
332
|
-
Set.new( links.map { |l| Link.new(
|
270
|
+
Set.new( links.map { |l| Link.new(*l) } )
|
333
271
|
end
|
334
272
|
extract_if_present(metadata, 'X-Riak-Meta', :meta) do |meta|
|
335
273
|
Hash[
|
@@ -349,20 +287,5 @@ module Riak
|
|
349
287
|
send("#{attribute}=", value)
|
350
288
|
end
|
351
289
|
end
|
352
|
-
|
353
|
-
def extract_header(response, name, attribute=nil, &block)
|
354
|
-
extract_if_present(response[:headers], name, attribute) do |value|
|
355
|
-
block ? block.call(value[0]) : value[0]
|
356
|
-
end
|
357
|
-
end
|
358
|
-
|
359
|
-
def map_walk_group(group)
|
360
|
-
group.map do |obj|
|
361
|
-
if obj[:headers] && obj[:body] && obj[:headers]['location']
|
362
|
-
bucket, key = $1, $2 if obj[:headers]['location'].first =~ %r{/.*/(.*)/(.*)$}
|
363
|
-
RObject.new(@bucket.client.bucket(bucket, :keys => false), key).load(obj)
|
364
|
-
end
|
365
|
-
end
|
366
|
-
end
|
367
290
|
end
|
368
291
|
end
|