riak-client 0.8.1 → 0.8.2

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.
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