seomoz-riak-client 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. data/Gemfile +27 -0
  2. data/Guardfile +14 -0
  3. data/Rakefile +76 -0
  4. data/erl_src/riak_kv_test_backend.beam +0 -0
  5. data/erl_src/riak_kv_test_backend.erl +174 -0
  6. data/erl_src/riak_search_test_backend.beam +0 -0
  7. data/erl_src/riak_search_test_backend.erl +175 -0
  8. data/lib/active_support/cache/riak_store.rb +2 -0
  9. data/lib/riak.rb +21 -0
  10. data/lib/riak/bucket.rb +215 -0
  11. data/lib/riak/cache_store.rb +84 -0
  12. data/lib/riak/client.rb +415 -0
  13. data/lib/riak/client/beefcake/messages.rb +147 -0
  14. data/lib/riak/client/beefcake/object_methods.rb +92 -0
  15. data/lib/riak/client/beefcake_protobuffs_backend.rb +176 -0
  16. data/lib/riak/client/excon_backend.rb +65 -0
  17. data/lib/riak/client/http_backend.rb +203 -0
  18. data/lib/riak/client/http_backend/configuration.rb +46 -0
  19. data/lib/riak/client/http_backend/key_streamer.rb +43 -0
  20. data/lib/riak/client/http_backend/object_methods.rb +94 -0
  21. data/lib/riak/client/http_backend/request_headers.rb +34 -0
  22. data/lib/riak/client/http_backend/transport_methods.rb +218 -0
  23. data/lib/riak/client/net_http_backend.rb +79 -0
  24. data/lib/riak/client/protobuffs_backend.rb +97 -0
  25. data/lib/riak/client/pump.rb +30 -0
  26. data/lib/riak/client/search.rb +94 -0
  27. data/lib/riak/core_ext.rb +6 -0
  28. data/lib/riak/core_ext/blank.rb +53 -0
  29. data/lib/riak/core_ext/extract_options.rb +7 -0
  30. data/lib/riak/core_ext/json.rb +15 -0
  31. data/lib/riak/core_ext/slice.rb +18 -0
  32. data/lib/riak/core_ext/stringify_keys.rb +10 -0
  33. data/lib/riak/core_ext/symbolize_keys.rb +10 -0
  34. data/lib/riak/core_ext/to_param.rb +31 -0
  35. data/lib/riak/encoding.rb +6 -0
  36. data/lib/riak/failed_request.rb +81 -0
  37. data/lib/riak/i18n.rb +3 -0
  38. data/lib/riak/json.rb +28 -0
  39. data/lib/riak/link.rb +85 -0
  40. data/lib/riak/locale/en.yml +48 -0
  41. data/lib/riak/map_reduce.rb +206 -0
  42. data/lib/riak/map_reduce/filter_builder.rb +94 -0
  43. data/lib/riak/map_reduce/phase.rb +98 -0
  44. data/lib/riak/map_reduce_error.rb +7 -0
  45. data/lib/riak/robject.rb +290 -0
  46. data/lib/riak/search.rb +3 -0
  47. data/lib/riak/serializers.rb +74 -0
  48. data/lib/riak/stamp.rb +77 -0
  49. data/lib/riak/test_server.rb +252 -0
  50. data/lib/riak/util/escape.rb +45 -0
  51. data/lib/riak/util/fiber1.8.rb +48 -0
  52. data/lib/riak/util/headers.rb +53 -0
  53. data/lib/riak/util/multipart.rb +52 -0
  54. data/lib/riak/util/multipart/stream_parser.rb +62 -0
  55. data/lib/riak/util/tcp_socket_extensions.rb +58 -0
  56. data/lib/riak/util/translation.rb +19 -0
  57. data/lib/riak/walk_spec.rb +105 -0
  58. data/riak-client.gemspec +55 -0
  59. data/seomoz-riak-client.gemspec +55 -0
  60. data/spec/fixtures/cat.jpg +0 -0
  61. data/spec/fixtures/multipart-blank.txt +7 -0
  62. data/spec/fixtures/multipart-mapreduce.txt +10 -0
  63. data/spec/fixtures/multipart-with-body.txt +16 -0
  64. data/spec/fixtures/server.cert.crt +15 -0
  65. data/spec/fixtures/server.cert.key +15 -0
  66. data/spec/fixtures/test.pem +1 -0
  67. data/spec/integration/riak/cache_store_spec.rb +154 -0
  68. data/spec/integration/riak/http_backends_spec.rb +58 -0
  69. data/spec/integration/riak/protobuffs_backends_spec.rb +32 -0
  70. data/spec/integration/riak/test_server_spec.rb +161 -0
  71. data/spec/riak/beefcake_protobuffs_backend_spec.rb +59 -0
  72. data/spec/riak/bucket_spec.rb +205 -0
  73. data/spec/riak/client_spec.rb +517 -0
  74. data/spec/riak/core_ext/to_param_spec.rb +15 -0
  75. data/spec/riak/escape_spec.rb +69 -0
  76. data/spec/riak/excon_backend_spec.rb +64 -0
  77. data/spec/riak/headers_spec.rb +38 -0
  78. data/spec/riak/http_backend/configuration_spec.rb +51 -0
  79. data/spec/riak/http_backend/object_methods_spec.rb +217 -0
  80. data/spec/riak/http_backend/transport_methods_spec.rb +117 -0
  81. data/spec/riak/http_backend_spec.rb +269 -0
  82. data/spec/riak/link_spec.rb +71 -0
  83. data/spec/riak/map_reduce/filter_builder_spec.rb +32 -0
  84. data/spec/riak/map_reduce/phase_spec.rb +136 -0
  85. data/spec/riak/map_reduce_spec.rb +310 -0
  86. data/spec/riak/multipart_spec.rb +23 -0
  87. data/spec/riak/net_http_backend_spec.rb +16 -0
  88. data/spec/riak/robject_spec.rb +427 -0
  89. data/spec/riak/search_spec.rb +178 -0
  90. data/spec/riak/serializers_spec.rb +93 -0
  91. data/spec/riak/stamp_spec.rb +54 -0
  92. data/spec/riak/stream_parser_spec.rb +53 -0
  93. data/spec/riak/walk_spec_spec.rb +195 -0
  94. data/spec/spec_helper.rb +39 -0
  95. data/spec/support/drb_mock_server.rb +39 -0
  96. data/spec/support/http_backend_implementation_examples.rb +266 -0
  97. data/spec/support/integration_setup.rb +10 -0
  98. data/spec/support/mock_server.rb +81 -0
  99. data/spec/support/mocks.rb +4 -0
  100. data/spec/support/test_server.yml.example +2 -0
  101. data/spec/support/unified_backend_examples.rb +255 -0
  102. metadata +271 -0
@@ -0,0 +1,94 @@
1
+
2
+ require 'riak/util/translation'
3
+
4
+ module Riak
5
+ class MapReduce
6
+ # Builds key-filter lists for MapReduce inputs in a DSL-like fashion.
7
+ class FilterBuilder
8
+ include Util::Translation
9
+
10
+ # Known filters available in riak_kv_mapred_filters, mapped to
11
+ # their arities. These are turned into instance methods.
12
+ # Example:
13
+ #
14
+ # FilterBuilder.new do
15
+ # string_to_int
16
+ # less_than 50
17
+ # end
18
+ FILTERS = {
19
+ :int_to_string => 0,
20
+ :string_to_int => 0,
21
+ :float_to_string => 0,
22
+ :string_to_float => 0,
23
+ :to_upper => 0,
24
+ :to_lower => 0,
25
+ :tokenize => 2,
26
+ :urldecode => 0,
27
+ :greater_than => 1,
28
+ :less_than => 1,
29
+ :greater_than_eq => 1,
30
+ :less_than_eq => 1,
31
+ :between => [2,3],
32
+ :matches => 1,
33
+ :neq => 1,
34
+ :eq => 1,
35
+ :set_member => -1,
36
+ :similar_to => 2,
37
+ :starts_with => 1,
38
+ :ends_with => 1
39
+ }
40
+
41
+ # Available logical operations for joining filter chains. These
42
+ # are turned into instance methods with leading underscores,
43
+ # with aliases to uppercase versions.
44
+ # Example:
45
+ #
46
+ # FilterBuilder.new do
47
+ # string_to_int
48
+ # AND do
49
+ # seq { greater_than_eq 50 }
50
+ # seq { neq 100 }
51
+ # end
52
+ # end
53
+ LOGICAL_OPERATIONS = %w{and or not}
54
+
55
+ FILTERS.each do |f,arity|
56
+ class_eval <<-CODE
57
+ def #{f}(*args)
58
+ raise ArgumentError.new(t("filter_arity_mismatch", :filter => :#{f}, :expected => #{arity.inspect}, :received => args.size)) unless #{arity.inspect} == -1 || Array(#{arity.inspect}).include?(args.size)
59
+ @filters << ([:#{f}] + args)
60
+ end
61
+ CODE
62
+ end
63
+
64
+ LOGICAL_OPERATIONS.each do |op|
65
+ class_eval <<-CODE
66
+ def _#{op}(&block)
67
+ raise ArgumentError.new(t('filter_needs_block', :filter => '#{op}')) unless block_given?
68
+ @filters << [:#{op}, self.class.new(&block).to_a]
69
+ end
70
+ alias :#{op.to_s.upcase} :_#{op}
71
+ CODE
72
+ end
73
+
74
+ # Creates a new FilterBuilder. Pass a block that will be
75
+ # instance_eval'ed to construct the sequence of filters.
76
+ def initialize(&block)
77
+ @filters = []
78
+ instance_eval(&block) if block_given?
79
+ end
80
+
81
+ # Wraps multi-step filters for use inside logical
82
+ # operations. Does not correspond to an actual filter.
83
+ def sequence(&block)
84
+ @filters << self.class.new(&block).to_a
85
+ end
86
+ alias :seq :sequence
87
+
88
+ # @return A list of filters for handing to the MapReduce inputs.
89
+ def to_a
90
+ @filters
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,98 @@
1
+
2
+ require 'riak/json'
3
+ require 'riak/util/translation'
4
+ require 'riak/walk_spec'
5
+
6
+ module Riak
7
+ class MapReduce
8
+ # Represents an individual phase in a map-reduce pipeline. Generally you'll want to call
9
+ # methods of MapReduce instead of using this directly.
10
+ class Phase
11
+ include Util::Translation
12
+ # @return [Symbol] the type of phase - :map, :reduce, or :link
13
+ attr_accessor :type
14
+
15
+ # @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.
16
+ attr_accessor :function
17
+
18
+ # @return [String] the language of the phase's function - "javascript" or "erlang". Meaningless for :link type phases.
19
+ attr_accessor :language
20
+
21
+ # @return [Boolean] whether results of this phase will be returned
22
+ attr_accessor :keep
23
+
24
+ # @return [Array] any extra static arguments to pass to the phase
25
+ attr_accessor :arg
26
+
27
+ # Creates a phase in the map-reduce pipeline
28
+ # @param [Hash] options options for the phase
29
+ # @option options [Symbol] :type one of :map, :reduce, :link
30
+ # @option options [String] :language ("javascript") "erlang" or "javascript"
31
+ # @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.
32
+ # @option options [Boolean] :keep (false) whether to return the results of this phase
33
+ # @option options [Array] :arg (nil) any extra static arguments to pass to the phase
34
+ def initialize(options={})
35
+ self.type = options[:type]
36
+ self.language = options[:language] || "javascript"
37
+ self.function = options[:function]
38
+ self.keep = options[:keep] || false
39
+ self.arg = options[:arg]
40
+ end
41
+
42
+ def type=(value)
43
+ raise ArgumentError, t("invalid_phase_type") unless value.to_s =~ /^(map|reduce|link)$/i
44
+ @type = value.to_s.downcase.to_sym
45
+ end
46
+
47
+ def function=(value)
48
+ case value
49
+ when Array
50
+ raise ArgumentError, t("module_function_pair_required") unless value.size == 2
51
+ @language = "erlang"
52
+ when Hash
53
+ raise ArgumentError, t("stored_function_invalid") unless type == :link || value.has_key?(:bucket) && value.has_key?(:key)
54
+ @language = "javascript"
55
+ when String
56
+ @language = "javascript"
57
+ when WalkSpec
58
+ raise ArgumentError, t("walk_spec_invalid_unless_link") unless type == :link
59
+ else
60
+ raise ArgumentError, t("invalid_function_value", :value => value.inspect)
61
+ end
62
+ @function = value
63
+ end
64
+
65
+ # Converts the phase to JSON for use while invoking a job.
66
+ # @return [String] a JSON representation of the phase
67
+ def to_json(*a)
68
+ as_json.to_json(*a)
69
+ end
70
+
71
+ # Converts the phase to its JSON-compatible representation for job invocation.
72
+ # @return [Hash] a Hash-equivalent of the phase
73
+ def as_json(options=nil)
74
+ obj = case type
75
+ when :map, :reduce
76
+ defaults = {"language" => language, "keep" => keep}
77
+ case function
78
+ when Hash
79
+ defaults.merge(function)
80
+ when String
81
+ if function =~ /\s*function/
82
+ defaults.merge("source" => function)
83
+ else
84
+ defaults.merge("name" => function)
85
+ end
86
+ when Array
87
+ defaults.merge("module" => function[0], "function" => function[1])
88
+ end
89
+ when :link
90
+ spec = WalkSpec.normalize(function).first
91
+ {"bucket" => spec.bucket, "tag" => spec.tag, "keep" => spec.keep || keep}
92
+ end
93
+ obj["arg"] = arg if arg
94
+ { type => obj }
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,7 @@
1
+ require 'riak/util/translation'
2
+
3
+ module Riak
4
+ # Raised when an error occurred in the Javascript map-reduce chain.
5
+ # The message will be the body of the JSON error response.
6
+ class MapReduceError < StandardError; end
7
+ end
@@ -0,0 +1,290 @@
1
+ require 'set'
2
+ require 'time'
3
+ require 'yaml'
4
+ require 'riak/util/translation'
5
+ require 'riak/util/escape'
6
+ require 'riak/bucket'
7
+ require 'riak/link'
8
+ require 'riak/walk_spec'
9
+ require 'riak/serializers'
10
+
11
+ module Riak
12
+ # Represents the data and metadata stored in a bucket/key pair in
13
+ # the Riak database, the base unit of data manipulation.
14
+ class RObject
15
+ include Util::Translation
16
+ extend Util::Translation
17
+ include Util::Escape
18
+ extend Util::Escape
19
+
20
+ # @return [Bucket] the bucket in which this object is contained
21
+ attr_accessor :bucket
22
+
23
+ # @return [String] the key of this object within its bucket
24
+ attr_accessor :key
25
+
26
+ # @return [String] the MIME content type of the object
27
+ attr_accessor :content_type
28
+
29
+ # @return [String] the Riak vector clock for the object
30
+ attr_accessor :vclock
31
+
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 [Boolean] whether to attempt to prevent stale writes using conditional PUT semantics, If-None-Match: * or If-Match: {#etag}
45
+ # @see http://wiki.basho.com/display/RIAK/REST+API#RESTAPI-Storeaneworexistingobjectwithakey Riak Rest API Docs
46
+ attr_accessor :prevent_stale_writes
47
+
48
+ def self.on_conflict(&conflict_hook)
49
+ on_conflict_hooks << conflict_hook
50
+ end
51
+
52
+ def self.on_conflict_hooks
53
+ @on_conflict_hooks ||= []
54
+ end
55
+
56
+ def attempt_conflict_resolution
57
+ return self unless conflict?
58
+
59
+ self.class.on_conflict_hooks.each do |hook|
60
+ result = hook.call(self)
61
+ return result if result.is_a?(RObject)
62
+ end
63
+
64
+ self
65
+ end
66
+
67
+ # Loads a list of RObjects that were emitted from a MapReduce
68
+ # query.
69
+ # @param [Client] client A Riak::Client with which the results will be associated
70
+ # @param [Array<Hash>] response A list of results a MapReduce job. Each entry should contain these keys: bucket, key, vclock, values
71
+ # @return [Array<RObject>] An array of RObject instances
72
+ def self.load_from_mapreduce(client, response)
73
+ response.map do |item|
74
+ RObject.new(client[unescape(item['bucket'])], unescape(item['key'])).load_from_mapreduce(item)
75
+ end
76
+ end
77
+
78
+ # Create a new object manually
79
+ # @param [Bucket] bucket the bucket in which the object exists
80
+ # @param [String] key the key at which the object resides. If nil, a key will be assigned when the object is saved.
81
+ # @yield self the new RObject
82
+ # @see Bucket#get
83
+ def initialize(bucket, key=nil)
84
+ @bucket, @key = bucket, key
85
+ @links, @meta = Set.new, {}
86
+ yield self if block_given?
87
+ end
88
+
89
+ # Load object data from a map/reduce response item.
90
+ # This method is used by RObject::load_from_mapreduce to instantiate the necessary
91
+ # objects.
92
+ # @param [Hash] response a response from {Riak::MapReduce}
93
+ # @return [RObject] self
94
+ def load_from_mapreduce(response)
95
+ self.vclock = response['vclock']
96
+ if response['values'].size == 1
97
+ value = response['values'].first
98
+ load_map_reduce_value(value)
99
+ else
100
+ @conflict = true
101
+ @siblings = response['values'].map do |v|
102
+ RObject.new(self.bucket, self.key) do |robj|
103
+ robj.vclock = self.vclock
104
+ robj.load_map_reduce_value(v)
105
+ end
106
+ end
107
+ end
108
+ self
109
+ end
110
+
111
+ # @return [Object] the unmarshaled form of {#raw_data} stored in riak at this object's key
112
+ def data
113
+ if @raw_data && !@data
114
+ raw = @raw_data.respond_to?(:read) ? @raw_data.read : @raw_data
115
+ @data = deserialize(raw)
116
+ @raw_data = nil
117
+ end
118
+ @data
119
+ end
120
+
121
+ # @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=}
122
+ # @return [Object] the object stored
123
+ def data=(new_data)
124
+ if new_data.respond_to?(:read)
125
+ raise ArgumentError.new(t("invalid_io_object"))
126
+ end
127
+
128
+ @raw_data = nil
129
+ @data = new_data
130
+ end
131
+
132
+ # @return [String] raw data stored in riak for this object's key
133
+ def raw_data
134
+ if @data && !@raw_data
135
+ @raw_data = serialize(@data)
136
+ @data = nil
137
+ end
138
+ @raw_data
139
+ end
140
+
141
+ # @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=}
142
+ # @return [String] the data stored
143
+ def raw_data=(new_raw_data)
144
+ @data = nil
145
+ @raw_data = new_raw_data
146
+ end
147
+
148
+ # Store the object in Riak
149
+ # @param [Hash] options query parameters
150
+ # @option options [Fixnum] :r the "r" parameter (Read quorum for the implicit read performed when validating the store operation)
151
+ # @option options [Fixnum] :w the "w" parameter (Write quorum)
152
+ # @option options [Fixnum] :dw the "dw" parameter (Durable-write quorum)
153
+ # @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.
154
+ # @return [Riak::RObject] self
155
+ # @raise [ArgumentError] if the content_type is not defined
156
+ def store(options={})
157
+ raise ArgumentError, t("content_type_undefined") unless @content_type.present?
158
+ params = {:returnbody => true}.merge(options)
159
+ @bucket.client.backend.store_object(self, params[:returnbody], params[:w], params[:dw])
160
+ self
161
+ end
162
+
163
+ # Reload the object from Riak. Will use conditional GETs when possible.
164
+ # @param [Hash] options query parameters
165
+ # @option options [Fixnum] :r the "r" parameter (Read quorum)
166
+ # @option options [Boolean] :force will force a reload request if
167
+ # the vclock is not present, useful for reloading the object after
168
+ # a store (not passed in the query params)
169
+ # @return [Riak::RObject] self
170
+ def reload(options={})
171
+ force = options.delete(:force)
172
+ return self unless @key && (@vclock || force)
173
+ self.etag = self.last_modified = nil if force
174
+ bucket.client.backend.reload_object(self, options[:r])
175
+ end
176
+
177
+ alias :fetch :reload
178
+
179
+ # Delete the object from Riak and freeze this instance. Will work whether or not the object actually
180
+ # exists in the Riak database.
181
+ def delete(options={})
182
+ return if key.blank?
183
+ @bucket.delete(key, options)
184
+ freeze
185
+ end
186
+
187
+ attr_writer :siblings, :conflict
188
+
189
+ # Returns sibling objects when in conflict.
190
+ # @return [Array<RObject>] an array of conflicting sibling objects for this key
191
+ # @return [self] this object when not in conflict
192
+ def siblings
193
+ return self unless conflict?
194
+ @siblings
195
+ end
196
+
197
+ # @return [true,false] Whether this object has conflicting sibling objects (divergent vclocks)
198
+ def conflict?
199
+ @conflict.present?
200
+ end
201
+
202
+ # Serializes the internal object data for sending to Riak. Differs based on the content-type.
203
+ # This method is called internally when storing the object.
204
+ # Automatically serialized formats:
205
+ # * JSON (application/json)
206
+ # * YAML (text/yaml)
207
+ # * Marshal (application/x-ruby-marshal)
208
+ # When given an IO-like object (e.g. File), no serialization will
209
+ # be done.
210
+ # @param [Object] payload the data to serialize
211
+ def serialize(payload)
212
+ Serializers.serialize(@content_type, payload)
213
+ end
214
+
215
+ # Deserializes the internal object data from a Riak response. Differs based on the content-type.
216
+ # This method is called internally when loading the object.
217
+ # Automatically deserialized formats:
218
+ # * JSON (application/json)
219
+ # * YAML (text/yaml)
220
+ # * Marshal (application/x-ruby-marshal)
221
+ # @param [String] body the serialized response body
222
+ def deserialize(body)
223
+ Serializers.deserialize(@content_type, body)
224
+ end
225
+
226
+ # @return [String] A representation suitable for IRB and debugging output
227
+ def inspect
228
+ body = if @data || Serializers[content_type]
229
+ data.inspect
230
+ else
231
+ @raw_data && "(#{@raw_data.size} bytes)"
232
+ end
233
+ "#<#{self.class.name} {#{bucket.name}#{"," + @key if @key}} [#{@content_type}]:#{body}>"
234
+ end
235
+
236
+ # Walks links from this object to other objects in Riak.
237
+ # @param [Array<Hash,WalkSpec>] link specifications for the query
238
+ def walk(*params)
239
+ specs = WalkSpec.normalize(*params)
240
+ @bucket.client.http.link_walk(self, specs)
241
+ end
242
+
243
+ # Converts the object to a link suitable for linking other objects
244
+ # to it
245
+ # @param [String] tag the tag to apply to the link
246
+ def to_link(tag)
247
+ Link.new(@bucket.name, @key, tag)
248
+ end
249
+
250
+ # Generates a URL representing the object according to the client, bucket and key.
251
+ # If the key is blank, the bucket URL will be returned (where the object will be
252
+ # submitted to when stored).
253
+ def url
254
+ segments = [ @bucket.client.prefix, escape(@bucket.name)]
255
+ segments << escape(@key) if @key
256
+ @bucket.client.http.path(*segments).to_s
257
+ end
258
+
259
+ alias :vector_clock :vclock
260
+ alias :vector_clock= :vclock=
261
+
262
+ protected
263
+ def load_map_reduce_value(hash)
264
+ metadata = hash['metadata']
265
+ extract_if_present(metadata, 'X-Riak-VTag', :etag)
266
+ extract_if_present(metadata, 'content-type', :content_type)
267
+ extract_if_present(metadata, 'X-Riak-Last-Modified', :last_modified) { |v| Time.httpdate( v ) }
268
+ extract_if_present(metadata, 'Links', :links) do |links|
269
+ Set.new( links.map { |l| Link.new(*l) } )
270
+ end
271
+ extract_if_present(metadata, 'X-Riak-Meta', :meta) do |meta|
272
+ Hash[
273
+ meta.map do |k,v|
274
+ [k.sub(%r{^x-riak-meta-}i, ''), [v]]
275
+ end
276
+ ]
277
+ end
278
+ extract_if_present(hash, 'data', :data) { |v| deserialize(v) }
279
+ end
280
+
281
+ private
282
+ def extract_if_present(hash, key, attribute=nil)
283
+ if hash[key].present?
284
+ attribute ||= key
285
+ value = block_given? ? yield(hash[key]) : hash[key]
286
+ send("#{attribute}=", value)
287
+ end
288
+ end
289
+ end
290
+ end