riak-client 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -9,7 +9,7 @@ gemspec = Gem::Specification.new do |gem|
9
9
  gem.email = "seancribbs@gmail.com"
10
10
  gem.homepage = "http://seancribbs.github.com/ripple"
11
11
  gem.authors = ["Sean Cribbs"]
12
- gem.add_development_dependency "rspec", "~>2.0.0.beta.11"
12
+ gem.add_development_dependency "rspec", "~>2.0.0"
13
13
  gem.add_development_dependency "fakeweb", ">=1.2"
14
14
  gem.add_development_dependency "rack", ">=1.0"
15
15
  gem.add_development_dependency "curb", ">=0.6"
@@ -27,6 +27,7 @@ gemspec = Gem::Specification.new do |gem|
27
27
  files.exclude '**/*.rej'
28
28
  files.exclude /^pkg/
29
29
  files.exclude 'riak-client.gemspec'
30
+ files.exclude 'spec/support/test_server.yml'
30
31
 
31
32
  gem.files = files.to_a
32
33
 
@@ -12,6 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  require 'riak'
15
+ require 'tempfile'
16
+ require 'delegate'
15
17
 
16
18
  module Riak
17
19
  # A client connection to Riak.
@@ -44,6 +46,9 @@ module Riak
44
46
  # @return [String] The URL path to the map-reduce HTTP endpoint
45
47
  attr_accessor :mapred
46
48
 
49
+ # @return [String] The URL path to the luwak HTTP endpoint
50
+ attr_accessor :luwak
51
+
47
52
  # Creates a client connection to Riak
48
53
  # @param [Hash] options configuration options for the client
49
54
  # @option options [String] :host ('127.0.0.1') The host or IP address for the Riak endpoint
@@ -53,12 +58,13 @@ module Riak
53
58
  # @option options [Fixnum, String] :client_id (rand(MAX_CLIENT_ID)) The internal client ID used by Riak to route responses
54
59
  # @raise [ArgumentError] raised if any options are invalid
55
60
  def initialize(options={})
56
- options.assert_valid_keys(:host, :port, :prefix, :client_id, :mapred)
61
+ options.assert_valid_keys(:host, :port, :prefix, :client_id, :mapred, :luwak)
57
62
  self.host = options[:host] || "127.0.0.1"
58
63
  self.port = options[:port] || 8098
59
64
  self.client_id = options[:client_id] || make_client_id
60
65
  self.prefix = options[:prefix] || "/riak/"
61
66
  self.mapred = options[:mapred] || "/mapred"
67
+ self.luwak = options[:luwak] || "/luwak"
62
68
  raise ArgumentError, t("missing_host_and_port") unless @host && @port
63
69
  end
64
70
 
@@ -122,6 +128,73 @@ module Riak
122
128
  end
123
129
  alias :[] :bucket
124
130
 
131
+ # Stores a large file/IO object in Riak via the "Luwak" interface.
132
+ # @overload store_file(filename, content_type, data)
133
+ # Stores the file at the given key/filename
134
+ # @param [String] filename the key/filename for the object
135
+ # @param [String] content_type the MIME Content-Type for the data
136
+ # @param [IO, String] data the contents of the file
137
+ # @overload store_file(content_type, data)
138
+ # Stores the file with a server-determined key/filename
139
+ # @param [String] content_type the MIME Content-Type for the data
140
+ # @param [IO, String] data the contents of the file
141
+ # @return [String] the key/filename where the object was stored
142
+ def store_file(*args)
143
+ data, content_type, filename = args.reverse
144
+ if filename
145
+ http.put(204, luwak, escape(filename), data, {"Content-Type" => content_type})
146
+ filename
147
+ else
148
+ response = http.post(201, luwak, data, {"Content-Type" => content_type})
149
+ response[:headers]["location"].first.split("/").last
150
+ end
151
+ end
152
+
153
+ # Retrieves a large file/IO object from Riak via the "Luwak"
154
+ # interface. Streams the data to a temporary file unless a block
155
+ # is given.
156
+ # @param [String] filename the key/filename for the object
157
+ # @return [IO, nil] the file (also having content_type and
158
+ # original_filename accessors). The file will need to be
159
+ # reopened to be read. nil will be returned if a block is given.
160
+ # @yield [chunk] stream contents of the file through the
161
+ # block. Passing the block will result in nil being returned
162
+ # from the method.
163
+ # @yieldparam [String] chunk a single chunk of the object's data
164
+ def get_file(filename, &block)
165
+ if block_given?
166
+ http.get(200, luwak, escape(filename), &block)
167
+ nil
168
+ else
169
+ tmpfile = LuwakFile.new(escape(filename))
170
+ begin
171
+ response = http.get(200, luwak, escape(filename)) do |chunk|
172
+ tmpfile.write chunk
173
+ end
174
+ tmpfile.content_type = response[:headers]['content-type'].first
175
+ tmpfile
176
+ ensure
177
+ tmpfile.close
178
+ end
179
+ end
180
+ end
181
+
182
+ # Deletes a file stored via the "Luwak" interface
183
+ # @param [String] filename the key/filename to delete
184
+ def delete_file(filename)
185
+ http.delete([204,404], luwak, escape(filename))
186
+ true
187
+ end
188
+
189
+ # Checks whether a file exists in "Luwak".
190
+ # @param [String] key the key to check
191
+ # @return [true, false] whether the key exists in "Luwak"
192
+ def file_exists?(key)
193
+ result = http.head([200,404], luwak, escape(key))
194
+ result[:code] == 200
195
+ end
196
+ alias :file_exist? :file_exists?
197
+
125
198
  # @return [String] A representation suitable for IRB and debugging output.
126
199
  def inspect
127
200
  "#<Riak::Client #{http.root_uri.to_s}>"
@@ -135,5 +208,15 @@ module Riak
135
208
  def b64encode(n)
136
209
  Base64.encode64([n].pack("N")).chomp
137
210
  end
211
+
212
+ # @private
213
+ class LuwakFile < DelegateClass(Tempfile)
214
+ attr_accessor :original_filename, :content_type
215
+ alias :key :original_filename
216
+ def initialize(fn)
217
+ super(Tempfile.new(fn))
218
+ @original_filename = fn
219
+ end
220
+ end
138
221
  end
139
222
  end
@@ -16,6 +16,7 @@ en:
16
16
  bucket_link_conversion: "Can't convert a bucket link to a walk spec"
17
17
  client_type: "invalid argument %{client} is not a Riak::Client"
18
18
  content_type_undefined: "content_type is not defined!"
19
+ empty_map_reduce_query: "Specify one or more query phases to your MapReduce."
19
20
  failed_request: "Expected %{expected} from Riak but received %{code}. %{body}"
20
21
  hash_type: "invalid argument %{hash} is not a Hash"
21
22
  hostname_invalid: "host must be a valid hostname"
@@ -30,6 +31,8 @@ en:
30
31
  port_invalid: "port must be an integer between 0 and 65535"
31
32
  request_body_type: "Request body must be a string or IO."
32
33
  resource_path_short: "Resource path too short"
34
+ search_docs_require_id: "Search index documents must include the 'id' field."
35
+ search_remove_requires_id_or_query: "Search index documents to be removed must have 'id' or 'query' keys."
33
36
  stored_function_invalid: "function must have :bucket and :key when a hash"
34
37
  string_type: "invalid_argument %{string} is not a String"
35
38
  too_few_arguments: "too few arguments: %{params}"
@@ -18,7 +18,7 @@ module Riak
18
18
  class MapReduce
19
19
  include Util::Translation
20
20
  include Util::Escape
21
-
21
+
22
22
  # @return [Array<[bucket,key]>,String] The bucket/keys for input to the job, or the bucket (all keys).
23
23
  # @see #add
24
24
  attr_accessor :inputs
@@ -134,11 +134,11 @@ module Riak
134
134
  return self
135
135
  end
136
136
  alias :timeout= :timeout
137
-
137
+
138
138
  # Convert the job to JSON for submission over the HTTP interface.
139
139
  # @return [String] the JSON representation
140
140
  def to_json(options={})
141
- hash = {"inputs" => inputs, "query" => query.map(&:as_json)}
141
+ hash = {"inputs" => inputs.as_json, "query" => query.map(&:as_json)}
142
142
  hash['timeout'] = @timeout.to_i if @timeout
143
143
  ActiveSupport::JSON.encode(hash, options)
144
144
  end
@@ -146,6 +146,7 @@ module Riak
146
146
  # Executes this map-reduce job.
147
147
  # @return [Array<Array>] similar to link-walking, each element is an array of results from a phase where "keep" is true. If there is only one "keep" phase, only the results from that phase will be returned.
148
148
  def run
149
+ raise MapReduceError.new(t("empty_map_reduce_query")) if @query.empty?
149
150
  response = @client.http.post(200, @client.mapred, to_json, {"Content-Type" => "application/json", "Accept" => "application/json"})
150
151
  if response.try(:[], :headers).try(:[],'content-type').include?("application/json")
151
152
  ActiveSupport::JSON.decode(response[:body])
@@ -0,0 +1,157 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'riak'
16
+ require 'riak/client'
17
+ require 'riak/bucket'
18
+ require 'riak/map_reduce'
19
+ require 'builder' # Needed to generate Solr XML
20
+
21
+ module Riak
22
+ class Client
23
+ # @return [String] The URL path prefix to the Solr HTTP endpoint
24
+ attr_accessor :solr
25
+
26
+ # @private
27
+ alias :initialize_core :initialize
28
+ # @option options [String] :solr ('/solr') The URL path prefix to the Solr HTTP endpoint
29
+ def initialize(options={})
30
+ self.solr = options.delete(:solr) || "/solr"
31
+ initialize_core(options)
32
+ end
33
+
34
+ # Performs a search via the Solr interface.
35
+ # @overload search(index, query, options={})
36
+ # @param [String] index the index to query on
37
+ # @param [String] query a Lucene query string
38
+ # @overload search(query, options={})
39
+ # Queries the default index
40
+ # @param [String] query a Lucene query string
41
+ # @param [Hash] options extra options for the Solr query
42
+ # @option options [String] :df the default field to search in
43
+ # @option options [String] :'q.op' the default operator between terms ("or", "and")
44
+ # @option options [String] :wt ("json") the response type - "json" and "xml" are valid
45
+ # @option options [String] :sort ('none') the field and direction to sort, e.g. "name asc"
46
+ # @option options [Fixnum] :start (0) the offset into the query to start from, e.g. for pagination
47
+ # @option options [Fixnum] :rows (10) the number of results to return
48
+ # @return [Hash] the query result, containing the 'responseHeaders' and 'response' keys
49
+ def search(*args)
50
+ options = args.extract_options!
51
+ index, query = args[-2], args[-1] # Allows nil index, while keeping it as first argument
52
+ path = [solr, index, "select", {"q" => query, "wt" => "json"}.merge(options.stringify_keys), {}].compact
53
+ response = http.get(200, *path)
54
+ if response[:headers]['content-type'].include?("application/json")
55
+ ActiveSupport::JSON.decode(response[:body])
56
+ else
57
+ response[:body]
58
+ end
59
+ end
60
+ alias :select :search
61
+
62
+ # Adds documents to a search index via the Solr interface.
63
+ # @overload index(index, *docs)
64
+ # Adds documents to the specified search index
65
+ # @param [String] index the index in which to add/update the given documents
66
+ # @param [Array<Hash>] docs unnested document hashes, with one key per field
67
+ # @overload index(*docs)
68
+ # Adds documents to the default search index
69
+ # @param [Array<Hash>] docs unnested document hashes, with one key per field
70
+ # @raise [ArgumentError] if any documents don't include 'id' key
71
+ def index(*args)
72
+ index = args.shift if String === args.first # Documents must be hashes of fields
73
+ raise ArgumentError.new(t("search_docs_require_id")) unless args.all? {|d| d.key?("id") || d.key?(:id) }
74
+ xml = Builder::XmlMarkup.new
75
+ xml.add do
76
+ args.each do |doc|
77
+ xml.doc do
78
+ doc.each do |k,v|
79
+ xml.field('name' => k.to_s) { xml.text!(v.to_s) }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ path = [solr, index, "update", xml.target!, {'Content-Type' => 'text/xml'}].compact
85
+ http.post(200, *path)
86
+ true
87
+ end
88
+ alias :add_doc :index
89
+
90
+ # Removes documents from a search index via the Solr interface.
91
+ # @overload remove(index, specs)
92
+ # Removes documents from the specified index
93
+ # @param [String] index the index from which to remove documents
94
+ # @param [Array<Hash>] specs the specificaiton of documents to remove (must contain 'id' or 'query' keys)
95
+ # @overload remove(specs)
96
+ # Removes documents from the default index
97
+ # @param [Array<Hash>] specs the specification of documents to remove (must contain 'id' or 'query' keys)
98
+ # @raise [ArgumentError] if any document specs don't include 'id' or 'query' keys
99
+ def remove(*args)
100
+ index = args.shift if String === args.first
101
+ raise ArgumentError.new(t("search_remove_requires_id_or_query")) unless args.all? {|s| s.stringify_keys.key?("id") || s.stringify_keys.key?("query") }
102
+ xml = Builder::XmlMarkup.new
103
+ xml.delete do
104
+ args.each do |spec|
105
+ spec.each do |k,v|
106
+ xml.tag!(k.to_sym, v)
107
+ end
108
+ end
109
+ end
110
+ path = [solr, index, "update", xml.target!, {'Content-Type' => 'text/xml'}].compact
111
+ http.post(200, *path)
112
+ true
113
+ end
114
+ alias :delete_doc :remove
115
+ alias :deindex :remove
116
+ end
117
+
118
+ class Bucket
119
+ # The precommit specification for kv/search integration
120
+ SEARCH_PRECOMMIT_HOOK = {"mod" => "riak_search_kv_hook", "fun" => "precommit"}
121
+
122
+ # Installs a precommit hook that automatically indexes objects
123
+ # into riak_search.
124
+ def enable_index!
125
+ unless is_indexed?
126
+ self.props = {"precommit" => (props['precommit'] + [SEARCH_PRECOMMIT_HOOK])}
127
+ end
128
+ end
129
+
130
+ # Removes the precommit hook that automatically indexes objects
131
+ # into riak_search.
132
+ def disable_index!
133
+ if is_indexed?
134
+ self.props = {"precommit" => (props['precommit'] - [SEARCH_PRECOMMIT_HOOK])}
135
+ end
136
+ end
137
+
138
+ # Detects whether the bucket is automatically indexed into
139
+ # riak_search.
140
+ # @return [true,false] whether the bucket includes the search indexing hook
141
+ def is_indexed?
142
+ props['precommit'].include?(SEARCH_PRECOMMIT_HOOK)
143
+ end
144
+ end
145
+
146
+ class MapReduce
147
+ # Use a search query to start a map/reduce job.
148
+ # @param [String, Bucket] bucket the bucket/index to search
149
+ # @param [String] query the query to run
150
+ # @return [MapReduce] self
151
+ def search(bucket, query)
152
+ bucket = bucket.name if bucket.respond_to?(:name)
153
+ @inputs = {:module => "riak_search", :function => "mapred_search", :arg => [bucket, query]}
154
+ self
155
+ end
156
+ end
157
+ end
@@ -1,4 +1,4 @@
1
- 60# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -62,6 +62,15 @@ describe Riak::Client do
62
62
  it "should default the mapreduce path to /mapred if not specified" do
63
63
  Riak::Client.new.mapred.should == "/mapred"
64
64
  end
65
+
66
+ it "should accept a luwak path" do
67
+ client = Riak::Client.new(:luwak => "/beans")
68
+ client.luwak.should == "/beans"
69
+ end
70
+
71
+ it "should default the luwak path to /luwak if not specified" do
72
+ Riak::Client.new.luwak.should == "/luwak"
73
+ end
65
74
  end
66
75
 
67
76
  describe "reconfiguring" do
@@ -171,4 +180,87 @@ describe Riak::Client do
171
180
  @client.bucket("foo/bar ", :keys => false)
172
181
  end
173
182
  end
183
+
184
+ describe "storing a file" do
185
+ before :each do
186
+ @client = Riak::Client.new
187
+ @http = mock(Riak::Client::HTTPBackend)
188
+ @client.stub!(:http).and_return(@http)
189
+ end
190
+
191
+ it "should store the file in Luwak and return the key/filename when no filename is given" do
192
+ @http.should_receive(:post).with(201, "/luwak", anything, {"Content-Type" => "text/plain"}).and_return(:code => 201, :headers => {"location" => ["/luwak/123456789"]})
193
+ @client.store_file("text/plain", "Hello, world").should == "123456789"
194
+ end
195
+
196
+ it "should store the file in Luwak and return the key/filename when the filename is given" do
197
+ @http.should_receive(:put).with(204, "/luwak", "greeting.txt", anything, {"Content-Type" => "text/plain"}).and_return(:code => 204, :headers => {})
198
+ @client.store_file("greeting.txt", "text/plain", "Hello, world").should == "greeting.txt"
199
+ end
200
+ end
201
+
202
+ describe "retrieving a file" do
203
+ before :each do
204
+ @client = Riak::Client.new
205
+ @http = mock(Riak::Client::HTTPBackend)
206
+ @client.stub!(:http).and_return(@http)
207
+ @http.should_receive(:get).with(200, "/luwak", "greeting.txt").and_yield("Hello,").and_yield(" world!").and_return({:code => 200, :headers => {"content-type" => ["text/plain"]}})
208
+ end
209
+
210
+ it "should stream the data to a temporary file" do
211
+ file = @client.get_file("greeting.txt")
212
+ file.open {|f| f.read.should == "Hello, world!" }
213
+ end
214
+
215
+ it "should stream the data through the given block, returning nil" do
216
+ string = ""
217
+ result = @client.get_file("greeting.txt"){|chunk| string << chunk }
218
+ result.should be_nil
219
+ string.should == "Hello, world!"
220
+ end
221
+
222
+ it "should expose the original key and content-type on the temporary file" do
223
+ file = @client.get_file("greeting.txt")
224
+ file.content_type.should == "text/plain"
225
+ file.original_filename.should == "greeting.txt"
226
+ end
227
+ end
228
+
229
+ it "should delete a file" do
230
+ @client = Riak::Client.new
231
+ @http = mock(Riak::Client::HTTPBackend)
232
+ @client.stub!(:http).and_return(@http)
233
+ @http.should_receive(:delete).with([204,404], "/luwak", "greeting.txt")
234
+ @client.delete_file("greeting.txt")
235
+ end
236
+
237
+ it "should return true if the file exists" do
238
+ @client = Riak::Client.new
239
+ @client.http.should_receive(:head).and_return(:code => 200)
240
+ @client.file_exists?("foo").should be_true
241
+ end
242
+
243
+ it "should return false if the file doesn't exist" do
244
+ @client = Riak::Client.new
245
+ @client.http.should_receive(:head).and_return(:code => 404)
246
+ @client.file_exists?("foo").should be_false
247
+ end
248
+
249
+ it "should escape the filename when storing, retrieving or deleting files" do
250
+ @client = Riak::Client.new
251
+ @http = mock(Riak::Client::HTTPBackend)
252
+ @client.stub!(:http).and_return(@http)
253
+ # Delete escapes keys
254
+ @http.should_receive(:delete).with([204,404], "/luwak", "docs%2FA%20Big%20PDF.pdf")
255
+ @client.delete_file("docs/A Big PDF.pdf")
256
+ # Get escapes keys
257
+ @http.should_receive(:get).with(200, "/luwak", "docs%2FA%20Big%20PDF.pdf").and_yield("foo").and_return(:headers => {"content-type" => ["text/plain"]}, :code => 200)
258
+ @client.get_file("docs/A Big PDF.pdf")
259
+ # Streamed get escapes keys
260
+ @http.should_receive(:get).with(200, "/luwak", "docs%2FA%20Big%20PDF.pdf").and_yield("foo").and_return(:headers => {"content-type" => ["text/plain"]}, :code => 200)
261
+ @client.get_file("docs/A Big PDF.pdf"){|chunk| chunk }
262
+ # Put escapes keys
263
+ @http.should_receive(:put).with(204, "/luwak", "docs%2FA%20Big%20PDF.pdf", "foo", {"Content-Type" => "text/plain"})
264
+ @client.store_file("docs/A Big PDF.pdf", "text/plain", "foo")
265
+ end
174
266
  end
@@ -207,21 +207,30 @@ describe Riak::MapReduce do
207
207
  @mr.timeout(50000)
208
208
  @mr.to_json.should include('"timeout":50000')
209
209
  end
210
+ end
210
211
 
211
- it "should return self from setting the timeout" do
212
- @mr.timeout(5000).should == @mr
213
- end
212
+ it "should return self from setting the timeout" do
213
+ @mr.timeout(5000).should == @mr
214
214
  end
215
215
 
216
216
  describe "executing the map reduce job" do
217
+ before :each do
218
+ @mr.map("Riak.mapValues",:keep => true)
219
+ end
220
+
221
+ it "should raise an exception when no phases are defined" do
222
+ @mr.query.clear
223
+ lambda { @mr.run }.should raise_error(Riak::MapReduceError)
224
+ end
225
+
217
226
  it "should issue POST request to the mapred endpoint" do
218
- @http.should_receive(:post).with(200, "/mapred", @mr.to_json, hash_including("Content-Type" => "application/json")).and_return({:headers => {'content-type' => ["application/json"]}, :body => "{}"})
227
+ @http.should_receive(:post).with(200, "/mapred", @mr.to_json, hash_including("Content-Type" => "application/json")).and_return({:headers => {'content-type' => ["application/json"]}, :body => "[]"})
219
228
  @mr.run
220
229
  end
221
230
 
222
231
  it "should vivify JSON responses" do
223
- @http.stub!(:post).and_return(:headers => {'content-type' => ["application/json"]}, :body => '{"key":"value"}')
224
- @mr.run.should == {"key" => "value"}
232
+ @http.stub!(:post).and_return(:headers => {'content-type' => ["application/json"]}, :body => '[{"key":"value"}]')
233
+ @mr.run.should == [{"key" => "value"}]
225
234
  end
226
235
 
227
236
  it "should return the full response hash for non-JSON responses" do
@@ -335,7 +344,7 @@ describe Riak::MapReduce::Phase do
335
344
  @phase.to_json.should include('"name":"Riak.mapValues"')
336
345
  @phase.to_json.should_not include('"source"')
337
346
  end
338
-
347
+
339
348
  it "should include the bucket and key when referring to a stored function" do
340
349
  @phase.function = {:bucket => "design", :key => "wordcount_map"}
341
350
  @phase.to_json.should include('"bucket":"design"')
@@ -0,0 +1,206 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require File.expand_path('../../spec_helper', __FILE__)
15
+
16
+ describe "Search mixins" do
17
+ before :all do
18
+ require 'riak/search'
19
+ end
20
+
21
+ describe Riak::Client do
22
+ before :each do
23
+ @client = Riak::Client.new
24
+ @http = mock(Riak::Client::HTTPBackend)
25
+ @client.stub!(:http).and_return(@http)
26
+ end
27
+ describe "searching" do
28
+ it "should exclude the index from the URL when not specified" do
29
+ @http.should_receive(:get).with(200, "/solr", "select", hash_including("q" => "foo"), {}).and_return({:code => 200, :headers => {"content-type"=>["application/json"]}, :body => "{}"})
30
+ @client.search("foo")
31
+ end
32
+
33
+ it "should include extra options in the query string" do
34
+ @http.should_receive(:get).with(200, "/solr", "select", hash_including('rows' => 30), {}).and_return({:code => 200, :headers => {"content-type"=>["application/json"]}, :body => "{}"})
35
+ @client.search("foo", 'rows' => 30)
36
+ end
37
+
38
+ it "should include the index in the URL when specified" do
39
+ @http.should_receive(:get).with(200, "/solr", "search", "select", hash_including("q" => "foo"), {}).and_return({:code => 200, :headers => {"content-type"=>["application/json"]}, :body => "{}"})
40
+ @client.search("search", "foo")
41
+ end
42
+
43
+ it "should vivify JSON responses" do
44
+ @http.should_receive(:get).and_return({:code => 200, :headers => {"content-type"=>["application/json"]}, :body => '{"response":{"docs":["foo"]}}'})
45
+ @client.search("foo").should == {"response" => {"docs" => ["foo"]}}
46
+ end
47
+
48
+ it "should return non-JSON responses raw" do
49
+ @http.should_receive(:get).and_return({:code => 200, :headers => {"content-type"=>["text/plain"]}, :body => '{"response":{"docs":["foo"]}}'})
50
+ @client.search("foo").should == '{"response":{"docs":["foo"]}}'
51
+ end
52
+ end
53
+ describe "indexing documents" do
54
+ it "should exclude the index from the URL when not specified" do
55
+ @http.should_receive(:post).with(200, "/solr", "update", anything, anything).and_return({:code => 200, :headers => {'content-type' => ['text/html']}, :body => ""})
56
+ @client.index({:id => 1, :field => "value"})
57
+ end
58
+
59
+ it "should include the index in the URL when specified" do
60
+ @http.should_receive(:post).with(200, "/solr", "foo", "update", anything, anything).and_return({:code => 200, :headers => {'content-type' => ['text/html']}, :body => ""})
61
+ @client.index("foo", {:id => 1, :field => "value"})
62
+ end
63
+
64
+ it "should raise an error when documents do not contain an id" do
65
+ @http.stub!(:post).and_return(true)
66
+ lambda { @client.index({:field => "value"}) }.should raise_error(ArgumentError)
67
+ lambda { @client.index({:id => 1, :field => "value"}) }.should_not raise_error(ArgumentError)
68
+ end
69
+
70
+ it "should build a Solr <add> request" do
71
+ expect_update_body('<add><doc><field name="id">1</field><field name="field">value</field></doc></add>')
72
+ @client.index({:id => 1, :field => "value"})
73
+ end
74
+
75
+ it "should include multiple documents in the <add> request" do
76
+ expect_update_body('<add><doc><field name="id">1</field><field name="field">value</field></doc><doc><field name="id">2</field><field name="foo">bar</field></doc></add>')
77
+ @client.index({:id => 1, :field => "value"}, {:id => 2, :foo => "bar"})
78
+ end
79
+ end
80
+ describe "removing documents" do
81
+ it "should exclude the index from the URL when not specified" do
82
+ @http.should_receive(:post).with(200, "/solr","update", anything, anything).and_return({:code => 200, :headers => {'content-type' => ['text/html']}, :body => ""})
83
+ @client.remove({:id => 1})
84
+ end
85
+
86
+ it "should include the index in the URL when specified" do
87
+ @http.should_receive(:post).with(200, "/solr", "foo", "update", anything, anything).and_return({:code => 200, :headers => {'content-type' => ['text/html']}, :body => ""})
88
+ @client.remove("foo", {:id => 1})
89
+ end
90
+
91
+ it "should raise an error when document specifications don't include an id or query" do
92
+ @http.stub!(:post).and_return({:code => 200})
93
+ lambda { @client.remove({:foo => "bar"}) }.should raise_error(ArgumentError)
94
+ lambda { @client.remove({:id => 1}) }.should_not raise_error
95
+ end
96
+
97
+ it "should build a Solr <delete> request" do
98
+ expect_update_body('<delete><id>1</id></delete>')
99
+ @client.remove(:id => 1)
100
+ expect_update_body('<delete><query>title:old</query></delete>')
101
+ @client.remove(:query => "title:old")
102
+ end
103
+
104
+ it "should include multiple specs in the <delete> request" do
105
+ expect_update_body('<delete><id>1</id><query>title:old</query></delete>')
106
+ @client.remove({:id => 1}, {:query => "title:old"})
107
+ end
108
+ end
109
+
110
+ def expect_update_body(body, index=nil)
111
+ args = [200, "/solr", index, "update", body, {"Content-Type" => "text/xml"}].compact
112
+ @http.should_receive(:post).with(*args).and_return({:code => 200, :headers => {'content-type' => ['text/html']}, :body => ""})
113
+ end
114
+ end
115
+
116
+ describe Riak::Bucket do
117
+ before :each do
118
+ @client = Riak::Client.new
119
+ @bucket = Riak::Bucket.new(@client, "foo")
120
+ end
121
+
122
+ def do_load(overrides={})
123
+ @bucket.load({
124
+ :body => '{"props":{"name":"foo","n_val":3,"allow_mult":false,"last_write_wins":false,"precommit":[],"postcommit":[],"chash_keyfun":{"mod":"riak_core_util","fun":"chash_std_keyfun"},"linkfun":{"mod":"riak_kv_wm_link_walker","fun":"mapreduce_linkfun"},"old_vclock":86400,"young_vclock":20,"big_vclock":50,"small_vclock":10,"r":"quorum","w":"quorum","dw":"quorum","rw":"quorum"},"keys":["bar"]}',
125
+ :headers => {
126
+ "vary" => ["Accept-Encoding"],
127
+ "server" => ["MochiWeb/1.1 WebMachine/1.5.1 (hack the charles gibson)"],
128
+ "link" => ['</riak/foo/bar>; riaktag="contained"'],
129
+ "date" => ["Tue, 12 Jan 2010 15:30:43 GMT"],
130
+ "content-type" => ["application/json"],
131
+ "content-length" => ["257"]
132
+ }
133
+ }.merge(overrides))
134
+ end
135
+ alias :load_without_index_hook :do_load
136
+
137
+ def load_with_index_hook
138
+ do_load(:body => '{"props":{"precommit":[{"mod":"riak_search_kv_hook","fun":"precommit"}]}}')
139
+ end
140
+
141
+ it "should detect whether the indexing hook is installed" do
142
+ load_without_index_hook
143
+ @bucket.props['precommit'].should be_empty
144
+ @bucket.is_indexed?.should be_false
145
+
146
+ load_with_index_hook
147
+ @bucket.props['precommit'].should_not be_empty
148
+ @bucket.is_indexed?.should be_true
149
+ end
150
+
151
+ describe "enabling indexing" do
152
+ it "should add the index hook when not present" do
153
+ load_without_index_hook
154
+ @bucket.should_receive(:props=).with({"precommit" => [Riak::Bucket::SEARCH_PRECOMMIT_HOOK]})
155
+ @bucket.enable_index!
156
+ end
157
+
158
+ it "should not modify the precommit when the hook is present" do
159
+ load_with_index_hook
160
+ @bucket.should_not_receive(:props=)
161
+ @bucket.enable_index!
162
+ end
163
+ end
164
+
165
+ describe "disabling indexing" do
166
+ it "should remove the index hook when present" do
167
+ load_with_index_hook
168
+ @bucket.should_receive(:props=).with({"precommit" => []})
169
+ @bucket.disable_index!
170
+ end
171
+
172
+ it "should not modify the precommit when the hook is missing" do
173
+ load_without_index_hook
174
+ @bucket.should_not_receive(:props=)
175
+ @bucket.disable_index!
176
+ end
177
+ end
178
+ end
179
+
180
+ describe Riak::MapReduce do
181
+ before :each do
182
+ @client = Riak::Client.new
183
+ @mr = Riak::MapReduce.new(@client)
184
+ end
185
+
186
+ describe "using a search query as inputs" do
187
+ it "should accept a bucket name and query" do
188
+ @mr.search("foo", "bar OR baz")
189
+ @mr.inputs.should == {:module => "riak_search", :function => "mapred_search", :arg => ["foo", "bar OR baz"]}
190
+ end
191
+
192
+ it "should accept a Riak::Bucket and query" do
193
+ @mr.search(Riak::Bucket.new(@client, "foo"), "bar OR baz")
194
+ @mr.inputs.should == {:module => "riak_search", :function => "mapred_search", :arg => ["foo", "bar OR baz"]}
195
+ end
196
+
197
+ it "should emit the Erlang function and arguments" do
198
+ @mr.search("foo", "bar OR baz")
199
+ @mr.to_json.should include('"inputs":{')
200
+ @mr.to_json.should include('"module":"riak_search"')
201
+ @mr.to_json.should include('"function":"mapred_search"')
202
+ @mr.to_json.should include('"arg":["foo","bar OR baz"]')
203
+ end
204
+ end
205
+ end
206
+ end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 8
8
- - 1
9
- version: 0.8.1
8
+ - 2
9
+ version: 0.8.2
10
10
  platform: ruby
11
11
  authors:
12
12
  - Sean Cribbs
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-10-11 00:00:00 -04:00
17
+ date: 2010-10-22 00:00:00 -04:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -29,9 +29,7 @@ dependencies:
29
29
  - 2
30
30
  - 0
31
31
  - 0
32
- - beta
33
- - 11
34
- version: 2.0.0.beta.11
32
+ version: 2.0.0
35
33
  type: :development
36
34
  version_requirements: *id001
37
35
  - !ruby/object:Gem::Dependency
@@ -115,11 +113,6 @@ extensions: []
115
113
  extra_rdoc_files: []
116
114
 
117
115
  files:
118
- - bin/htmldiff
119
- - bin/ldiff
120
- - bin/rackup
121
- - bin/rake
122
- - bin/rspec
123
116
  - erl_src/riak_kv_test_backend.beam
124
117
  - erl_src/riak_kv_test_backend.erl
125
118
  - Gemfile
@@ -139,6 +132,7 @@ files:
139
132
  - lib/riak/map_reduce.rb
140
133
  - lib/riak/map_reduce_error.rb
141
134
  - lib/riak/robject.rb
135
+ - lib/riak/search.rb
142
136
  - lib/riak/test_server.rb
143
137
  - lib/riak/util/escape.rb
144
138
  - lib/riak/util/fiber1.8.rb
@@ -165,12 +159,12 @@ files:
165
159
  - spec/riak/multipart_spec.rb
166
160
  - spec/riak/net_http_backend_spec.rb
167
161
  - spec/riak/object_spec.rb
162
+ - spec/riak/search_spec.rb
168
163
  - spec/riak/walk_spec_spec.rb
169
164
  - spec/spec_helper.rb
170
165
  - spec/support/http_backend_implementation_examples.rb
171
166
  - spec/support/mock_server.rb
172
167
  - spec/support/mocks.rb
173
- - spec/support/test_server.yml
174
168
  has_rdoc: true
175
169
  homepage: http://seancribbs.github.com/ripple
176
170
  licenses: []
@@ -217,6 +211,7 @@ test_files:
217
211
  - spec/riak/multipart_spec.rb
218
212
  - spec/riak/net_http_backend_spec.rb
219
213
  - spec/riak/object_spec.rb
214
+ - spec/riak/search_spec.rb
220
215
  - spec/riak/walk_spec_spec.rb
221
216
  - spec/spec_helper.rb
222
217
  - spec/support/http_backend_implementation_examples.rb
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
- #
3
- # This file was generated by Bundler.
4
- #
5
- # The application 'htmldiff' is installed as part of a gem, and
6
- # this file is here to facilitate running it.
7
- #
8
-
9
- ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", __FILE__)
10
-
11
- require 'rubygems'
12
- require 'bundler/setup'
13
-
14
- load Gem.bin_path('diff-lcs', 'htmldiff')
data/bin/ldiff DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
- #
3
- # This file was generated by Bundler.
4
- #
5
- # The application 'ldiff' is installed as part of a gem, and
6
- # this file is here to facilitate running it.
7
- #
8
-
9
- ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", __FILE__)
10
-
11
- require 'rubygems'
12
- require 'bundler/setup'
13
-
14
- load Gem.bin_path('diff-lcs', 'ldiff')
data/bin/rackup DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
- #
3
- # This file was generated by Bundler.
4
- #
5
- # The application 'rackup' is installed as part of a gem, and
6
- # this file is here to facilitate running it.
7
- #
8
-
9
- ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", __FILE__)
10
-
11
- require 'rubygems'
12
- require 'bundler/setup'
13
-
14
- load Gem.bin_path('rack', 'rackup')
data/bin/rake DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
- #
3
- # This file was generated by Bundler.
4
- #
5
- # The application 'rake' is installed as part of a gem, and
6
- # this file is here to facilitate running it.
7
- #
8
-
9
- ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", __FILE__)
10
-
11
- require 'rubygems'
12
- require 'bundler/setup'
13
-
14
- load Gem.bin_path('rake', 'rake')
data/bin/rspec DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
- #
3
- # This file was generated by Bundler.
4
- #
5
- # The application 'rspec' is installed as part of a gem, and
6
- # this file is here to facilitate running it.
7
- #
8
-
9
- ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", __FILE__)
10
-
11
- require 'rubygems'
12
- require 'bundler/setup'
13
-
14
- load Gem.bin_path('rspec-core', 'rspec')
@@ -1,2 +0,0 @@
1
- bin_dir: /Users/sean/Development/riak/rel/riak/bin
2
- temp_dir: /Users/sean/Development/ripple/.riaktest