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,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,201 @@
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, headers={})
24
+ headers = default_headers.merge(headers)
25
+ perform(:head, resource, headers, expect)
26
+ end
27
+
28
+ # Performs a GET request to the specified resource on the Riak server.
29
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
30
+ # @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
31
+ # @overload get(expect, *resource)
32
+ # @overload get(expect, *resource, headers)
33
+ # Send the request with custom headers
34
+ # @param [Hash] headers custom headers to send with the request
35
+ # @overload get(expect, *resource, headers={})
36
+ # Stream the response body through the supplied block
37
+ # @param [Hash] headers custom headers to send with the request
38
+ # @yield [chunk] yields successive chunks of the response body as strings
39
+ # @return [Hash] response data, containing only the :headers and :code keys
40
+ # @return [Hash] response data, containing :headers, :body, and :code keys
41
+ # @raise [FailedRequest] if the response code doesn't match the expected response
42
+ def get(expect, resource, headers={}, &block)
43
+ headers = default_headers.merge(headers)
44
+ perform(:get, resource, headers, expect, &block)
45
+ end
46
+
47
+ # Performs a PUT request to the specified resource on the Riak server.
48
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
49
+ # @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
50
+ # @param [String] body the request body to send to the server
51
+ # @overload put(expect, *resource, body)
52
+ # @overload put(expect, *resource, body, headers)
53
+ # Send the request with custom headers
54
+ # @param [Hash] headers custom headers to send with the request
55
+ # @overload put(expect, *resource, body, headers={})
56
+ # Stream the response body through the supplied block
57
+ # @param [Hash] headers custom headers to send with the request
58
+ # @yield [chunk] yields successive chunks of the response body as strings
59
+ # @return [Hash] response data, containing only the :headers and :code keys
60
+ # @return [Hash] response data, containing :headers, :code, and :body keys
61
+ # @raise [FailedRequest] if the response code doesn't match the expected response
62
+ def put(expect, resource, body, headers={}, &block)
63
+ headers = default_headers.merge(headers)
64
+ verify_body!(body)
65
+ perform(:put, resource, headers, expect, body, &block)
66
+ end
67
+
68
+ # Performs a POST request to the specified resource on the Riak server.
69
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
70
+ # @param [String, Array<String>] resource a relative path or array of path segments that will be joined to the root URI
71
+ # @param [String] body the request body to send to the server
72
+ # @overload post(expect, *resource, body)
73
+ # @overload post(expect, *resource, body, headers)
74
+ # Send the request with custom headers
75
+ # @param [Hash] headers custom headers to send with the request
76
+ # @overload post(expect, *resource, body, headers={})
77
+ # Stream the response body through the supplied block
78
+ # @param [Hash] headers custom headers to send with the request
79
+ # @yield [chunk] yields successive chunks of the response body as strings
80
+ # @return [Hash] response data, containing only the :headers and :code keys
81
+ # @return [Hash] response data, containing :headers, :code and :body keys
82
+ # @raise [FailedRequest] if the response code doesn't match the expected response
83
+ def post(expect, resource, body, headers={}, &block)
84
+ headers = default_headers.merge(headers)
85
+ verify_body!(body)
86
+ perform(:post, resource, headers, expect, body, &block)
87
+ end
88
+
89
+ # Performs a DELETE request to the specified resource on the Riak server.
90
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
91
+ # @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
92
+ # @overload delete(expect, *resource)
93
+ # @overload delete(expect, *resource, headers)
94
+ # Send the request with custom headers
95
+ # @param [Hash] headers custom headers to send with the request
96
+ # @overload delete(expect, *resource, headers={})
97
+ # Stream the response body through the supplied block
98
+ # @param [Hash] headers custom headers to send with the request
99
+ # @yield [chunk] yields successive chunks of the response body as strings
100
+ # @return [Hash] response data, containing only the :headers and :code keys
101
+ # @return [Hash] response data, containing :headers, :code and :body keys
102
+ # @raise [FailedRequest] if the response code doesn't match the expected response
103
+ def delete(expect, resource, headers={}, &block)
104
+ headers = default_headers.merge(headers)
105
+ perform(:delete, resource, headers, expect, &block)
106
+ end
107
+
108
+ # Executes requests according to the underlying HTTP client library semantics.
109
+ # @abstract Subclasses must implement this internal method to perform HTTP requests
110
+ # according to the API of their HTTP libraries.
111
+ # @param [Symbol] method one of :head, :get, :post, :put, :delete
112
+ # @param [URI] uri the HTTP URI to request
113
+ # @param [Hash] headers headers to send along with the request
114
+ # @param [Fixnum, Array] expect the expected response code(s)
115
+ # @param [String, #read] body the PUT or POST request body
116
+ # @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.
117
+ # @yield [chunk] if the method is not :head, successive chunks of the response body will be yielded as strings
118
+ # @raise [NotImplementedError] if a subclass does not implement this method
119
+ def perform(method, uri, headers, expect, body=nil)
120
+ raise NotImplementedError
121
+ end
122
+
123
+ # Default header hash sent with every request, based on settings in the client
124
+ # @return [Hash] headers that will be merged with user-specified headers on every request
125
+ def default_headers
126
+ {
127
+ "Accept" => "multipart/mixed, application/json;q=0.7, */*;q=0.5",
128
+ "X-Riak-ClientId" => client_id
129
+ }.merge(basic_auth_header)
130
+ end
131
+
132
+ def client_id
133
+ value = @client.client_id
134
+ case value
135
+ when Integer
136
+ b64encode(value)
137
+ when String
138
+ value
139
+ end
140
+ end
141
+
142
+ def basic_auth_header
143
+ @node.basic_auth ? {"Authorization" => "Basic #{Base64::encode64(@node.basic_auth)}"} : {}
144
+ end
145
+
146
+ # @return [URI] The calculated root URI for the Riak HTTP endpoint
147
+ def root_uri
148
+ protocol = node.ssl_enabled? ? "https" : "http"
149
+ URI.parse("#{protocol}://#{node.host}:#{node.http_port}")
150
+ end
151
+
152
+ # Calculates an absolute URI from a relative path specification
153
+ # @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
154
+ # @return [URI] an absolute URI for the resource
155
+ def path(*segments)
156
+ segments = segments.flatten
157
+ query = segments.extract_options!.to_param
158
+ root_uri.merge(segments.join("/").gsub(/\/+/, "/").sub(/^\//, '')).tap do |uri|
159
+ uri.query = query if query.present?
160
+ end
161
+ end
162
+
163
+ # Checks the expected response codes against the actual response code. Use internally when
164
+ # implementing {#perform}.
165
+ # @param [String, Fixnum, Array<String,Fixnum>] expected the expected response code(s)
166
+ # @param [String, Fixnum] actual the received response code
167
+ # @return [Boolean] whether the actual response code is acceptable given the expectations
168
+ def valid_response?(expected, actual)
169
+ Array(expected).map(&:to_i).include?(actual.to_i)
170
+ end
171
+
172
+ # Checks whether a combination of the HTTP method, response code, and block should
173
+ # result in returning the :body in the response hash. Use internally when implementing {#perform}.
174
+ # @param [Symbol] method the HTTP method
175
+ # @param [String, Fixnum] code the received response code
176
+ # @param [Boolean] has_block whether a streaming block was passed to {#perform}. Pass block_given? to this parameter.
177
+ # @return [Boolean] whether to return the body in the response hash
178
+ def return_body?(method, code, has_block)
179
+ method != :head && !valid_response?([204,205,304], code) && !has_block
180
+ end
181
+
182
+ # Checks whether the submitted body is valid. That is, it must
183
+ # be a String or respond to the 'read' method.
184
+ # @param [String, #read] body the body
185
+ # @raise [ArgumentError] if the body is of invalid type
186
+ def verify_body!(body)
187
+ raise ArgumentError, t('request_body_type') unless String === body || body.respond_to?(:read)
188
+ end
189
+
190
+ private
191
+ def response_headers
192
+ Thread.current[:response_headers] ||= Riak::Util::Headers.new
193
+ end
194
+
195
+ def b64encode(n)
196
+ Base64.encode64([n].pack("N")).chomp
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,340 @@
1
+ require 'riak/util/escape'
2
+ require 'riak/util/translation'
3
+ require 'riak/util/multipart'
4
+ require 'riak/util/multipart/stream_parser'
5
+ require 'riak/json'
6
+ require 'riak/client'
7
+ require 'riak/bucket'
8
+ require 'riak/robject'
9
+ require 'riak/client/http_backend/transport_methods'
10
+ require 'riak/client/http_backend/object_methods'
11
+ require 'riak/client/http_backend/configuration'
12
+ require 'riak/client/http_backend/key_streamer'
13
+ require 'riak/client/feature_detection'
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
+ include FeatureDetection
26
+
27
+ include TransportMethods
28
+ include ObjectMethods
29
+ include Configuration
30
+
31
+ # The Riak::Client that uses this backend
32
+ attr_reader :client
33
+
34
+ # The Riak::Client::Node that uses this backend
35
+ attr_reader :node
36
+
37
+ # Create an HTTPBackend for the Riak::Client.
38
+ # @param [Client] The client
39
+ # @param [Node] The node we're connecting to.
40
+ def initialize(client, node)
41
+ raise ArgumentError, t("client_type", :client => client) unless Client === client
42
+ raise ArgumentError, t("node_type", :node => node) unless Node === node
43
+ @client = client
44
+ @node = node
45
+ end
46
+
47
+ # Pings the server
48
+ # @return [true,false] whether the server is available
49
+ def ping
50
+ get(200, ping_path)
51
+ true
52
+ rescue
53
+ false
54
+ end
55
+
56
+ # Fetches an object by bucket/key
57
+ # @param [Bucket, String] bucket the bucket where the object is
58
+ # stored
59
+ # @param [String] key the key of the object
60
+ # @param [Hash] options request quorums
61
+ # @option options [Fixnum, String, Symbol] :r the read quorum for the
62
+ # request - how many nodes should concur on the read
63
+ # @option options [Fixnum, String, Symbol] :pr the "primary"
64
+ # read quorum for the request - how many primary partitions
65
+ # must be available
66
+ # @return [RObject] the fetched object
67
+ def fetch_object(bucket, key, options={})
68
+ bucket = Bucket.new(client, bucket) if String === bucket
69
+ response = get([200,300], object_path(bucket.name, key, options))
70
+ load_object(RObject.new(bucket, key), response)
71
+ end
72
+
73
+ # Reloads the data for a given RObject, a special case of {#fetch_object}.
74
+ def reload_object(robject, options={})
75
+ response = get([200,300,304], object_path(robject.bucket.name, robject.key, options), reload_headers(robject))
76
+ if response[:code].to_i == 304
77
+ robject
78
+ else
79
+ load_object(robject, response)
80
+ end
81
+ end
82
+
83
+ # Stores an object
84
+ # @param [RObject] robject the object to store
85
+ # @param [Hash] options quorum and storage options
86
+ # @option options [true,false] :returnbody (false) whether to update the object
87
+ # after write with the new value
88
+ # @option options [Fixnum, String, Symbol] :w the write quorum
89
+ # @option options [Fixnum, String, Symbol] :pw the "primary"
90
+ # write quorum - how many primary partitions must be available
91
+ # @option options [Fixnum, String, Symbol] :dw the durable write quorum
92
+ def store_object(robject, options={})
93
+ method, codes = if robject.key.present?
94
+ [:put, [200,204,300]]
95
+ else
96
+ [:post, 201]
97
+ end
98
+ response = send(method, codes, object_path(robject.bucket.name, robject.key, options), robject.raw_data, store_headers(robject))
99
+ load_object(robject, response) if options[:returnbody]
100
+ end
101
+
102
+ # Deletes an object
103
+ # @param [Bucket, String] bucket the bucket where the object
104
+ # lives
105
+ # @param [String] key the key where the object lives
106
+ # @param [Hash] options quorum and delete options
107
+ # @options options [Fixnum, String, Symbol] :rw the read/write quorum for
108
+ # the request
109
+ # @options options [String] :vclock the vector clock of the
110
+ # object to be deleted
111
+ def delete_object(bucket, key, options={})
112
+ bucket = bucket.name if Bucket === bucket
113
+ vclock = options.delete(:vclock)
114
+ headers = vclock ? {"X-Riak-VClock" => vclock} : {}
115
+ delete([204, 404], object_path(bucket, key, options), headers)
116
+ end
117
+
118
+ # Fetches bucket properties
119
+ # @param [Bucket, String] bucket the bucket properties to fetch
120
+ # @return [Hash] bucket properties
121
+ def get_bucket_props(bucket)
122
+ bucket = bucket.name if Bucket === bucket
123
+ response = get(200, bucket_properties_path(bucket))
124
+ JSON.parse(response[:body])['props']
125
+ end
126
+
127
+ # Sets bucket properties
128
+ # @param [Bucket, String] bucket the bucket to set properties on
129
+ # @param [Hash] properties the properties to set
130
+ def set_bucket_props(bucket, props)
131
+ bucket = bucket.name if Bucket === bucket
132
+ body = {'props' => props}.to_json
133
+ put(204, bucket_properties_path(bucket), body, {"Content-Type" => "application/json"})
134
+ end
135
+
136
+ # List keys in a bucket
137
+ # @param [Bucket, String] bucket the bucket to fetch the keys
138
+ # for
139
+ # @yield [Array<String>] a list of keys from the current
140
+ # streamed chunk
141
+ # @return [Array<String>] the list of keys, if no block was given
142
+ def list_keys(bucket, &block)
143
+ bucket = bucket.name if Bucket === bucket
144
+ if block_given?
145
+ get(200, key_list_path(bucket, :keys => 'stream'), {}, &KeyStreamer.new(block))
146
+ else
147
+ response = get(200, key_list_path(bucket))
148
+ obj = JSON.parse(response[:body])
149
+ obj && obj['keys'].map {|k| unescape(k) }
150
+ end
151
+ end
152
+
153
+ # Lists known buckets
154
+ # @return [Array<String>] the list of buckets
155
+ def list_buckets
156
+ response = get(200, bucket_list_path)
157
+ JSON.parse(response[:body])['buckets']
158
+ end
159
+
160
+ # Performs a MapReduce query.
161
+ # @param [MapReduce] mr the query to perform
162
+ # @yield [Fixnum, Object] the phase number and single result
163
+ # from the phase
164
+ # @return [Array<Object>] the list of results, if no block was
165
+ # given
166
+ def mapred(mr)
167
+ raise MapReduceError.new(t("empty_map_reduce_query")) if mr.query.empty? && !mapred_phaseless?
168
+ if block_given?
169
+ parser = Riak::Util::Multipart::StreamParser.new do |response|
170
+ result = JSON.parse(response[:body])
171
+ yield result['phase'], result['data']
172
+ end
173
+ post(200, mapred_path({:chunked => true}), mr.to_json, {"Content-Type" => "application/json", "Accept" => "application/json"}, &parser)
174
+ nil
175
+ else
176
+ response = post(200, mapred_path, mr.to_json, {"Content-Type" => "application/json", "Accept" => "application/json"})
177
+ begin
178
+ JSON.parse(response[:body])
179
+ rescue
180
+ response
181
+ end
182
+ end
183
+ end
184
+
185
+ # Gets health statistics
186
+ # @return [Hash] information about the server, including stats
187
+ def stats
188
+ response = get(200, stats_path)
189
+ JSON.parse(response[:body])
190
+ end
191
+
192
+ # Performs a link-walking query
193
+ # @param [RObject] robject the object to start at
194
+ # @param [Array<WalkSpec>] walk_specs a list of walk
195
+ # specifications to process
196
+ # @return [Array<Array<RObject>>] a list of the matched objects,
197
+ # grouped by phase
198
+ def link_walk(robject, walk_specs)
199
+ response = get(200, link_walk_path(robject.bucket.name, robject.key, walk_specs))
200
+ if boundary = Util::Multipart.extract_boundary(response[:headers]['content-type'].first)
201
+ Util::Multipart.parse(response[:body], boundary).map do |group|
202
+ group.map do |obj|
203
+ if obj[:headers] && !obj[:headers]['x-riak-deleted'] && !obj[:body].blank? && obj[:headers]['location']
204
+ link = Riak::Link.new(obj[:headers]['location'].first, "")
205
+ load_object(RObject.new(client.bucket(link.bucket), link.key), obj)
206
+ end
207
+ end.compact
208
+ end
209
+ else
210
+ []
211
+ end
212
+ end
213
+
214
+ # Performs a secondary-index query.
215
+ # @param [String, Bucket] bucket the bucket to query
216
+ # @param [String] index the index to query
217
+ # @param [String, Integer, Range] query the equality query or
218
+ # range query to perform
219
+ # @return [Array<String>] a list of keys matching the query
220
+ def get_index(bucket, index, query)
221
+ bucket = bucket.name if Bucket === bucket
222
+ path = case query
223
+ when Range
224
+ raise ArgumentError, t('invalid_index_query', :value => query.inspect) unless String === query.begin || Integer === query.end
225
+ index_range_path(bucket, index, query.begin, query.end)
226
+ when String, Integer
227
+ index_eq_path(bucket, index, query)
228
+ else
229
+ raise ArgumentError, t('invalid_index_query', :value => query.inspect)
230
+ end
231
+ response = get(200, path)
232
+ JSON.parse(response[:body])['keys']
233
+ end
234
+
235
+ # (Riak Search) Performs a search query
236
+ # @param [String,nil] index the index to query, or nil for the
237
+ # default
238
+ # @param [String] query the Lucene query to perform
239
+ # @param [Hash] options query options
240
+ # @see Client#search
241
+ def search(index, query, options={})
242
+ response = get(200, solr_select_path(index, query, options.stringify_keys))
243
+ if response[:headers]['content-type'].include?("application/json")
244
+ normalize_search_response JSON.parse(response[:body])
245
+ else
246
+ response[:body]
247
+ end
248
+ end
249
+
250
+ # (Riak Search) Updates a search index (includes deletes).
251
+ # @param [String, nil] index the index to update, or nil for the
252
+ # default index.
253
+ # @param [String] updates an XML update string in Solr's required format
254
+ # @see Client#index
255
+ def update_search_index(index, updates)
256
+ post(200, solr_update_path(index), updates, {'Content-Type' => 'text/xml'})
257
+ end
258
+
259
+ # (Luwak) Fetches a file from the Luwak large-file interface.
260
+ # @param [String] filename the name of the file
261
+ # @yield [chunk] A block which will receive individual chunks of
262
+ # the file as they are streamed
263
+ # @yieldparam [String] chunk a block of the file
264
+ # @return [IO, nil] the file (also having content_type and
265
+ # original_filename accessors). The file will need to be
266
+ # reopened to be read
267
+ def get_file(filename, &block)
268
+ if block_given?
269
+ get(200, luwak_path(filename), &block)
270
+ nil
271
+ else
272
+ tmpfile = LuwakFile.new(escape(filename))
273
+ begin
274
+ response = get(200, luwak_path(filename)) do |chunk|
275
+ tmpfile.write chunk
276
+ end
277
+ tmpfile.content_type = response[:headers]['content-type'].first
278
+ tmpfile
279
+ ensure
280
+ tmpfile.close
281
+ end
282
+ end
283
+ end
284
+
285
+ # (Luwak) Detects whether a file exists in the Luwak large-file
286
+ # interface.
287
+ # @param [String] filename the name of the file
288
+ # @return [true,false] whether the file exists
289
+ def file_exists?(filename)
290
+ result = head([200,404], luwak_path(filename))
291
+ result[:code] == 200
292
+ end
293
+
294
+ # (Luwak) Deletes a file from the Luwak large-file interface.
295
+ # @param [String] filename the name of the file
296
+ def delete_file(filename)
297
+ delete([204,404], luwak_path(filename))
298
+ end
299
+
300
+ # (Luwak) Uploads a file to the Luwak large-file interface.
301
+ # @overload store_file(filename, content_type, data)
302
+ # Stores the file at the given key/filename
303
+ # @param [String] filename the key/filename for the object
304
+ # @param [String] content_type the MIME Content-Type for the data
305
+ # @param [IO, String] data the contents of the file
306
+ # @overload store_file(content_type, data)
307
+ # Stores the file with a server-determined key/filename
308
+ # @param [String] content_type the MIME Content-Type for the data
309
+ # @param [String, #read] data the contents of the file
310
+ # @return [String] the key/filename where the object was stored
311
+ def store_file(*args)
312
+ data, content_type, filename = args.reverse
313
+ if filename
314
+ put(204, luwak_path(filename), data, {"Content-Type" => content_type})
315
+ filename
316
+ else
317
+ response = post(201, luwak_path(nil), data, {"Content-Type" => content_type})
318
+ response[:headers]["location"].first.split("/").last
319
+ end
320
+ end
321
+
322
+ private
323
+ def normalize_search_response(json)
324
+ {}.tap do |result|
325
+ if json['response']
326
+ result['num_found'] = json['response']['numFound']
327
+ result['max_score'] = json['response']['maxScore'].to_f
328
+ result['docs'] = json['response']['docs'].map do |d|
329
+ if d['fields']
330
+ d['fields'].merge('id' => d['id'])
331
+ else
332
+ d
333
+ end
334
+ end
335
+ end
336
+ end
337
+ end
338
+ end
339
+ end
340
+ end