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,203 @@
1
+
2
+ require 'riak/util/escape'
3
+ require 'riak/util/translation'
4
+ require 'riak/util/multipart'
5
+ require 'riak/util/multipart/stream_parser'
6
+ require 'riak/json'
7
+ require 'riak/client'
8
+ require 'riak/bucket'
9
+ require 'riak/robject'
10
+ require 'riak/client/http_backend/transport_methods'
11
+ require 'riak/client/http_backend/object_methods'
12
+ require 'riak/client/http_backend/configuration'
13
+ require 'riak/client/http_backend/key_streamer'
14
+
15
+ module Riak
16
+ class Client
17
+ # The parent class for all backends that connect to Riak via
18
+ # HTTP. This class implements all of the universal backend API
19
+ # methods on behalf of subclasses, which need only implement the
20
+ # {TransportMethods#perform} method for library-specific
21
+ # semantics.
22
+ class HTTPBackend
23
+ include Util::Escape
24
+ include Util::Translation
25
+
26
+ include TransportMethods
27
+ include ObjectMethods
28
+ include Configuration
29
+
30
+ # The Riak::Client that uses this backend
31
+ attr_reader :client
32
+
33
+ # Create an HTTPBackend for the Riak::Client.
34
+ # @param [Client] client the client
35
+ def initialize(client)
36
+ raise ArgumentError, t("client_type", :client => client) unless Client === client
37
+ @client = client
38
+ end
39
+
40
+ # Pings the server
41
+ # @return [true,false] whether the server is available
42
+ def ping
43
+ get(200, riak_kv_wm_ping, {}, {})
44
+ true
45
+ rescue
46
+ false
47
+ end
48
+
49
+ # Fetches an object by bucket/key
50
+ # @param [Bucket, String] bucket the bucket where the object is
51
+ # stored
52
+ # @param [String] key the key of the object
53
+ # @param [Fixnum, String, Symbol] r the read quorum for the
54
+ # request - how many nodes should concur on the read
55
+ # @return [RObject] the fetched object
56
+ def fetch_object(bucket, key, r=nil)
57
+ bucket = Bucket.new(client, bucket) if String === bucket
58
+ options = r ? {:r => r} : {}
59
+ response = get([200,300],riak_kv_wm_raw, escape(bucket.name), escape(key), options, {})
60
+ load_object(RObject.new(bucket, key), response)
61
+ end
62
+
63
+ # Reloads the data for a given RObject, a special case of {#fetch_object}.
64
+ def reload_object(robject, r = nil)
65
+ options = r ? {:r => r} : {}
66
+ response = get([200,300,304], riak_kv_wm_raw, escape(robject.bucket.name), escape(robject.key), options, reload_headers(robject))
67
+ if response[:code].to_i == 304
68
+ robject
69
+ else
70
+ load_object(robject, response)
71
+ end
72
+ end
73
+
74
+ # Stores an object
75
+ # @param [RObject] robject the object to store
76
+ # @param [true,false] returnbody (false) whether to update the object
77
+ # after write with the new value
78
+ # @param [Fixnum, String, Symbol] w the write quorum
79
+ # @param [Fixnum, String, Symbol] dw the durable write quorum
80
+ def store_object(robject, returnbody=false, w=nil, dw=nil)
81
+ query = {}.tap do |q|
82
+ q[:returnbody] = returnbody unless returnbody.nil?
83
+ q[:w] = w unless w.nil?
84
+ q[:dw] = dw unless dw.nil?
85
+ end
86
+ method, codes, path = if robject.key.present?
87
+ [:put, [200,204,300], "#{escape(robject.bucket.name)}/#{escape(robject.key)}"]
88
+ else
89
+ [:post, 201, escape(robject.bucket.name)]
90
+ end
91
+ response = send(method, codes, riak_kv_wm_raw, path, query, robject.raw_data, store_headers(robject))
92
+ load_object(robject, response) if returnbody
93
+ end
94
+
95
+ # Deletes an object
96
+ # @param [Bucket, String] bucket the bucket where the object
97
+ # lives
98
+ # @param [String] key the key where the object lives
99
+ # @param [Fixnum, String, Symbol] rw the read/write quorum for
100
+ # the request
101
+ def delete_object(bucket, key, rw=nil)
102
+ bucket = bucket.name if Bucket === bucket
103
+ options = rw ? {:rw => rw} : {}
104
+ delete([204, 404], riak_kv_wm_raw, escape(bucket), escape(key), options, {})
105
+ end
106
+
107
+ # Fetches bucket properties
108
+ # @param [Bucket, String] bucket the bucket properties to fetch
109
+ # @return [Hash] bucket properties
110
+ def get_bucket_props(bucket)
111
+ bucket = bucket.name if Bucket === bucket
112
+ response = get(200, riak_kv_wm_raw, escape(bucket), {:keys => false, :props => true}, {})
113
+ JSON.parse(response[:body])['props']
114
+ end
115
+
116
+ # Sets bucket properties
117
+ # @param [Bucket, String] bucket the bucket to set properties on
118
+ # @param [Hash] properties the properties to set
119
+ def set_bucket_props(bucket, props)
120
+ bucket = bucket.name if Bucket === bucket
121
+ body = {'props' => props}.to_json
122
+ put(204, riak_kv_wm_raw, escape(bucket), body, {"Content-Type" => "application/json"})
123
+ end
124
+
125
+ # List keys in a bucket
126
+ # @param [Bucket, String] bucket the bucket to fetch the keys
127
+ # for
128
+ # @yield [Array<String>] a list of keys from the current
129
+ # streamed chunk
130
+ # @return [Array<String>] the list of keys, if no block was given
131
+ def list_keys(bucket, &block)
132
+ bucket = bucket.name if Bucket === bucket
133
+ if block_given?
134
+ get(200, riak_kv_wm_raw, escape(bucket), {:props => false, :keys => 'stream'}, {}, &KeyStreamer.new(block))
135
+ else
136
+ response = get(200, riak_kv_wm_raw, escape(bucket), {:props => false, :keys => true}, {})
137
+ obj = JSON.parse(response[:body])
138
+ obj && obj['keys'].map {|k| unescape(k) }
139
+ end
140
+ end
141
+
142
+ # Lists known buckets
143
+ # @return [Array<String>] the list of buckets
144
+ def list_buckets
145
+ response = get(200, riak_kv_wm_raw, {:buckets => true}, {})
146
+ JSON.parse(response[:body])['buckets']
147
+ end
148
+
149
+ # Performs a MapReduce query.
150
+ # @param [MapReduce] mr the query to perform
151
+ # @yield [Fixnum, Object] the phase number and single result
152
+ # from the phase
153
+ # @return [Array<Object>] the list of results, if no block was
154
+ # given
155
+ def mapred(mr)
156
+ if block_given?
157
+ parser = Riak::Util::Multipart::StreamParser.new do |response|
158
+ result = JSON.parse(response[:body])
159
+ yield result['phase'], result['data']
160
+ end
161
+ post(200, riak_kv_wm_mapred, {:chunked => true}, mr.to_json, {"Content-Type" => "application/json", "Accept" => "application/json"}, &parser)
162
+ nil
163
+ else
164
+ response = post(200, riak_kv_wm_mapred, mr.to_json, {"Content-Type" => "application/json", "Accept" => "application/json"})
165
+ begin
166
+ JSON.parse(response[:body])
167
+ rescue
168
+ response
169
+ end
170
+ end
171
+ end
172
+
173
+ # Gets health statistics
174
+ # @return [Hash] information about the server, including stats
175
+ def stats
176
+ response = get(200, riak_kv_wm_stats, {}, {})
177
+ JSON.parse(response[:body])
178
+ end
179
+
180
+ # Performs a link-walking query
181
+ # @param [RObject] robject the object to start at
182
+ # @param [Array<WalkSpec>] walk_specs a list of walk
183
+ # specifications to process
184
+ # @return [Array<Array<RObject>>] a list of the matched objects,
185
+ # grouped by phase
186
+ def link_walk(robject, walk_specs)
187
+ response = get(200, riak_kv_wm_link_walker, escape(robject.bucket.name), escape(robject.key), walk_specs.join("/"))
188
+ if boundary = Util::Multipart.extract_boundary(response[:headers]['content-type'].first)
189
+ Util::Multipart.parse(response[:body], boundary).map do |group|
190
+ group.map do |obj|
191
+ if obj[:headers] && obj[:body] && obj[:headers]['location']
192
+ bucket = $1 if obj[:headers]['location'].first =~ %r{/.*/(.*)/.*$}
193
+ load_object(RObject.new(client.bucket(bucket), nil), obj)
194
+ end
195
+ end
196
+ end
197
+ else
198
+ []
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,46 @@
1
+
2
+ require 'riak/failed_request'
3
+ require 'riak/client/http_backend'
4
+ require 'riak/link'
5
+
6
+ module Riak
7
+ class Client
8
+ class HTTPBackend
9
+ # Riak 0.14 provides a root URL that enumerates all of the
10
+ # HTTP endpoints and their paths. This module adds methods to
11
+ # auto-discover those endpoints via the root URL.
12
+ module Configuration
13
+ private
14
+ def server_config
15
+ @server_config ||= {}.tap do |hash|
16
+ begin
17
+ response = get(200, "/", {}, {})
18
+ Link.parse(response[:headers]['link'].first).each {|l| hash[l.tag.intern] = l.url }
19
+ rescue Riak::FailedRequest
20
+ end
21
+ end
22
+ end
23
+
24
+ def riak_kv_wm_raw
25
+ server_config[:riak_kv_wm_raw] || client.prefix
26
+ end
27
+
28
+ def riak_kv_wm_link_walker
29
+ server_config[:riak_kv_wm_link_walker] || client.prefix
30
+ end
31
+
32
+ def riak_kv_wm_mapred
33
+ server_config[:riak_kv_wm_mapred] || client.mapred
34
+ end
35
+
36
+ def riak_kv_wm_ping
37
+ server_config[:riak_kv_wm_ping] || "/ping"
38
+ end
39
+
40
+ def riak_kv_wm_stats
41
+ server_config[:riak_kv_wm_stats] || "/stats"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,43 @@
1
+ require 'riak/util/escape'
2
+ require 'riak/json'
3
+
4
+ module Riak
5
+ class Client
6
+ class HTTPBackend
7
+ # @private
8
+ class KeyStreamer
9
+ include Util::Escape
10
+
11
+ def initialize(block)
12
+ @buffer = ""
13
+ @block = block
14
+ end
15
+
16
+ def accept(chunk)
17
+ @buffer << chunk
18
+ consume
19
+ end
20
+
21
+ def to_proc
22
+ method(:accept).to_proc
23
+ end
24
+
25
+ private
26
+ def consume
27
+ while @buffer =~ /\}\{/
28
+ stream($~.pre_match + '}')
29
+ @buffer = '{' + $~.post_match
30
+ end
31
+ end
32
+
33
+ def stream(str)
34
+ obj = JSON.parse(str) rescue nil
35
+ if obj && obj['keys']
36
+ @block.call obj['keys'].map(&method(:unescape))
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
@@ -0,0 +1,94 @@
1
+ require 'uri'
2
+ require 'set'
3
+ require 'time'
4
+ require 'riak/client/http_backend'
5
+ require 'riak/robject'
6
+ require 'riak/link'
7
+ require 'riak/util/multipart'
8
+
9
+ module Riak
10
+ class Client
11
+ class HTTPBackend
12
+ # Methods for assisting with the handling of {RObject}s present
13
+ # in HTTP requests and responses.
14
+ module ObjectMethods
15
+ # HTTP header hash that will be sent along when reloading the object
16
+ # @return [Hash] hash of HTTP headers
17
+ def reload_headers(robject)
18
+ {}.tap do |h|
19
+ h['If-None-Match'] = robject.etag if robject.etag.present?
20
+ h['If-Modified-Since'] = robject.last_modified.httpdate if robject.last_modified.present?
21
+ end
22
+ end
23
+
24
+ # HTTP header hash that will be sent along when storing the object
25
+ # @return [Hash] hash of HTTP Headers
26
+ def store_headers(robject)
27
+ {}.tap do |hash|
28
+ hash["Content-Type"] = robject.content_type
29
+ hash["X-Riak-Vclock"] = robject.vclock if robject.vclock
30
+ if robject.prevent_stale_writes && robject.etag.present?
31
+ hash["If-Match"] = robject.etag
32
+ elsif robject.prevent_stale_writes
33
+ hash["If-None-Match"] = "*"
34
+ end
35
+ unless robject.links.blank?
36
+ hash["Link"] = robject.links.reject {|l| l.rel == "up" }.map(&:to_s).join(", ")
37
+ end
38
+ unless robject.meta.blank?
39
+ robject.meta.each do |k,v|
40
+ hash["X-Riak-Meta-#{k}"] = v.to_s
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ # Load object data from an HTTP response
47
+ # @param [Hash] response a response from {Riak::Client::HTTPBackend}
48
+ def load_object(robject, response)
49
+ extract_header(robject, response, "location", :key) {|v| URI.unescape(v.match(%r{.*/(.*?)(\?.*)?$})[1]) }
50
+ extract_header(robject, response, "content-type", :content_type)
51
+ extract_header(robject, response, "x-riak-vclock", :vclock)
52
+ extract_header(robject, response, "link", :links) {|v| Set.new(Link.parse(v)) }
53
+ extract_header(robject, response, "etag", :etag)
54
+ extract_header(robject, response, "last-modified", :last_modified) {|v| Time.httpdate(v) }
55
+ robject.meta = response[:headers].inject({}) do |h,(k,v)|
56
+ if k =~ /x-riak-meta-(.*)/
57
+ h[$1] = v
58
+ end
59
+ h
60
+ end
61
+ robject.conflict = (response[:code] && response[:code].to_i == 300 && robject.content_type =~ /multipart\/mixed/)
62
+ robject.siblings = robject.conflict? ? extract_siblings(robject, response[:body]) : nil
63
+ robject.raw_data = response[:body] if response[:body].present? && !robject.conflict?
64
+
65
+ robject.conflict? ? robject.attempt_conflict_resolution : robject
66
+ end
67
+
68
+ private
69
+ def extract_header(robject, response, name, attribute=nil, &block)
70
+ extract_if_present(robject, response[:headers], name, attribute) do |value|
71
+ block ? block.call(value[0]) : value[0]
72
+ end
73
+ end
74
+
75
+ def extract_if_present(robject, hash, key, attribute=nil)
76
+ if hash[key].present?
77
+ attribute ||= key
78
+ value = block_given? ? yield(hash[key]) : hash[key]
79
+ robject.send("#{attribute}=", value)
80
+ end
81
+ end
82
+
83
+ def extract_siblings(robject, data)
84
+ Util::Multipart.parse(data, Util::Multipart.extract_boundary(robject.content_type)).map do |part|
85
+ RObject.new(robject.bucket, robject.key) do |sibling|
86
+ load_object(sibling, part)
87
+ sibling.vclock = robject.vclock
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,34 @@
1
+
2
+ require 'riak/util/headers'
3
+
4
+ module Riak
5
+ class Client
6
+ class HTTPBackend
7
+ # @private
8
+ class RequestHeaders < Riak::Util::Headers
9
+ alias each each_capitalized
10
+
11
+ def initialize(hash)
12
+ initialize_http_header(hash)
13
+ end
14
+
15
+ def to_a
16
+ [].tap do |arr|
17
+ each_capitalized do |k,v|
18
+ arr << "#{k}: #{v}"
19
+ end
20
+ end
21
+ end
22
+
23
+ def to_hash
24
+ {}.tap do |hash|
25
+ each_capitalized do |k,v|
26
+ hash[k] ||= []
27
+ hash[k] << v
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,218 @@
1
+
2
+ require 'base64'
3
+ require 'uri'
4
+ require 'riak/client'
5
+ require 'riak/util/headers'
6
+
7
+ module Riak
8
+ class Client
9
+ class HTTPBackend
10
+ # Methods related to performing HTTP requests in a consistent
11
+ # fashion across multiple client libraries. HTTP/1.1 verbs are
12
+ # presented as methods.
13
+ module TransportMethods
14
+ # Performs a HEAD request to the specified resource on the Riak server.
15
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
16
+ # @param [String, Array<String,Hash>] resource a relative path or array of path segments and optional query params Hash that will be joined to the root URI
17
+ # @overload head(expect, *resource)
18
+ # @overload head(expect, *resource, headers)
19
+ # Send the request with custom headers
20
+ # @param [Hash] headers custom headers to send with the request
21
+ # @return [Hash] response data, containing only the :headers and :code keys
22
+ # @raise [FailedRequest] if the response code doesn't match the expected response
23
+ def head(expect, *resource)
24
+ headers = default_headers.merge(resource.extract_options!)
25
+ verify_path!(resource)
26
+ perform(:head, path(*resource), headers, expect)
27
+ end
28
+
29
+ # Performs a GET request to the specified resource on the Riak server.
30
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
31
+ # @param [String, Array<String,Hash>] resource a relative path or array of path segments and optional query params Hash that will be joined to the root URI
32
+ # @overload get(expect, *resource)
33
+ # @overload get(expect, *resource, headers)
34
+ # Send the request with custom headers
35
+ # @param [Hash] headers custom headers to send with the request
36
+ # @overload get(expect, *resource, headers={})
37
+ # Stream the response body through the supplied block
38
+ # @param [Hash] headers custom headers to send with the request
39
+ # @yield [chunk] yields successive chunks of the response body as strings
40
+ # @return [Hash] response data, containing only the :headers and :code keys
41
+ # @return [Hash] response data, containing :headers, :body, and :code keys
42
+ # @raise [FailedRequest] if the response code doesn't match the expected response
43
+ def get(expect, *resource, &block)
44
+ headers = default_headers.merge(resource.extract_options!)
45
+ verify_path!(resource)
46
+ perform(:get, path(*resource), headers, expect, &block)
47
+ end
48
+
49
+ # Performs a PUT request to the specified resource on the Riak server.
50
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
51
+ # @param [String, Array<String,Hash>] resource a relative path or array of path segments and optional query params Hash that will be joined to the root URI
52
+ # @param [String] body the request body to send to the server
53
+ # @overload put(expect, *resource, body)
54
+ # @overload put(expect, *resource, body, headers)
55
+ # Send the request with custom headers
56
+ # @param [Hash] headers custom headers to send with the request
57
+ # @overload put(expect, *resource, body, headers={})
58
+ # Stream the response body through the supplied block
59
+ # @param [Hash] headers custom headers to send with the request
60
+ # @yield [chunk] yields successive chunks of the response body as strings
61
+ # @return [Hash] response data, containing only the :headers and :code keys
62
+ # @return [Hash] response data, containing :headers, :code, and :body keys
63
+ # @raise [FailedRequest] if the response code doesn't match the expected response
64
+ def put(expect, *resource, &block)
65
+ headers = default_headers.merge(resource.extract_options!)
66
+ uri, data = verify_path_and_body!(resource)
67
+ perform(:put, path(*uri), headers, expect, data, &block)
68
+ end
69
+
70
+ # Performs a POST request to the specified resource on the Riak server.
71
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
72
+ # @param [String, Array<String>] resource a relative path or array of path segments that will be joined to the root URI
73
+ # @param [String] body the request body to send to the server
74
+ # @overload post(expect, *resource, body)
75
+ # @overload post(expect, *resource, body, headers)
76
+ # Send the request with custom headers
77
+ # @param [Hash] headers custom headers to send with the request
78
+ # @overload post(expect, *resource, body, headers={})
79
+ # Stream the response body through the supplied block
80
+ # @param [Hash] headers custom headers to send with the request
81
+ # @yield [chunk] yields successive chunks of the response body as strings
82
+ # @return [Hash] response data, containing only the :headers and :code keys
83
+ # @return [Hash] response data, containing :headers, :code and :body keys
84
+ # @raise [FailedRequest] if the response code doesn't match the expected response
85
+ def post(expect, *resource, &block)
86
+ headers = default_headers.merge(resource.extract_options!)
87
+ uri, data = verify_path_and_body!(resource)
88
+ perform(:post, path(*uri), headers, expect, data, &block)
89
+ end
90
+
91
+ # Performs a DELETE request to the specified resource on the Riak server.
92
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
93
+ # @param [String, Array<String,Hash>] resource a relative path or array of path segments and optional query params Hash that will be joined to the root URI
94
+ # @overload delete(expect, *resource)
95
+ # @overload delete(expect, *resource, headers)
96
+ # Send the request with custom headers
97
+ # @param [Hash] headers custom headers to send with the request
98
+ # @overload delete(expect, *resource, headers={})
99
+ # Stream the response body through the supplied block
100
+ # @param [Hash] headers custom headers to send with the request
101
+ # @yield [chunk] yields successive chunks of the response body as strings
102
+ # @return [Hash] response data, containing only the :headers and :code keys
103
+ # @return [Hash] response data, containing :headers, :code and :body keys
104
+ # @raise [FailedRequest] if the response code doesn't match the expected response
105
+ def delete(expect, *resource, &block)
106
+ headers = default_headers.merge(resource.extract_options!)
107
+ verify_path!(resource)
108
+ perform(:delete, path(*resource), headers, expect, &block)
109
+ end
110
+
111
+ # Executes requests according to the underlying HTTP client library semantics.
112
+ # @abstract Subclasses must implement this internal method to perform HTTP requests
113
+ # according to the API of their HTTP libraries.
114
+ # @param [Symbol] method one of :head, :get, :post, :put, :delete
115
+ # @param [URI] uri the HTTP URI to request
116
+ # @param [Hash] headers headers to send along with the request
117
+ # @param [Fixnum, Array] expect the expected response code(s)
118
+ # @param [String, #read] body the PUT or POST request body
119
+ # @return [Hash] response data, containing :headers, :code and :body keys. Only :headers and :code should be present when the body is streamed or the method is :head.
120
+ # @yield [chunk] if the method is not :head, successive chunks of the response body will be yielded as strings
121
+ # @raise [NotImplementedError] if a subclass does not implement this method
122
+ def perform(method, uri, headers, expect, body=nil)
123
+ raise NotImplementedError
124
+ end
125
+
126
+ # Default header hash sent with every request, based on settings in the client
127
+ # @return [Hash] headers that will be merged with user-specified headers on every request
128
+ def default_headers
129
+ {
130
+ "Accept" => "multipart/mixed, application/json;q=0.7, */*;q=0.5",
131
+ "X-Riak-ClientId" => client_id
132
+ }.merge(basic_auth_header)
133
+ end
134
+
135
+ def client_id
136
+ value = @client.client_id
137
+ case value
138
+ when Integer
139
+ b64encode(value)
140
+ when String
141
+ value
142
+ end
143
+ end
144
+
145
+ def basic_auth_header
146
+ @client.basic_auth ? {"Authorization" => "Basic #{Base64::encode64(@client.basic_auth)}"} : {}
147
+ end
148
+
149
+ # @return [URI] The calculated root URI for the Riak HTTP endpoint
150
+ def root_uri
151
+ protocol = client.ssl_enabled? ? "https" : "http"
152
+ URI.parse("#{protocol}://#{client.host}:#{client.http_port}")
153
+ end
154
+
155
+ # Calculates an absolute URI from a relative path specification
156
+ # @param [Array<String,Hash>] segments a relative path or sequence of path segments and optional query params Hash that will be joined to the root URI
157
+ # @return [URI] an absolute URI for the resource
158
+ def path(*segments)
159
+ query = segments.extract_options!.to_param
160
+ root_uri.merge(segments.join("/").gsub(/\/+/, "/").sub(/^\//, '')).tap do |uri|
161
+ uri.query = query if query.present?
162
+ end
163
+ end
164
+
165
+ # Verifies that both a resource path and body are present in the arguments
166
+ # @param [Array] args the arguments to verify
167
+ # @raise [ArgumentError] if the body or resource is missing, or if the body is not a String
168
+ def verify_path_and_body!(args)
169
+ body = args.pop
170
+ begin
171
+ verify_path!(args)
172
+ rescue ArgumentError
173
+ raise ArgumentError, t("path_and_body_required")
174
+ end
175
+
176
+ raise ArgumentError, t("request_body_type") unless String === body || body.respond_to?(:read)
177
+ [args, body]
178
+ end
179
+
180
+ # Verifies that the specified resource is valid
181
+ # @param [String, Array] resource the resource specification
182
+ # @raise [ArgumentError] if the resource path is too short
183
+ def verify_path!(resource)
184
+ resource = Array(resource).flatten
185
+ raise ArgumentError, t("resource_path_short") unless resource.length > 1 || resource.include?(@client.mapred)
186
+ end
187
+
188
+ # Checks the expected response codes against the actual response code. Use internally when
189
+ # implementing {#perform}.
190
+ # @param [String, Fixnum, Array<String,Fixnum>] expected the expected response code(s)
191
+ # @param [String, Fixnum] actual the received response code
192
+ # @return [Boolean] whether the actual response code is acceptable given the expectations
193
+ def valid_response?(expected, actual)
194
+ Array(expected).map(&:to_i).include?(actual.to_i)
195
+ end
196
+
197
+ # Checks whether a combination of the HTTP method, response code, and block should
198
+ # result in returning the :body in the response hash. Use internally when implementing {#perform}.
199
+ # @param [Symbol] method the HTTP method
200
+ # @param [String, Fixnum] code the received response code
201
+ # @param [Boolean] has_block whether a streaming block was passed to {#perform}. Pass block_given? to this parameter.
202
+ # @return [Boolean] whether to return the body in the response hash
203
+ def return_body?(method, code, has_block)
204
+ method != :head && !valid_response?([204,205,304], code) && !has_block
205
+ end
206
+
207
+ private
208
+ def response_headers
209
+ Thread.current[:response_headers] ||= Riak::Util::Headers.new
210
+ end
211
+
212
+ def b64encode(n)
213
+ Base64.encode64([n].pack("N")).chomp
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end