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 +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')
|