better-riak-client 1.0.5

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 (72) hide show
  1. data/LICENSE +16 -0
  2. data/README.markdown +198 -0
  3. data/RELEASE_NOTES.md +211 -0
  4. data/better-riak-client.gemspec +61 -0
  5. data/erl_src/riak_kv_test014_backend.beam +0 -0
  6. data/erl_src/riak_kv_test014_backend.erl +189 -0
  7. data/erl_src/riak_kv_test_backend.beam +0 -0
  8. data/erl_src/riak_kv_test_backend.erl +697 -0
  9. data/erl_src/riak_search_test_backend.beam +0 -0
  10. data/erl_src/riak_search_test_backend.erl +175 -0
  11. data/lib/riak/bucket.rb +221 -0
  12. data/lib/riak/client/beefcake/messages.rb +213 -0
  13. data/lib/riak/client/beefcake/object_methods.rb +111 -0
  14. data/lib/riak/client/beefcake_protobuffs_backend.rb +226 -0
  15. data/lib/riak/client/decaying.rb +36 -0
  16. data/lib/riak/client/excon_backend.rb +162 -0
  17. data/lib/riak/client/feature_detection.rb +88 -0
  18. data/lib/riak/client/http_backend/configuration.rb +211 -0
  19. data/lib/riak/client/http_backend/key_streamer.rb +43 -0
  20. data/lib/riak/client/http_backend/object_methods.rb +106 -0
  21. data/lib/riak/client/http_backend/request_headers.rb +34 -0
  22. data/lib/riak/client/http_backend/transport_methods.rb +201 -0
  23. data/lib/riak/client/http_backend.rb +340 -0
  24. data/lib/riak/client/net_http_backend.rb +82 -0
  25. data/lib/riak/client/node.rb +115 -0
  26. data/lib/riak/client/protobuffs_backend.rb +173 -0
  27. data/lib/riak/client/search.rb +91 -0
  28. data/lib/riak/client.rb +540 -0
  29. data/lib/riak/cluster.rb +151 -0
  30. data/lib/riak/core_ext/blank.rb +53 -0
  31. data/lib/riak/core_ext/deep_dup.rb +13 -0
  32. data/lib/riak/core_ext/extract_options.rb +7 -0
  33. data/lib/riak/core_ext/json.rb +15 -0
  34. data/lib/riak/core_ext/slice.rb +18 -0
  35. data/lib/riak/core_ext/stringify_keys.rb +10 -0
  36. data/lib/riak/core_ext/symbolize_keys.rb +10 -0
  37. data/lib/riak/core_ext/to_param.rb +31 -0
  38. data/lib/riak/core_ext.rb +7 -0
  39. data/lib/riak/encoding.rb +6 -0
  40. data/lib/riak/failed_request.rb +81 -0
  41. data/lib/riak/i18n.rb +5 -0
  42. data/lib/riak/json.rb +52 -0
  43. data/lib/riak/link.rb +94 -0
  44. data/lib/riak/locale/en.yml +53 -0
  45. data/lib/riak/locale/fr.yml +52 -0
  46. data/lib/riak/map_reduce/filter_builder.rb +103 -0
  47. data/lib/riak/map_reduce/phase.rb +98 -0
  48. data/lib/riak/map_reduce.rb +225 -0
  49. data/lib/riak/map_reduce_error.rb +7 -0
  50. data/lib/riak/node/configuration.rb +293 -0
  51. data/lib/riak/node/console.rb +133 -0
  52. data/lib/riak/node/control.rb +207 -0
  53. data/lib/riak/node/defaults.rb +83 -0
  54. data/lib/riak/node/generation.rb +106 -0
  55. data/lib/riak/node/log.rb +34 -0
  56. data/lib/riak/node/version.rb +43 -0
  57. data/lib/riak/node.rb +38 -0
  58. data/lib/riak/robject.rb +318 -0
  59. data/lib/riak/search.rb +3 -0
  60. data/lib/riak/serializers.rb +74 -0
  61. data/lib/riak/stamp.rb +77 -0
  62. data/lib/riak/test_server.rb +89 -0
  63. data/lib/riak/util/escape.rb +76 -0
  64. data/lib/riak/util/headers.rb +53 -0
  65. data/lib/riak/util/multipart/stream_parser.rb +62 -0
  66. data/lib/riak/util/multipart.rb +52 -0
  67. data/lib/riak/util/tcp_socket_extensions.rb +58 -0
  68. data/lib/riak/util/translation.rb +19 -0
  69. data/lib/riak/version.rb +3 -0
  70. data/lib/riak/walk_spec.rb +105 -0
  71. data/lib/riak.rb +21 -0
  72. metadata +348 -0
@@ -0,0 +1,83 @@
1
+ require 'riak/core_ext/deep_dup'
2
+
3
+ module Riak
4
+ class Node
5
+ # Settings based on Riak 1.1.
6
+ ENV_DEFAULTS = {
7
+ :riak_core => {
8
+ :ring_creation_size => 64
9
+ },
10
+ :riak_kv => {
11
+ :storage_backend => :riak_kv_bitcask_backend,
12
+ :map_js_vm_count => 8,
13
+ :reduce_js_vm_count => 6,
14
+ :hook_js_vm_count => 2,
15
+ :mapper_batch_size => 5,
16
+ :js_max_vm_mem => 8,
17
+ :js_thread_stack => 16,
18
+ :riak_kv_stat => true,
19
+ :legacy_stats => true,
20
+ :vnode_vclocks => true,
21
+ :http_url_encoding => :on,
22
+ :legacy_keylisting => false,
23
+ :mapred_system => :pipe,
24
+ :mapred_2i_pipe => true,
25
+ :listkeys_backpressure => true,
26
+ :add_paths => []
27
+ },
28
+ :riak_search => {
29
+ :enabled => true
30
+ },
31
+ :luwak => {
32
+ :enabled => true
33
+ },
34
+ :merge_index => {
35
+ :buffer_rollover_size => 1048576,
36
+ :max_compact_segments => 20
37
+ },
38
+ :eleveldb => {},
39
+ :bitcask => {},
40
+ :lager => {
41
+ :crash_log_size => 10485760,
42
+ :crash_log_msg_size => 65536,
43
+ :crash_log_date => "$D0",
44
+ :crash_log_count => 5,
45
+ :error_logger_redirect => true
46
+ },
47
+ :riak_sysmon => {
48
+ :process_limit => 30,
49
+ :port_limit => 30,
50
+ :gc_ms_limit => 100,
51
+ :heap_word_limit => 40111000,
52
+ :busy_port => true,
53
+ :busy_dist_port => true
54
+ },
55
+ :sasl => {
56
+ :sasl_error_logger => false
57
+ },
58
+ :riak_control => {
59
+ :enabled => false,
60
+ :auth => :userlist,
61
+ :userlist => {"user" => "pass"},
62
+ :admin => true
63
+ }
64
+ }.freeze
65
+
66
+ # Based on Riak 1.1.
67
+ VM_DEFAULTS = {
68
+ "+K" => true,
69
+ "+A" => 64,
70
+ "-smp" => "enable",
71
+ "+W" => "w",
72
+ "-env ERL_MAX_PORTS" => 4096,
73
+ "-env ERL_FULLSWEEP_AFTER" => 0
74
+ }.freeze
75
+
76
+ protected
77
+ # Populates the defaults
78
+ def set_defaults
79
+ @env = ENV_DEFAULTS.deep_dup
80
+ @vm = VM_DEFAULTS.deep_dup
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,106 @@
1
+ require 'yaml'
2
+
3
+ module Riak
4
+ class Node
5
+ # Does the node exist on disk?
6
+ def exist?
7
+ manifest.exist?
8
+ end
9
+
10
+ # Deletes the node and regenerates it.
11
+ def recreate
12
+ delete
13
+ create
14
+ end
15
+
16
+ # Generates the node.
17
+ def create
18
+ unless exist?
19
+ create_directories
20
+ write_scripts
21
+ write_vm_args
22
+ write_app_config
23
+ write_manifest
24
+ end
25
+ end
26
+
27
+ # Clears data from known data directories. Stops the node if it is
28
+ # running.
29
+ def drop
30
+ was_started = started?
31
+ stop if was_started
32
+ data.children.each do |item|
33
+ if item.directory?
34
+ item.children.each {|c| c.rmtree }
35
+ else
36
+ item.delete
37
+ end
38
+ end
39
+ start if was_started
40
+ end
41
+
42
+ # Removes the node from disk and freezes the object.
43
+ def destroy
44
+ delete
45
+ freeze
46
+ end
47
+
48
+ protected
49
+ def delete
50
+ stop unless stopped?
51
+ root.rmtree if root.exist?
52
+ end
53
+
54
+ def create_directories
55
+ root.mkpath
56
+ NODE_DIRECTORIES.each {|d| send(d).mkpath }
57
+ end
58
+
59
+ def write_vm_args
60
+ (etc + 'vm.args').open('w') do |f|
61
+ vm.each do |k,v|
62
+ f.puts "#{k} #{v}"
63
+ end
64
+ end
65
+ end
66
+
67
+ def write_app_config
68
+ (etc + 'app.config').open('w') do |f|
69
+ f.write to_erlang_config(env) + '.'
70
+ end
71
+ end
72
+
73
+ def write_scripts
74
+ [control_script, admin_script].each {|s| write_script(s.basename, s) }
75
+ end
76
+
77
+ def write_script(name, target)
78
+ source_script = source + name
79
+ target.open('wb') do |f|
80
+ source_script.readlines.each do |line|
81
+ line.sub!(/(RUNNER_SCRIPT_DIR=)(.*)/, '\1' + bin.to_s)
82
+ line.sub!(/(RUNNER_ETC_DIR=)(.*)/, '\1' + etc.to_s)
83
+ line.sub!(/(RUNNER_USER=)(.*)/, '\1')
84
+ line.sub!(/(RUNNER_LOG_DIR=)(.*)/, '\1' + log.to_s)
85
+ line.sub!(/(PIPE_DIR=)(.*)/, '\1' + pipe.to_s + "/") # PIPE_DIR must have a trailing slash
86
+ line.sub!(/(PLATFORM_DATA_DIR=)(.*)/, '\1' + data.to_s)
87
+ line.sub!('grep "$RUNNER_BASE_DIR/.*/[b]eam"', 'grep "$RUNNER_ETC_DIR/app.config"')
88
+ if line.strip == "RUNNER_BASE_DIR=${RUNNER_SCRIPT_DIR%/*}"
89
+ line = "RUNNER_BASE_DIR=#{source.parent.to_s}\n"
90
+ end
91
+ f.write line
92
+ end
93
+ end
94
+ target.chmod 0755
95
+ end
96
+
97
+ def write_manifest
98
+ # TODO: For now this only saves the information that was used when
99
+ # configuring the node. Later we'll verify/warn if the settings
100
+ # used differ on subsequent generations.
101
+ @configuration[:env] = @env
102
+ @configuration[:vm] = @vm
103
+ manifest.open('w') {|f| YAML.dump(@configuration, f) }
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,34 @@
1
+ module Riak
2
+ class Node
3
+ LAGER_LEVELS = [
4
+ :debug,
5
+ :info,
6
+ :notice,
7
+ :warning,
8
+ :error,
9
+ :critical,
10
+ :alert,
11
+ :emergency
12
+ ]
13
+
14
+ def read_console_log(*levels)
15
+ console_log = log + 'console.log'
16
+ if console_log.exist?
17
+ levels = levels.map { |level| expand_log_level(level) }.compact.flatten
18
+ pattern = /(#{levels.map { |level| "\\[#{level}\\]" }.join("|")})/
19
+ console_log.readlines.grep(pattern)
20
+ end
21
+ end
22
+
23
+ def expand_log_level(level)
24
+ case level
25
+ when Range
26
+ first = LAGER_LEVELS.index(level.begin.to_sym) || 0
27
+ last = LAGER_LEVELS.index(level.end.to_sym) || -1
28
+ LAGER_LEVELS[first..last]
29
+ when Symbol
30
+ level
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ require 'pathname'
2
+
3
+ module Riak
4
+ class Node
5
+ # @return [String] the version of the Riak node
6
+ def version
7
+ @version ||= configure_version
8
+ end
9
+
10
+ # @return [Pathname] the location of Riak installation, aka RUNNER_BASE_DIR
11
+ def base_dir
12
+ @base_dir ||= configure_base_dir
13
+ end
14
+
15
+ protected
16
+ # Detects the Riak version from the generated release
17
+ def configure_version
18
+ if base_dir
19
+ versions = (base_dir + 'releases' + 'start_erl.data').read
20
+ versions.split(" ")[1]
21
+ end
22
+ end
23
+
24
+ # Determines the base_dir from source control script
25
+ def configure_base_dir
26
+ # Use the script from the source directory so we don't require
27
+ # it to be generated first.
28
+ (source + control_script_name).each_line.find {|l| l =~ /^RUNNER_BASE_DIR=(.*)/ }
29
+
30
+ # There should only be one matching line, so the contents of $1
31
+ # will be the matched path. If there's nothing matched, we
32
+ # return nil.
33
+ case $1
34
+ when '${RUNNER_SCRIPT_DIR%/*}'
35
+ source.parent
36
+ when String
37
+ Pathname.new($1).expand_path
38
+ else
39
+ nil
40
+ end
41
+ end
42
+ end
43
+ end
data/lib/riak/node.rb ADDED
@@ -0,0 +1,38 @@
1
+ require 'riak/util/translation'
2
+ require 'riak/node/defaults'
3
+ require 'riak/node/configuration'
4
+ require 'riak/node/generation'
5
+ require 'riak/node/control'
6
+ require 'riak/node/version'
7
+ require 'riak/node/log'
8
+
9
+ module Riak
10
+ # A Node encapsulates the generation and management of standalone
11
+ # Riak nodes. It is used by the {TestServer} to start and manage an
12
+ # in-memory node for supporting integration test suites.
13
+ class Node
14
+ include Util::Translation
15
+
16
+ # Creates a new Riak node. Unlike {#new}, this will also generate
17
+ # the node if it does not exist on disk. Equivalent to {::new}
18
+ # followed by {#create}.
19
+ # @see #new
20
+ def self.create(configuration={})
21
+ new(configuration).tap do |node|
22
+ node.create
23
+ end
24
+ end
25
+
26
+ # Creates the template for a Riak node. To generate the node after
27
+ # initialization, use {#create}.
28
+ def initialize(configuration={})
29
+ set_defaults
30
+ configure configuration
31
+ end
32
+
33
+ protected
34
+ def debug(msg)
35
+ $stderr.puts msg if ENV["DEBUG_RIAK_NODE"]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,318 @@
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 [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
48
+
49
+ # @return [Boolean] whether to attempt to prevent stale writes using conditional PUT semantics, If-None-Match: * or If-Match: {#etag}
50
+ # @see http://wiki.basho.com/display/RIAK/REST+API#RESTAPI-Storeaneworexistingobjectwithakey Riak Rest API Docs
51
+ attr_accessor :prevent_stale_writes
52
+
53
+ # Defines a callback to be invoked when there is conflict.
54
+ #
55
+ # @yield The conflict callback.
56
+ # @yieldparam [RObject] robject The conflicted RObject
57
+ # @yieldreturn [RObject, nil] Either the resolved RObject or nil if your
58
+ # callback cannot resolve it. The next registered
59
+ # callback will be given the chance to resolve it.
60
+ #
61
+ # @note Ripple registers its own document-level conflict handler, so if you're
62
+ # using ripple, you will probably want to use that instead.
63
+ def self.on_conflict(&conflict_hook)
64
+ on_conflict_hooks << conflict_hook
65
+ end
66
+
67
+ # @return [Array<Proc>] the list of registered conflict callbacks.
68
+ def self.on_conflict_hooks
69
+ @on_conflict_hooks ||= []
70
+ end
71
+
72
+ # Attempts to resolve conflict using the registered conflict callbacks.
73
+ #
74
+ # @return [RObject] the RObject
75
+ # @note There is no guarantee the returned RObject will have been resolved
76
+ def attempt_conflict_resolution
77
+ return self unless conflict?
78
+
79
+ self.class.on_conflict_hooks.each do |hook|
80
+ result = hook.call(self)
81
+ return result if result.is_a?(RObject)
82
+ end
83
+
84
+ self
85
+ end
86
+
87
+ # Loads a list of RObjects that were emitted from a MapReduce
88
+ # query.
89
+ # @param [Client] client A Riak::Client with which the results will be associated
90
+ # @param [Array<Hash>] response A list of results a MapReduce job. Each entry should contain these keys: bucket, key, vclock, values
91
+ # @return [Array<RObject>] An array of RObject instances
92
+ def self.load_from_mapreduce(client, response)
93
+ response.map do |item|
94
+ RObject.new(client[unescape(item['bucket'])], unescape(item['key'])).load_from_mapreduce(item)
95
+ end
96
+ end
97
+
98
+ # Create a new object manually
99
+ # @param [Bucket] bucket the bucket in which the object exists
100
+ # @param [String] key the key at which the object resides. If nil, a key will be assigned when the object is saved.
101
+ # @yield self the new RObject
102
+ # @see Bucket#get
103
+ def initialize(bucket, key=nil)
104
+ @bucket, @key = bucket, key
105
+ @links, @meta = Set.new, {}
106
+ @indexes = new_index_hash
107
+ yield self if block_given?
108
+ end
109
+
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
+ # Load object data from a map/reduce response item.
118
+ # This method is used by RObject::load_from_mapreduce to instantiate the necessary
119
+ # objects.
120
+ # @param [Hash] response a response from {Riak::MapReduce}
121
+ # @return [RObject] self
122
+ def load_from_mapreduce(response)
123
+ 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
134
+ end
135
+ end
136
+ self
137
+ end
138
+
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
+ # Store the object in Riak
177
+ # @param [Hash] options query parameters
178
+ # @option options [Fixnum] :r the "r" parameter (Read quorum for the implicit read performed when validating the store operation)
179
+ # @option options [Fixnum] :w the "w" parameter (Write quorum)
180
+ # @option options [Fixnum] :dw the "dw" parameter (Durable-write quorum)
181
+ # @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
+ # @return [Riak::RObject] self
183
+ # @raise [ArgumentError] if the content_type is not defined
184
+ def store(options={})
185
+ raise ArgumentError, t("content_type_undefined") unless @content_type.present?
186
+ @bucket.client.store_object(self, options)
187
+ self
188
+ end
189
+
190
+ # Reload the object from Riak. Will use conditional GETs when possible.
191
+ # @param [Hash] options query parameters
192
+ # @option options [Fixnum] :r the "r" parameter (Read quorum)
193
+ # @option options [Boolean] :force will force a reload request if
194
+ # the vclock is not present, useful for reloading the object after
195
+ # a store (not passed in the query params)
196
+ # @return [Riak::RObject] self
197
+ def reload(options={})
198
+ force = options.delete(:force)
199
+ return self unless @key && (@vclock || force)
200
+ self.etag = self.last_modified = nil if force
201
+ bucket.client.reload_object(self, options)
202
+ end
203
+
204
+ alias :fetch :reload
205
+
206
+ # Delete the object from Riak and freeze this instance. Will work whether or not the object actually
207
+ # exists in the Riak database.
208
+ # @see Bucket#delete
209
+ def delete(options={})
210
+ return if key.blank?
211
+ options[:vclock] = vclock if vclock
212
+ @bucket.delete(key, options)
213
+ freeze
214
+ end
215
+
216
+ attr_writer :siblings, :conflict
217
+
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
225
+ end
226
+
227
+ # @return [true,false] Whether this object has conflicting sibling objects (divergent vclocks)
228
+ 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)
254
+ end
255
+
256
+ # @return [String] A representation suitable for IRB and debugging output
257
+ 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}>"
264
+ end
265
+
266
+ # Walks links from this object to other objects in Riak.
267
+ # @param [Array<Hash,WalkSpec>] link specifications for the query
268
+ def walk(*params)
269
+ specs = WalkSpec.normalize(*params)
270
+ @bucket.client.link_walk(self, specs)
271
+ end
272
+
273
+ # Converts the object to a link suitable for linking other objects
274
+ # to it
275
+ # @param [String] tag the tag to apply to the link
276
+ def to_link(tag)
277
+ Link.new(@bucket.name, @key, tag)
278
+ 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
+ end
318
+ end