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 +2 -1
- data/lib/riak/client.rb +84 -1
- data/lib/riak/locale/en.yml +3 -0
- data/lib/riak/map_reduce.rb +4 -3
- data/lib/riak/search.rb +157 -0
- data/spec/riak/client_spec.rb +93 -1
- data/spec/riak/map_reduce_spec.rb +16 -7
- data/spec/riak/search_spec.rb +206 -0
- metadata +7 -12
- data/bin/htmldiff +0 -14
- data/bin/ldiff +0 -14
- data/bin/rackup +0 -14
- data/bin/rake +0 -14
- data/bin/rspec +0 -14
- data/spec/support/test_server.yml +0 -2
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
|
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
|
|
data/lib/riak/client.rb
CHANGED
@@ -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
|
data/lib/riak/locale/en.yml
CHANGED
@@ -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}"
|
data/lib/riak/map_reduce.rb
CHANGED
@@ -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])
|
data/lib/riak/search.rb
ADDED
@@ -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
|
data/spec/riak/client_spec.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
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
|
-
|
212
|
-
|
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
|
-
-
|
9
|
-
version: 0.8.
|
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-
|
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
|
-
|
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
|
data/bin/htmldiff
DELETED
@@ -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')
|