riak-client 1.0.5 → 1.1.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 (56) hide show
  1. data/.document +5 -0
  2. data/.gitignore +4 -3
  3. data/.rspec +1 -0
  4. data/Gemfile +1 -0
  5. data/RELEASE_NOTES.md +47 -0
  6. data/Rakefile +0 -1
  7. data/erl_src/riak_kv_test_backend.erl +34 -0
  8. data/lib/riak/client.rb +3 -1
  9. data/lib/riak/client/beefcake/messages.rb +49 -1
  10. data/lib/riak/client/beefcake/object_methods.rb +14 -21
  11. data/lib/riak/client/beefcake_protobuffs_backend.rb +58 -12
  12. data/lib/riak/client/decaying.rb +31 -23
  13. data/lib/riak/client/feature_detection.rb +88 -0
  14. data/lib/riak/client/http_backend.rb +27 -6
  15. data/lib/riak/client/http_backend/configuration.rb +13 -0
  16. data/lib/riak/client/http_backend/object_methods.rb +33 -25
  17. data/lib/riak/client/node.rb +7 -2
  18. data/lib/riak/client/protobuffs_backend.rb +54 -3
  19. data/lib/riak/client/search.rb +2 -2
  20. data/lib/riak/conflict.rb +13 -0
  21. data/lib/riak/locale/en.yml +2 -0
  22. data/lib/riak/map_reduce.rb +1 -1
  23. data/lib/riak/map_reduce/filter_builder.rb +2 -2
  24. data/lib/riak/map_reduce/results.rb +49 -0
  25. data/lib/riak/node/console.rb +17 -16
  26. data/lib/riak/node/generation.rb +9 -0
  27. data/lib/riak/rcontent.rb +168 -0
  28. data/lib/riak/robject.rb +37 -157
  29. data/lib/riak/util/escape.rb +5 -1
  30. data/lib/riak/version.rb +1 -1
  31. data/riak-client.gemspec +37 -5
  32. data/spec/fixtures/multipart-basic-conflict.txt +15 -0
  33. data/spec/fixtures/munchausen.txt +1033 -0
  34. data/spec/integration/riak/cluster_spec.rb +1 -1
  35. data/spec/integration/riak/http_backends_spec.rb +23 -2
  36. data/spec/integration/riak/node_spec.rb +2 -2
  37. data/spec/integration/riak/protobuffs_backends_spec.rb +17 -2
  38. data/spec/integration/riak/test_server_spec.rb +1 -1
  39. data/spec/integration/riak/threading_spec.rb +3 -3
  40. data/spec/riak/beefcake_protobuffs_backend_spec.rb +58 -25
  41. data/spec/riak/escape_spec.rb +3 -0
  42. data/spec/riak/feature_detection_spec.rb +61 -0
  43. data/spec/riak/http_backend/object_methods_spec.rb +4 -13
  44. data/spec/riak/http_backend_spec.rb +6 -5
  45. data/spec/riak/map_reduce_spec.rb +0 -5
  46. data/spec/riak/robject_spec.rb +12 -11
  47. data/spec/spec_helper.rb +3 -1
  48. data/spec/support/riak_test.rb +77 -0
  49. data/spec/support/search_corpus_setup.rb +18 -0
  50. data/spec/support/sometimes.rb +1 -1
  51. data/spec/support/test_server.rb +1 -1
  52. data/spec/support/unified_backend_examples.rb +53 -7
  53. data/spec/support/version_filter.rb +4 -11
  54. metadata +56 -22
  55. data/lib/riak/client/pool.rb +0 -180
  56. data/spec/riak/pool_spec.rb +0 -306
@@ -0,0 +1,49 @@
1
+ module Riak
2
+ class MapReduce
3
+ # @api private
4
+ # Collects and normalizes results from MapReduce requests
5
+ class Results
6
+ # Creates a new result collector
7
+ # @param [MapReduce] mr the MapReduce query
8
+ def initialize(mr)
9
+ @keep_count = mr.query.select {|p| p.keep }.size
10
+ @hash = create_results_hash(mr.query)
11
+ end
12
+
13
+ # Adds a new result to the collector
14
+ # @param [Fixnum] phase the phase index
15
+ # @param [Array] data the phase result
16
+ def add(phase, result)
17
+ @hash[phase] += result
18
+ end
19
+
20
+ # Coalesces the query results
21
+ # @return [Array] the query results, coalesced according to the
22
+ # phase configuration
23
+ def report
24
+ if @keep_count > 1
25
+ @hash.to_a.sort.map {|(num, results)| results }
26
+ else
27
+ @hash[@hash.keys.first]
28
+ end
29
+ end
30
+
31
+ private
32
+ def create_results_hash(query)
33
+ # When the query is empty, only bucket/key pairs are returned,
34
+ # but implicitly in phase 0.
35
+ return { 0 => [] } if query.empty?
36
+
37
+ # Pre-populate the hash with empty results for kept phases.
38
+ # Additionally, the last phase is always implictly kept, even
39
+ # when keep is false.
40
+ query.inject({}) do |hash, phase|
41
+ if phase.keep || query[-1] == phase
42
+ hash[query.index(phase)] = []
43
+ end
44
+ hash
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -46,15 +46,25 @@ module Riak
46
46
  end
47
47
  end
48
48
  raise ArgumentError, t('no_pipes', :path => pipedir.to_s) if [@rfile, @wfile].any? {|p| p.nil? }
49
- # We have to open the read pipe AFTER we have sent some data
50
- # to the write pipe, otherwise JRuby hangs.
49
+
51
50
  begin
52
51
  debug "Opening write pipe."
53
52
  @w = open_write_pipe
54
53
  @w.sync = true
55
- debug "Opening read pipe."
56
- @r = open_read_pipe
57
- command 'ok.'
54
+ read_ready = false
55
+ # Using threads, we get around JRuby's blocking-open
56
+ # behavior. The main thread pumps the write pipe full of
57
+ # data so that run_erl will start echoing it into the read
58
+ # pipe once we have started the open. On non-JVM rubies,
59
+ # O_NONBLOCK works and we proceed as expected.
60
+ Thread.new do
61
+ debug "Opening read pipe."
62
+ @r = open_read_pipe
63
+ read_ready = true
64
+ end
65
+ Thread.pass
66
+ @w.print "\n" until read_ready
67
+ command "ok."
58
68
  debug "Initialized console: #{@r.inspect} #{@w.inspect}"
59
69
  rescue => e
60
70
  debug e.message
@@ -102,7 +112,6 @@ module Riak
102
112
  def close
103
113
  @r.close if @r && !@r.closed?
104
114
  @w.close if @w && !@w.closed?
105
- Signal.trap("WINCH", @winch)
106
115
  freeze
107
116
  end
108
117
 
@@ -113,19 +122,11 @@ module Riak
113
122
  end
114
123
 
115
124
  def open_write_pipe
116
- if defined?(::Java)
117
- java.io.FileOutputStream.new(@wfile.to_s).to_io
118
- else
119
- @wfile.open(File::WRONLY|File::NONBLOCK)
120
- end
125
+ @wfile.open(File::WRONLY|File::NONBLOCK)
121
126
  end
122
127
 
123
128
  def open_read_pipe
124
- if defined?(::Java)
125
- IO.popen("cat #{@rfile}", "rb")
126
- else
127
- @rfile.open(File::RDONLY|File::NONBLOCK)
128
- end
129
+ @rfile.open(File::RDONLY|File::NONBLOCK)
129
130
  end
130
131
  end
131
132
  end
@@ -16,6 +16,7 @@ module Riak
16
16
  # Generates the node.
17
17
  def create
18
18
  unless exist?
19
+ touch_ssl_distribution_args
19
20
  create_directories
20
21
  write_scripts
21
22
  write_vm_args
@@ -102,5 +103,13 @@ module Riak
102
103
  @configuration[:vm] = @vm
103
104
  manifest.open('w') {|f| YAML.dump(@configuration, f) }
104
105
  end
106
+
107
+ def touch_ssl_distribution_args
108
+ # To make sure that the ssl_distribution.args file is present,
109
+ # the control script in the source node has to have been run at
110
+ # least once. Running the `chkconfig` command is innocuous
111
+ # enough to accomplish this without other side-effects.
112
+ `#{(source + control_script.basename).to_s} chkconfig`
113
+ end
105
114
  end
106
115
  end
@@ -0,0 +1,168 @@
1
+ require 'set'
2
+ require 'time'
3
+ require 'yaml'
4
+ require 'forwardable'
5
+ require 'riak/util/translation'
6
+ require 'riak/serializers'
7
+
8
+ module Riak
9
+ # Represents single (potentially-conflicted) value stored against a
10
+ # key in the Riak database. This includes the raw value as well as
11
+ # metadata.
12
+ # @since 1.1.0
13
+ class RContent
14
+ extend Forwardable
15
+ include Util::Translation
16
+
17
+ # @return [String] the MIME content type of the value
18
+ attr_accessor :content_type
19
+
20
+ # @return [Set<Link>] a Set of {Riak::Link} objects for relationships between this object and other resources
21
+ attr_accessor :links
22
+
23
+ # @return [String] the ETag header from the most recent HTTP response, useful for caching and reloading
24
+ attr_accessor :etag
25
+
26
+ # @return [Time] the Last-Modified header from the most recent HTTP response, useful for caching and reloading
27
+ attr_accessor :last_modified
28
+
29
+ # @return [Hash] a hash of any X-Riak-Meta-* headers that were in the HTTP response, keyed on the trailing portion
30
+ attr_accessor :meta
31
+
32
+ # @return [Hash<Set>] a hash of secondary indexes, where the
33
+ # key is the index name and the value is a Set of index
34
+ # entries for that index
35
+ attr_accessor :indexes
36
+
37
+ # @return [Riak::RObject] the RObject to which this sibling belongs
38
+ attr_accessor :robject
39
+
40
+ def_delegators :robject, :bucket, :key, :vclock
41
+
42
+ # Creates a new object value. This should not normally need to be
43
+ # called by users of the client. Normal, single-value use can rely
44
+ # on the delegating accessors on {Riak::RObject}.
45
+ # @param [RObject] robject the object that this value belongs to
46
+ # @yield self the new RContent
47
+ def initialize(robject)
48
+ @robject = robject
49
+ @links, @meta = Set.new, {}
50
+ @indexes = new_index_hash
51
+ yield self if block_given?
52
+ end
53
+
54
+ def indexes=(hash)
55
+ @indexes = hash.inject(new_index_hash) do |h, (k,v)|
56
+ h[k].merge([*v])
57
+ h
58
+ end
59
+ end
60
+
61
+ # @return [Object] the unmarshaled form of {#raw_data} stored in riak at this object's key
62
+ def data
63
+ if @raw_data && !@data
64
+ raw = @raw_data.respond_to?(:read) ? @raw_data.read : @raw_data
65
+ @data = deserialize(raw)
66
+ @raw_data = nil
67
+ end
68
+ @data
69
+ end
70
+
71
+ # @param [Object] unmarshaled form of the data to be stored in riak. Object will be serialized using {#serialize} if a known content_type is used. Setting this overrides values stored with {#raw_data=}
72
+ # @return [Object] the object stored
73
+ def data=(new_data)
74
+ if new_data.respond_to?(:read)
75
+ raise ArgumentError.new(t("invalid_io_object"))
76
+ end
77
+
78
+ @raw_data = nil
79
+ @data = new_data
80
+ end
81
+
82
+
83
+ # @return [String] raw data stored in riak for this object's key
84
+ def raw_data
85
+ if @data && !@raw_data
86
+ @raw_data = serialize(@data)
87
+ @data = nil
88
+ end
89
+ @raw_data
90
+ end
91
+
92
+ # @param [String, IO-like] the raw data to be stored in riak at this key, will not be marshaled or manipulated prior to storage. Overrides any data stored by {#data=}
93
+ # @return [String] the data stored
94
+ def raw_data=(new_raw_data)
95
+ @data = nil
96
+ @raw_data = new_raw_data
97
+ end
98
+
99
+ # Serializes the internal object data for sending to Riak. Differs based on the content-type.
100
+ # This method is called internally when storing the object.
101
+ # Automatically serialized formats:
102
+ # * JSON (application/json)
103
+ # * YAML (text/yaml)
104
+ # * Marshal (application/x-ruby-marshal)
105
+ # When given an IO-like object (e.g. File), no serialization will
106
+ # be done.
107
+ # @param [Object] payload the data to serialize
108
+ def serialize(payload)
109
+ Serializers.serialize(@content_type, payload)
110
+ end
111
+
112
+ # Deserializes the internal object data from a Riak response. Differs based on the content-type.
113
+ # This method is called internally when loading the object.
114
+ # Automatically deserialized formats:
115
+ # * JSON (application/json)
116
+ # * YAML (text/yaml)
117
+ # * Marshal (application/x-ruby-marshal)
118
+ # @param [String] body the serialized response body
119
+ def deserialize(body)
120
+ Serializers.deserialize(@content_type, body)
121
+ end
122
+
123
+ # @return [String] A representation suitable for IRB and debugging output
124
+ def inspect
125
+ body = if @data || Serializers[content_type]
126
+ data.inspect
127
+ else
128
+ @raw_data && "(#{@raw_data.size} bytes)"
129
+ end
130
+ "#<#{self.class.name} [#{@content_type}]:#{body}>"
131
+ end
132
+
133
+ # @hidden
134
+ def load_map_reduce_value(hash)
135
+ metadata = hash['metadata']
136
+ extract_if_present(metadata, 'X-Riak-VTag', :etag)
137
+ extract_if_present(metadata, 'content-type', :content_type)
138
+ extract_if_present(metadata, 'X-Riak-Last-Modified', :last_modified) { |v| Time.httpdate( v ) }
139
+ extract_if_present(metadata, 'index', :indexes) do |entries|
140
+ Hash[ entries.map {|k,v| [k, Set.new(Array(v))] } ]
141
+ end
142
+ extract_if_present(metadata, 'Links', :links) do |links|
143
+ Set.new( links.map { |l| Link.new(*l) } )
144
+ end
145
+ extract_if_present(metadata, 'X-Riak-Meta', :meta) do |meta|
146
+ Hash[
147
+ meta.map do |k,v|
148
+ [k.sub(%r{^x-riak-meta-}i, ''), [v]]
149
+ end
150
+ ]
151
+ end
152
+ extract_if_present(hash, 'data', :data) { |v| deserialize(v) }
153
+ end
154
+
155
+ private
156
+ def extract_if_present(hash, key, attribute=nil)
157
+ if hash[key].present?
158
+ attribute ||= key
159
+ value = block_given? ? yield(hash[key]) : hash[key]
160
+ send("#{attribute}=", value)
161
+ end
162
+ end
163
+
164
+ def new_index_hash
165
+ Hash.new {|h,k| h[k] = Set.new }
166
+ end
167
+ end
168
+ end
data/lib/riak/robject.rb CHANGED
@@ -1,12 +1,11 @@
1
- require 'set'
2
- require 'time'
3
- require 'yaml'
1
+ require 'forwardable'
2
+ require 'riak/rcontent'
3
+ require 'riak/conflict'
4
4
  require 'riak/util/translation'
5
5
  require 'riak/util/escape'
6
6
  require 'riak/bucket'
7
7
  require 'riak/link'
8
8
  require 'riak/walk_spec'
9
- require 'riak/serializers'
10
9
 
11
10
  module Riak
12
11
  # Represents the data and metadata stored in a bucket/key pair in
@@ -16,6 +15,7 @@ module Riak
16
15
  extend Util::Translation
17
16
  include Util::Escape
18
17
  extend Util::Escape
18
+ extend Forwardable
19
19
 
20
20
  # @return [Bucket] the bucket in which this object is contained
21
21
  attr_accessor :bucket
@@ -23,28 +23,11 @@ module Riak
23
23
  # @return [String] the key of this object within its bucket
24
24
  attr_accessor :key
25
25
 
26
- # @return [String] the MIME content type of the object
27
- attr_accessor :content_type
28
-
29
26
  # @return [String] the Riak vector clock for the object
30
27
  attr_accessor :vclock
31
28
 
32
- # @return [Set<Link>] a Set of {Riak::Link} objects for relationships between this object and other resources
33
- attr_accessor :links
34
-
35
- # @return [String] the ETag header from the most recent HTTP response, useful for caching and reloading
36
- attr_accessor :etag
37
-
38
- # @return [Time] the Last-Modified header from the most recent HTTP response, useful for caching and reloading
39
- attr_accessor :last_modified
40
-
41
- # @return [Hash] a hash of any X-Riak-Meta-* headers that were in the HTTP response, keyed on the trailing portion
42
- attr_accessor :meta
43
-
44
- # @return [Hash<Set>] a hash of secondary indexes, where the
45
- # key is the index name and the value is a Set of index
46
- # entries for that index
47
- attr_accessor :indexes
29
+ alias :vector_clock :vclock
30
+ alias :vector_clock= :vclock=
48
31
 
49
32
  # @return [Boolean] whether to attempt to prevent stale writes using conditional PUT semantics, If-None-Match: * or If-Match: {#etag}
50
33
  # @see http://wiki.basho.com/display/RIAK/REST+API#RESTAPI-Storeaneworexistingobjectwithakey Riak Rest API Docs
@@ -69,6 +52,16 @@ module Riak
69
52
  @on_conflict_hooks ||= []
70
53
  end
71
54
 
55
+ def_delegators :content, :content_type, :content_type=,
56
+ :links, :links=,
57
+ :etag, :etag=,
58
+ :last_modified, :last_modified=,
59
+ :meta, :meta=,
60
+ :indexes, :indexes=,
61
+ :data, :data=,
62
+ :raw_data, :raw_data=,
63
+ :deserialize, :serialize
64
+
72
65
  # Attempts to resolve conflict using the registered conflict callbacks.
73
66
  #
74
67
  # @return [RObject] the RObject
@@ -102,18 +95,10 @@ module Riak
102
95
  # @see Bucket#get
103
96
  def initialize(bucket, key=nil)
104
97
  @bucket, @key = bucket, key
105
- @links, @meta = Set.new, {}
106
- @indexes = new_index_hash
98
+ @siblings = [ RContent.new(self) ]
107
99
  yield self if block_given?
108
100
  end
109
101
 
110
- def indexes=(hash)
111
- @indexes = hash.inject(new_index_hash) do |h, (k,v)|
112
- h[k].merge([*v])
113
- h
114
- end
115
- end
116
-
117
102
  # Load object data from a map/reduce response item.
118
103
  # This method is used by RObject::load_from_mapreduce to instantiate the necessary
119
104
  # objects.
@@ -121,58 +106,14 @@ module Riak
121
106
  # @return [RObject] self
122
107
  def load_from_mapreduce(response)
123
108
  self.vclock = response['vclock']
124
- if response['values'].size == 1
125
- value = response['values'].first
126
- load_map_reduce_value(value)
127
- else
128
- @conflict = true
129
- @siblings = response['values'].map do |v|
130
- RObject.new(self.bucket, self.key) do |robj|
131
- robj.vclock = self.vclock
132
- robj.load_map_reduce_value(v)
133
- end
109
+ @siblings = response['values'].map do |v|
110
+ RContent.new(self) do |rcontent|
111
+ rcontent.load_map_reduce_value(v)
134
112
  end
135
113
  end
136
114
  self
137
115
  end
138
116
 
139
- # @return [Object] the unmarshaled form of {#raw_data} stored in riak at this object's key
140
- def data
141
- if @raw_data && !@data
142
- raw = @raw_data.respond_to?(:read) ? @raw_data.read : @raw_data
143
- @data = deserialize(raw)
144
- @raw_data = nil
145
- end
146
- @data
147
- end
148
-
149
- # @param [Object] unmarshaled form of the data to be stored in riak. Object will be serialized using {#serialize} if a known content_type is used. Setting this overrides values stored with {#raw_data=}
150
- # @return [Object] the object stored
151
- def data=(new_data)
152
- if new_data.respond_to?(:read)
153
- raise ArgumentError.new(t("invalid_io_object"))
154
- end
155
-
156
- @raw_data = nil
157
- @data = new_data
158
- end
159
-
160
- # @return [String] raw data stored in riak for this object's key
161
- def raw_data
162
- if @data && !@raw_data
163
- @raw_data = serialize(@data)
164
- @data = nil
165
- end
166
- @raw_data
167
- end
168
-
169
- # @param [String, IO-like] the raw data to be stored in riak at this key, will not be marshaled or manipulated prior to storage. Overrides any data stored by {#data=}
170
- # @return [String] the data stored
171
- def raw_data=(new_raw_data)
172
- @data = nil
173
- @raw_data = new_raw_data
174
- end
175
-
176
117
  # Store the object in Riak
177
118
  # @param [Hash] options query parameters
178
119
  # @option options [Fixnum] :r the "r" parameter (Read quorum for the implicit read performed when validating the store operation)
@@ -181,8 +122,10 @@ module Riak
181
122
  # @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.
182
123
  # @return [Riak::RObject] self
183
124
  # @raise [ArgumentError] if the content_type is not defined
125
+ # @raise [Conflict] if the object has siblings
184
126
  def store(options={})
185
- raise ArgumentError, t("content_type_undefined") unless @content_type.present?
127
+ raise Conflict, self if conflict?
128
+ raise ArgumentError, t("content_type_undefined") unless content_type.present?
186
129
  @bucket.client.store_object(self, options)
187
130
  self
188
131
  end
@@ -213,54 +156,29 @@ module Riak
213
156
  freeze
214
157
  end
215
158
 
216
- attr_writer :siblings, :conflict
159
+ # Returns sibling values. If the object is not in conflict, then
160
+ # only one value will be present in the array.
161
+ # @return [Array<RContent>] an array of conflicting sibling values
162
+ # for this key, possibly containing only one
163
+ attr_accessor :siblings
217
164
 
218
- # Returns sibling objects when in conflict.
219
- # @return [Array<RObject>] an array of conflicting sibling objects for this key
220
- # @return [Array<self>] a single-element array containing object when not
221
- # in conflict
222
- def siblings
223
- return [self] unless conflict?
224
- @siblings
165
+ # Returns the solitary sibling when not in conflict.
166
+ # @return [RContent] the sole value/sibling on this object
167
+ # @raise [Conflict] when multiple siblings are present
168
+ def content
169
+ raise Conflict, self if conflict?
170
+ @siblings.first
225
171
  end
226
172
 
227
173
  # @return [true,false] Whether this object has conflicting sibling objects (divergent vclocks)
228
174
  def conflict?
229
- @conflict.present?
230
- end
231
-
232
- # Serializes the internal object data for sending to Riak. Differs based on the content-type.
233
- # This method is called internally when storing the object.
234
- # Automatically serialized formats:
235
- # * JSON (application/json)
236
- # * YAML (text/yaml)
237
- # * Marshal (application/x-ruby-marshal)
238
- # When given an IO-like object (e.g. File), no serialization will
239
- # be done.
240
- # @param [Object] payload the data to serialize
241
- def serialize(payload)
242
- Serializers.serialize(@content_type, payload)
243
- end
244
-
245
- # Deserializes the internal object data from a Riak response. Differs based on the content-type.
246
- # This method is called internally when loading the object.
247
- # Automatically deserialized formats:
248
- # * JSON (application/json)
249
- # * YAML (text/yaml)
250
- # * Marshal (application/x-ruby-marshal)
251
- # @param [String] body the serialized response body
252
- def deserialize(body)
253
- Serializers.deserialize(@content_type, body)
175
+ @siblings.size != 1
254
176
  end
255
177
 
256
178
  # @return [String] A representation suitable for IRB and debugging output
257
179
  def inspect
258
- body = if @data || Serializers[content_type]
259
- data.inspect
260
- else
261
- @raw_data && "(#{@raw_data.size} bytes)"
262
- end
263
- "#<#{self.class.name} {#{bucket.name}#{"," + @key if @key}} [#{@content_type}]:#{body}>"
180
+ body = @siblings.map {|s| s.inspect }.join(", ")
181
+ "#<#{self.class.name} {#{bucket.name}#{"," + @key if @key}} [#{body}]>"
264
182
  end
265
183
 
266
184
  # Walks links from this object to other objects in Riak.
@@ -276,43 +194,5 @@ module Riak
276
194
  def to_link(tag)
277
195
  Link.new(@bucket.name, @key, tag)
278
196
  end
279
-
280
- alias :vector_clock :vclock
281
- alias :vector_clock= :vclock=
282
-
283
- protected
284
- def load_map_reduce_value(hash)
285
- metadata = hash['metadata']
286
- extract_if_present(metadata, 'X-Riak-VTag', :etag)
287
- extract_if_present(metadata, 'content-type', :content_type)
288
- extract_if_present(metadata, 'X-Riak-Last-Modified', :last_modified) { |v| Time.httpdate( v ) }
289
- extract_if_present(metadata, 'index', :indexes) do |entries|
290
- Hash[ entries.map {|k,v| [k, Set.new(Array(v))] } ]
291
- end
292
- extract_if_present(metadata, 'Links', :links) do |links|
293
- Set.new( links.map { |l| Link.new(*l) } )
294
- end
295
- extract_if_present(metadata, 'X-Riak-Meta', :meta) do |meta|
296
- Hash[
297
- meta.map do |k,v|
298
- [k.sub(%r{^x-riak-meta-}i, ''), [v]]
299
- end
300
- ]
301
- end
302
- extract_if_present(hash, 'data', :data) { |v| deserialize(v) }
303
- end
304
-
305
- private
306
- def extract_if_present(hash, key, attribute=nil)
307
- if hash[key].present?
308
- attribute ||= key
309
- value = block_given? ? yield(hash[key]) : hash[key]
310
- send("#{attribute}=", value)
311
- end
312
- end
313
-
314
- def new_index_hash
315
- Hash.new {|h,k| h[k] = Set.new }
316
- end
317
197
  end
318
198
  end