telvue-rsolr 2.2.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.
@@ -0,0 +1,25 @@
1
+ require 'uri'
2
+
3
+ module RSolr::Uri
4
+ # Creates a Solr based query string.
5
+ # Keys that have arrays values are set multiple times:
6
+ # params_to_solr(:q => 'query', :fq => ['a', 'b'])
7
+ # is converted to:
8
+ # ?q=query&fq=a&fq=b
9
+ # @param [boolean] escape false if no URI escaping is to be performed. Default true.
10
+ # @return [String] Solr query params as a String, suitable for use in a url
11
+ def self.params_to_solr(params, escape = true)
12
+ return URI.encode_www_form(params.reject{|k,v| k.to_s.empty? || v.to_s.empty?}) if escape
13
+
14
+ # escape = false if we are here
15
+ mapped = params.map do |k, v|
16
+ next if v.to_s.empty?
17
+ if v.class == ::Array
18
+ params_to_solr(v.map { |x| [k, x] }, false)
19
+ else
20
+ "#{k}=#{v}"
21
+ end
22
+ end
23
+ mapped.compact.join("&")
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ module RSolr
2
+ VERSION = "2.2.2"
3
+
4
+ def self.version
5
+ VERSION
6
+ end
7
+ end
@@ -0,0 +1,150 @@
1
+ module RSolr::Xml
2
+ Document = RSolr::Document
3
+ Field = RSolr::Field
4
+
5
+ class Generator < RSolr::Generator
6
+ class << self
7
+ attr_accessor :use_nokogiri
8
+
9
+ def builder_proc
10
+ if use_nokogiri
11
+ require 'nokogiri' unless defined?(::Nokogiri::XML::Builder)
12
+ :nokogiri_build
13
+ else
14
+ require 'builder' unless defined?(::Builder::XmlMarkup)
15
+ :builder_build
16
+ end
17
+ end
18
+ end
19
+ self.use_nokogiri = defined?(::Nokogiri::XML::Builder) ? true : false
20
+
21
+ CONTENT_TYPE = 'text/xml'.freeze
22
+
23
+ def content_type
24
+ CONTENT_TYPE
25
+ end
26
+
27
+
28
+ def nokogiri_build &block
29
+ b = ::Nokogiri::XML::Builder.new do |xml|
30
+ block_given? ? yield(xml) : xml
31
+ end
32
+ '<?xml version="1.0" encoding="UTF-8"?>'+b.to_xml(:indent => 0, :encoding => 'UTF-8', :save_with => ::Nokogiri::XML::Node::SaveOptions::AS_XML | ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION).strip
33
+ end
34
+ protected :nokogiri_build
35
+
36
+ def builder_build &block
37
+ b = ::Builder::XmlMarkup.new(:indent => 0, :margin => 0, :encoding => 'UTF-8')
38
+ b.instruct!
39
+ block_given? ? yield(b) : b
40
+ end
41
+ protected :builder_build
42
+
43
+ def build &block
44
+ self.send(self.class.builder_proc,&block)
45
+ end
46
+
47
+ # generates "add" xml for updating solr
48
+ # "data" can be a hash or an array of hashes.
49
+ # - each hash should be a simple key=>value pair representing a solr doc.
50
+ # If a value is an array, multiple fields will be created.
51
+ #
52
+ # "add_attrs" can be a hash for setting the add xml element attributes.
53
+ #
54
+ # This method can also accept a block.
55
+ # The value yielded to the block is a Message::Document; for each solr doc in "data".
56
+ # You can set xml element attributes for each "doc" element or individual "field" elements.
57
+ #
58
+ # For example:
59
+ #
60
+ # solr.add({:id=>1, :nickname=>'Tim'}, {:boost=>5.0, :commitWithin=>1.0}) do |doc_msg|
61
+ # doc_msg.attrs[:boost] = 10.00 # boost the document
62
+ # nickname = doc_msg.field_by_name(:nickname)
63
+ # nickname.attrs[:boost] = 20 if nickname.value=='Tim' # boost a field
64
+ # end
65
+ #
66
+ # would result in an add element having the attributes boost="10.0"
67
+ # and a commitWithin="1.0".
68
+ # Each doc element would have a boost="10.0".
69
+ # The "nickname" field would have a boost="20.0"
70
+ # if the doc had a "nickname" field with the value of "Tim".
71
+ #
72
+ def add data, add_attrs = nil, &block
73
+ add_attrs ||= {}
74
+ data = RSolr::Array.wrap(data)
75
+ build do |xml|
76
+ xml.add(add_attrs) do |add_node|
77
+ data.each do |doc|
78
+ doc = RSolr::Document.new(doc) if doc.respond_to?(:each_pair)
79
+ yield doc if block_given?
80
+ doc_node_builder = to_xml(doc)
81
+ self.class.use_nokogiri ? add_node.doc_(doc.attrs,&doc_node_builder) : add_node.doc(doc.attrs,&doc_node_builder)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ # generates a <commit/> message
88
+ def commit opts = nil
89
+ opts ||= {}
90
+ build {|xml| xml.commit(opts) }
91
+ end
92
+
93
+ # generates a <optimize/> message
94
+ def optimize opts = nil
95
+ opts ||= {}
96
+ build {|xml| xml.optimize(opts) }
97
+ end
98
+
99
+ # generates a <rollback/> message
100
+ def rollback
101
+ build {|xml| xml.rollback({}) }
102
+ end
103
+
104
+ # generates a <delete><id>ID</id></delete> message
105
+ # "ids" can be a single value or array of values
106
+ def delete_by_id ids
107
+ ids = RSolr::Array.wrap(ids)
108
+ build do |xml|
109
+ xml.delete do |delete_node|
110
+ ids.each do |id|
111
+ self.class.use_nokogiri ? delete_node.id_(id) : delete_node.id(id)
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ # generates a <delete><query>ID</query></delete> message
118
+ # "queries" can be a single value or an array of values
119
+ def delete_by_query(queries)
120
+ queries = RSolr::Array.wrap(queries)
121
+ build do |xml|
122
+ xml.delete do |delete_node|
123
+ queries.each { |query| delete_node.query(query) }
124
+ end
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def to_xml(doc)
131
+ lambda do |doc_node|
132
+ doc.fields.each do |field_obj|
133
+ value = field_obj.value
134
+
135
+ if field_obj.name.to_s == RSolr::Document::CHILD_DOCUMENT_KEY
136
+ child_node_builder = to_xml(field_obj.value)
137
+ self.class.use_nokogiri ? doc_node.doc_(&child_node_builder) : doc_node.doc(&child_node_builder)
138
+ elsif value.is_a?(Hash) && value.length == 1 && field_obj.attrs[:update].nil?
139
+ update_attr, real_value = value.first
140
+ doc_node.field real_value, field_obj.attrs.merge(update: update_attr)
141
+ elsif value.nil?
142
+ doc_node.field field_obj.value, field_obj.attrs.merge(null: true)
143
+ else
144
+ doc_node.field field_obj.value, field_obj.attrs
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,46 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ require "rsolr/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "telvue-rsolr"
8
+ s.summary = "A Ruby client for Apache Solr"
9
+ s.description = %q{RSolr aims to provide a simple and extensible library for working with Solr}
10
+ s.version = RSolr::VERSION
11
+ s.authors = ["Antoine Latter", "Dmitry Lihachev",
12
+ "Lucas Souza", "Peter Kieltyka",
13
+ "Rob Di Marco", "Magnus Bergmark",
14
+ "Jonathan Rochkind", "Chris Beer",
15
+ "Craig Smith", "Randy Souza",
16
+ "Colin Steele", "Peter Kieltyka",
17
+ "Lorenzo Riccucci", "Mike Perham",
18
+ "Mat Brown", "Shairon Toledo",
19
+ "Matthew Rudy", "Fouad Mardini",
20
+ "Jeremy Hinegardner", "Nathan Witmer",
21
+ "Naomi Dushay",
22
+ "\"shima\"", "Ben Liu"]
23
+ s.email = ["bliu@telvue.com"]
24
+ s.license = 'Apache-2.0'
25
+ s.homepage = "https://github.com/telvue/rsolr"
26
+ s.rubyforge_project = "rsolr"
27
+ s.files = `git ls-files`.split("\n")
28
+ s.test_files = `git ls-files -- {spec}/*`.split("\n")
29
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
30
+ s.require_paths = ["lib"]
31
+
32
+ s.required_ruby_version = '>= 1.9.3'
33
+
34
+ s.requirements << 'Apache Solr'
35
+
36
+ s.add_dependency 'builder', '>= 2.1.2'
37
+ s.add_dependency 'faraday', '>= 0.9.0'
38
+
39
+ s.add_development_dependency 'activesupport'
40
+ s.add_development_dependency 'nokogiri', '>= 1.4.0'
41
+ s.add_development_dependency 'rake', '>= 10.0'
42
+ s.add_development_dependency 'rdoc', '>= 4.0'
43
+ s.add_development_dependency 'rspec', '~> 3.0'
44
+ s.add_development_dependency 'simplecov'
45
+ s.add_development_dependency 'solr_wrapper'
46
+ end
@@ -0,0 +1,355 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RSolr::Client do
4
+ let(:connection) { nil }
5
+ let(:url) { "http://localhost:9999/solr" }
6
+ let(:connection_options) { { url: url, read_timeout: 42, open_timeout: 43, update_format: :xml } }
7
+
8
+ let(:client) do
9
+ RSolr::Client.new connection, connection_options
10
+ end
11
+
12
+ let(:client_with_proxy) do
13
+ RSolr::Client.new connection, connection_options.merge(proxy: 'http://localhost:8080')
14
+ end
15
+
16
+ context "initialize" do
17
+ it "should accept whatevs and set it as the @connection" do
18
+ expect(RSolr::Client.new(:whatevs).connection).to eq(:whatevs)
19
+ end
20
+
21
+ it "should use :update_path from options" do
22
+ client = RSolr::Client.new(:whatevs, { update_path: 'update_test' })
23
+ expect(client.update_path).to eql('update_test')
24
+ end
25
+
26
+ it "should use 'update' for update_path by default" do
27
+ client = RSolr::Client.new(:whatevs)
28
+ expect(client.update_path).to eql('update')
29
+ end
30
+
31
+ it "should use :proxy from options" do
32
+ client = RSolr::Client.new(:whatevs, { proxy: 'http://my.proxy/' })
33
+ expect(client.proxy.to_s).to eql('http://my.proxy/')
34
+ end
35
+
36
+ it "should use 'nil' for proxy by default" do
37
+ client = RSolr::Client.new(:whatevs)
38
+ expect(client.proxy).to be_nil
39
+ end
40
+
41
+ it "should use 'false' for proxy if passed 'false'" do
42
+ client = RSolr::Client.new(:whatevs, { proxy: false })
43
+ expect(client.proxy).to eq(false)
44
+ end
45
+
46
+ context "with an non-HTTP url" do
47
+ let(:url) { "fake://localhost:9999/solr" }
48
+
49
+ it "raises an argument error" do
50
+ expect { client }.to raise_error ArgumentError, "You must provide an HTTP(S) url."
51
+ end
52
+ end
53
+
54
+ context "with an HTTPS url" do
55
+ let(:url) { "https://localhost:9999/solr" }
56
+
57
+ it "creates a connection" do
58
+ expect(client.uri).to be_kind_of URI::HTTPS
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ context "send_and_receive" do
65
+ it "should forward these method calls the #connection object" do
66
+ [:get, :post, :head].each do |meth|
67
+ expect(client).to receive(:execute).
68
+ and_return({:status => 200, :body => "{}", :headers => {}})
69
+ client.send_and_receive '', :method => meth, :params => {}, :data => nil, :headers => {}
70
+ end
71
+ end
72
+ end
73
+
74
+ context "post" do
75
+ it "should pass the expected params to the connection's #execute method" do
76
+ request_opts = {:data => "the data", :method=>:post, :headers => {"Content-Type" => "text/plain"}}
77
+ expect(client).to receive(:execute).
78
+ with(hash_including(request_opts)).
79
+ and_return(
80
+ :body => "",
81
+ :status => 200,
82
+ :headers => {"Content-Type"=>"text/plain"}
83
+ )
84
+ client.post "update", request_opts
85
+ end
86
+ end
87
+
88
+ context "add" do
89
+ it "should send xml to the connection's #post method" do
90
+ expect(client).to receive(:execute).
91
+ with(
92
+ hash_including({
93
+ :path => "update",
94
+ :headers => {"Content-Type"=>"text/xml"},
95
+ :method => :post,
96
+ :data => "<xml/>"
97
+ })
98
+ ).
99
+ and_return(
100
+ :body => "",
101
+ :status => 200,
102
+ :headers => {"Content-Type"=>"text/xml"}
103
+ )
104
+ expect(client.builder).to receive(:add).
105
+ with({:id=>1}, {:commitWith=>10}).
106
+ and_return("<xml/>")
107
+ client.add({:id=>1}, :add_attributes => {:commitWith=>10})
108
+ end
109
+
110
+ context 'when the client is configured for json updates' do
111
+ let(:client) do
112
+ RSolr::Client.new nil, :url => "http://localhost:9999/solr", :read_timeout => 42, :open_timeout=>43, :update_format => :json
113
+ end
114
+ it "should send json to the connection's #post method" do
115
+ expect(client).to receive(:execute).
116
+ with(hash_including({
117
+ :path => 'update',
118
+ :headers => {"Content-Type" => 'application/json'},
119
+ :method => :post,
120
+ :data => '{"hello":"this is json"}'
121
+ })
122
+ ).
123
+ and_return(
124
+ :body => "",
125
+ :status => 200,
126
+ :headers => {"Content-Type"=>"text/xml"}
127
+ )
128
+ expect(client.builder).to receive(:add).
129
+ with({:id => 1}, {:commitWith=>10}).
130
+ and_return('{"hello":"this is json"}')
131
+ client.add({:id=>1}, :add_attributes => {:commitWith=>10})
132
+ end
133
+
134
+ it "should send json to the connection's #post method" do
135
+ expect(client).to receive(:execute).
136
+ with(hash_including({
137
+ :path => 'update',
138
+ :headers => {'Content-Type'=>'application/json'},
139
+ :method => :post,
140
+ :data => '{"optimise" : {}}'
141
+ })
142
+ ).
143
+ and_return(
144
+ :body => "",
145
+ :status => 200,
146
+ :headers => {"Content-Type"=>"text/xml"}
147
+ )
148
+ client.update(:data => '{"optimise" : {}}')
149
+ end
150
+ end
151
+ end
152
+
153
+ context "update" do
154
+ it "should send data to the connection's #post method" do
155
+ expect(client).to receive(:execute).
156
+ with(hash_including({
157
+ :path => "update",
158
+ :headers => {"Content-Type"=>"text/xml"},
159
+ :method => :post,
160
+ :data => "<optimize/>"
161
+ })
162
+ ).
163
+ and_return(
164
+ :body => "",
165
+ :status => 200,
166
+ :headers => {"Content-Type"=>"text/xml"}
167
+ )
168
+ client.update(:data => "<optimize/>")
169
+ end
170
+
171
+ it "should use #update_path" do
172
+ expect(client).to receive(:post).with('update_test', any_args)
173
+ expect(client).to receive(:update_path).and_return('update_test')
174
+ client.update({})
175
+ end
176
+
177
+ it "should use path from opts" do
178
+ expect(client).to receive(:post).with('update_opts', any_args)
179
+ allow(client).to receive(:update_path).and_return('update_test')
180
+ client.update({path: 'update_opts'})
181
+ end
182
+ end
183
+
184
+ context "post based helper methods:" do
185
+ [:commit, :optimize, :rollback].each do |meth|
186
+ it "should send a #{meth} message to the connection's #post method" do
187
+ expect(client).to receive(:execute).
188
+ with(hash_including({
189
+ :path => "update",
190
+ :headers => {"Content-Type"=>"text/xml"},
191
+ :method => :post,
192
+ :data => "<?xml version=\"1.0\" encoding=\"UTF-8\"?><#{meth}/>"
193
+ })
194
+ ).
195
+ and_return(
196
+ :body => "",
197
+ :status => 200,
198
+ :headers => {"Content-Type"=>"text/xml"}
199
+ )
200
+ client.send meth
201
+ end
202
+ end
203
+ end
204
+
205
+ context "delete_by_id" do
206
+ it "should send data to the connection's #post method" do
207
+ expect(client).to receive(:execute).
208
+ with(
209
+ hash_including({
210
+ :path => "update",
211
+ :headers => {"Content-Type"=>"text/xml"},
212
+ :method => :post,
213
+ :data => "<?xml version=\"1.0\" encoding=\"UTF-8\"?><delete><id>1</id></delete>"
214
+ })
215
+ ).
216
+ and_return(
217
+ :body => "",
218
+ :status => 200,
219
+ :headers => {"Content-Type"=>"text/xml"}
220
+ )
221
+ client.delete_by_id 1
222
+ end
223
+ end
224
+
225
+ context "delete_by_query" do
226
+ it "should send data to the connection's #post method" do
227
+ expect(client).to receive(:execute).
228
+ with(
229
+ hash_including({
230
+ :path => "update",
231
+ :headers => {"Content-Type"=>"text/xml"},
232
+ :method => :post,
233
+ :data => "<?xml version=\"1.0\" encoding=\"UTF-8\"?><delete><query fq=\"category:&quot;trash&quot;\"/></delete>"
234
+ })
235
+ ).
236
+ and_return(
237
+ :body => "",
238
+ :status => 200,
239
+ :headers => {"Content-Type"=>"text/xml"}
240
+ )
241
+ client.delete_by_query :fq => "category:\"trash\""
242
+ end
243
+ end
244
+
245
+ context "adapt_response" do
246
+ it 'should not try to evaluate ruby when the :qt is not :ruby' do
247
+ body = '{"time"=>"NOW"}'
248
+ result = client.adapt_response({:params=>{}}, {:status => 200, :body => body, :headers => {}})
249
+ expect(result).to eq(body)
250
+ end
251
+
252
+ it 'should evaluate ruby responses when the :wt is :ruby' do
253
+ body = '{"time"=>"NOW"}'
254
+ result = client.adapt_response({:params=>{:wt=>:ruby}}, {:status => 200, :body => body, :headers => {}})
255
+ expect(result).to eq({"time"=>"NOW"})
256
+ end
257
+
258
+ it 'should evaluate json responses when the :wt is :json' do
259
+ body = '{"time": "NOW"}'
260
+ result = client.adapt_response({:params=>{:wt=>:json}}, {:status => 200, :body => body, :headers => {}})
261
+ if defined? JSON
262
+ expect(result).to eq({"time"=>"NOW"})
263
+ else
264
+ # ruby 1.8 without the JSON gem
265
+ expect(result).to eq('{"time": "NOW"}')
266
+ end
267
+ end
268
+
269
+ it 'should return a response for a head request' do
270
+ result = client.adapt_response({:method=>:head,:params=>{}}, {:status => 200, :body => nil, :headers => {}})
271
+ expect(result.response[:status]).to eq 200
272
+ end
273
+
274
+ it "ought raise a RSolr::Error::InvalidRubyResponse when the ruby is indeed frugged, or even fruggified" do
275
+ expect {
276
+ client.adapt_response({:params=>{:wt => :ruby}}, {:status => 200, :body => "<woops/>", :headers => {}})
277
+ }.to raise_error RSolr::Error::InvalidRubyResponse
278
+ end
279
+
280
+ end
281
+
282
+ context "indifferent access" do
283
+ it "should raise a RuntimeError if the #with_indifferent_access extension isn't loaded" do
284
+ hide_const("::RSolr::HashWithIndifferentAccessWithResponse")
285
+ hide_const("ActiveSupport::HashWithIndifferentAccess")
286
+ body = "{'foo'=>'bar'}"
287
+ result = client.adapt_response({:params=>{:wt=>:ruby}}, {:status => 200, :body => body, :headers => {}})
288
+ expect { result.with_indifferent_access }.to raise_error RuntimeError
289
+ end
290
+
291
+ it "should provide indifferent access" do
292
+ require 'active_support/core_ext/hash/indifferent_access'
293
+ body = "{'foo'=>'bar'}"
294
+ result = client.adapt_response({:params=>{:wt=>:ruby}}, {:status => 200, :body => body, :headers => {}})
295
+ indifferent_result = result.with_indifferent_access
296
+
297
+ expect(result).to be_a(RSolr::Response)
298
+ expect(result['foo']).to eq('bar')
299
+ expect(result[:foo]).to be_nil
300
+
301
+ expect(indifferent_result).to be_a(RSolr::Response)
302
+ expect(indifferent_result['foo']).to eq('bar')
303
+ expect(indifferent_result[:foo]).to eq('bar')
304
+ end
305
+ end
306
+
307
+ context "build_request" do
308
+ let(:data) { 'data' }
309
+ let(:params) { { q: 'test', fq: [0,1] } }
310
+ let(:options) { { method: :post, params: params, data: data, headers: {} } }
311
+ subject { client.build_request('select', options) }
312
+
313
+ context "when params are symbols" do
314
+ it 'should return a request context array' do
315
+ [/fq=0/, /fq=1/, /q=test/, /wt=json/].each do |pattern|
316
+ expect(subject[:query]).to match pattern
317
+ end
318
+ expect(subject[:data]).to eq("data")
319
+ expect(subject[:headers]).to eq({})
320
+ end
321
+ end
322
+
323
+ context "when params are strings" do
324
+ let(:params) { { 'q' => 'test', 'wt' => 'json' } }
325
+ it 'should return a request context array' do
326
+ expect(subject[:query]).to eq 'q=test&wt=json'
327
+ expect(subject[:data]).to eq("data")
328
+ expect(subject[:headers]).to eq({})
329
+ end
330
+ end
331
+
332
+ context "when a Hash is passed in as data" do
333
+ let(:data) { { q: 'test', fq: [0,1] } }
334
+ let(:options) { { method: :post, data: data, headers: {} } }
335
+
336
+ it "sets the Content-Type header to application/x-www-form-urlencoded; charset=UTF-8" do
337
+ expect(subject[:query]).to eq("wt=json")
338
+ [/fq=0/, /fq=1/, /q=test/].each do |pattern|
339
+ expect(subject[:data]).to match pattern
340
+ end
341
+ expect(subject[:data]).not_to match(/wt=json/)
342
+ expect(subject[:headers]).to eq({"Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8"})
343
+ end
344
+ end
345
+
346
+ it "should properly handle proxy configuration" do
347
+ result = client_with_proxy.build_request('select',
348
+ :method => :post,
349
+ :data => {:q=>'test', :fq=>[0,1]},
350
+ :headers => {}
351
+ )
352
+ expect(result[:uri].to_s).to match %r{^http://localhost:9999/solr/}
353
+ end
354
+ end
355
+ end