rsolr 0.13.0.pre → 1.0.0.beta

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/README.rdoc CHANGED
@@ -1,5 +1,7 @@
1
1
  =RSolr
2
2
 
3
+ Notice: This document is only for the the 1.0 (pre-release) in the master branch. The last stable gem release documentation can be found here: http://github.com/mwmitchell/rsolr/tree/v0.12.1
4
+
3
5
  A simple, extensible Ruby client for Apache Solr.
4
6
 
5
7
  == Installation:
@@ -11,23 +13,20 @@ A simple, extensible Ruby client for Apache Solr.
11
13
  require 'rsolr'
12
14
 
13
15
  # Direct connection
14
- solr = RSolr.connect :url=>'http://solrserver.com'
16
+ solr = RSolr.connect 'http://solrserver.com'
15
17
 
16
18
  # Connecting over a proxy server
17
- solr = RSolr.connect :url=>'http://solrserver.com', :proxy=>'http://user:pass@proxy.example.com:8080'
19
+ solr = RSolr.connect 'http://solrserver.com', :proxy=>'http://user:pass@proxy.example.com:8080'
18
20
 
19
21
  # send a request to /select
20
- response = solr.select :q=>'*:*'
22
+ response = solr.get 'select', :q=>'*:*'
21
23
 
22
24
  # send a request to a custom request handler; /catalog
23
- response = solr.request '/catalog', :q=>'*:*'
25
+ response = solr.get 'catalog', :q=>'*:*'
24
26
 
25
- # alternative to above:
26
- response = solr.catalog :q=>'*:*'
27
-
28
27
  == Querying
29
28
  Use the #select method to send requests to the /select handler:
30
- response = solr.select({
29
+ response = solr.get('select', {
31
30
  :q=>'washington',
32
31
  :start=>0,
33
32
  :rows=>10
@@ -35,25 +34,23 @@ Use the #select method to send requests to the /select handler:
35
34
 
36
35
  The params sent into the method are sent to Solr as-is. The one exception is if a value is an array. When an array is used, multiple parameters *with the same name* are generated for the Solr query. Example:
37
36
 
38
- solr.select :q=>'roses', :fq=>['red', 'violet']
37
+ solr.get 'select', :q=>'roses', :fq=>['red', 'violet']
39
38
 
40
39
  The above statement generates this Solr query:
41
40
 
42
- ?q=roses&fq=red&fq=violet
41
+ select?q=roses&fq=red&fq=violet
43
42
 
44
- Use the #request method for a custom request handler path:
45
- response = solr.request '/documents', :q=>'test'
46
-
47
- A shortcut for the above example use a method call instead:
48
- response = solr.documents :q=>'test'
43
+ There may be cases where the query string is too long for a GET request. RSolr solves this issue by providing a simple way to POST a query to Solr:
44
+ response = solr.post "select", nil, enormous_params_hash
49
45
 
46
+ nil is passed in as the query string data. The enormous_params_hash variable ends up serialized as a form-encoded query string, and the correct content-type headers are sent along to Solr.
50
47
 
51
48
  == Updating Solr
52
- Updating uses native Ruby structures. Hashes are used for single documents and arrays are used for a collection of documents (hashes). These structures get turned into simple XML "messages". Raw XML strings can also be used.
49
+ Updating us done using native Ruby objects. Hashes are used for single documents and arrays are used for a collection of documents (hashes). These objects get turned into simple XML "messages". Raw XML strings can also be used.
53
50
 
54
51
  Raw XML via #update
55
- solr.update '</commit>'
56
- solr.update '</optimize>'
52
+ solr.update '<commit/>'
53
+ solr.update '<optimize/>'
57
54
 
58
55
  Single document via #add
59
56
  solr.add :id=>1, :price=>1.00
@@ -91,22 +88,26 @@ Commit & optimize shortcuts
91
88
  The default response format is Ruby. When the :wt param is set to :ruby, the response is eval'd resulting in a Hash. You can get a raw response by setting the :wt to "ruby" - notice, the string -- not a symbol. RSolr will eval the Ruby string ONLY if the :wt value is :ruby. All other response formats are available as expected, :wt=>'xml' etc..
92
89
 
93
90
  ===Evaluated Ruby (default)
94
- solr.select(:wt=>:ruby) # notice :ruby is a Symbol
91
+ solr.get 'select', :wt=>:ruby # notice :ruby is a Symbol
95
92
  ===Raw Ruby
96
- solr.select(:wt=>'ruby') # notice 'ruby' is a String
93
+ solr.get 'select', :wt=>'ruby' # notice 'ruby' is a String
97
94
 
98
95
  ===XML:
99
- solr.select(:wt=>:xml)
96
+ solr.get 'select', :wt=>:xml
100
97
  ===JSON:
101
- solr.select(:wt=>:json)
98
+ solr.get 'select', :wt=>:json
102
99
 
103
- You can access the original request context (path, params, url etc.) by calling the #raw method:
104
- response = solr.select :q=>'*:*'
105
- response.raw[:status_code]
106
- response.raw[:body]
107
- response.raw[:url]
100
+ You can access the original request context (path, params, url etc.) by calling the #request method:
101
+ result = solr.get 'select', :q=>'*:*'
102
+ result.request[:uri]
103
+ result.request[:params]
104
+ etc..
108
105
 
109
- The raw is a hash that contains the generated params, url, path, post data, headers etc., very useful for debugging and testing.
106
+ Similarly, the object returned has a response object. This contains any headers that Solr returned, along with the raw response body:
107
+ result = solr.get 'select', :q=>'*:*'
108
+ result.response[:headers]
109
+ result.response[:status]
110
+ result.response[:body]
110
111
 
111
112
  ==Related Resources & Projects
112
113
  * {RSolr Google Group}[http://groups.google.com/group/rsolr] -- The RSolr discussion group
@@ -127,6 +128,16 @@ The raw is a hash that contains the generated params, url, path, post data, head
127
128
  (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
128
129
  * Send me a pull request. Bonus points for topic branches.
129
130
 
131
+ == Note on Patches/Pull Requests
132
+
133
+ * Fork the project.
134
+ * Make your feature addition or bug fix.
135
+ * Add tests for it. This is important so I don't break it in a
136
+ future version unintentionally.
137
+ * Commit, do not mess with rakefile, version, or history.
138
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
139
+ * Send me a pull request. Bonus points for topic branches.
140
+
130
141
  ==Contributors
131
142
  * Lorenzo Riccucci
132
143
  * Mike Perham
@@ -136,4 +147,12 @@ The raw is a hash that contains the generated params, url, path, post data, head
136
147
  * Fouad Mardini
137
148
  * Jeremy Hinegardner
138
149
  * Nathan Witmer
139
- * Craig Smith
150
+ * Craig Smith
151
+
152
+ ==Author
153
+
154
+ Matt Mitchell <mailto:goodieboy@gmail.com>
155
+
156
+ ==Copyright
157
+
158
+ Copyright (c) 2008-2010 mwmitchell. See LICENSE for details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.13.0.pre
1
+ 1.0.0.beta
data/lib/rsolr/char.rb ADDED
@@ -0,0 +1,9 @@
1
+ # A module that contains (1) string related methods
2
+ module RSolr::Char
3
+
4
+ # backslash everything that isn't a word character
5
+ def escape value
6
+ value.gsub /(\W)/, '\\\\\1'
7
+ end
8
+
9
+ end
data/lib/rsolr/client.rb CHANGED
@@ -2,37 +2,31 @@ class RSolr::Client
2
2
 
3
3
  attr_reader :connection
4
4
 
5
- # "connection" is instance of:
6
- # RSolr::Adapter::HTTP
7
- # RSolr::Adapter::Direct (jRuby only)
8
- # or any other class that uses the connection "interface"
9
- def initialize(connection)
5
+ def initialize connection
10
6
  @connection = connection
11
7
  end
12
8
 
13
- # Send a request to a request handler using the method name.
14
- # Also proxies to the #paginate method if the method starts with "paginate_"
15
- def method_missing(method_name, *args, &blk)
16
- request("/#{method_name}", *args, &blk)
9
+ # GET request
10
+ def get path = '', params = nil, headers = nil
11
+ send_request :get, path, params, nil, headers
17
12
  end
18
13
 
19
- # sends data to the update handler
20
- # data can be a string of xml, or an object that returns xml from its #to_xml method
21
- def update(data, params={})
22
- request '/update', params, data
14
+ # essentially a GET, but no response body
15
+ def head path = '', params = nil, headers = nil
16
+ send_request :head, path, params, nil, headers
23
17
  end
24
18
 
25
- # send request solr
26
- # params is hash with valid solr request params (:q, :fl, :qf etc..)
27
- # if params[:wt] is not set, the default is :ruby
28
- # if :wt is something other than :ruby, the raw response body is used
29
- # otherwise, a simple Hash is returned
30
- # NOTE: to get raw ruby, use :wt=>'ruby' <- a string, not a symbol like :ruby
31
- #
32
- #
33
- def request(path, params={}, *extra)
34
- response = @connection.request(path, map_params(params), *extra)
35
- adapt_response(response)
19
+ # A path is required for a POST since, well...
20
+ # the / resource doesn't do anything with a POST.
21
+ # Also, Solr doesn't do headers with a POST
22
+ def post path, data = nil, params = nil, headers = nil
23
+ send_request :post, path, params, data, headers
24
+ end
25
+
26
+ # POST XML messages to /update with optional params
27
+ def update data, params = {}, headers = {}
28
+ headers['Content-Type'] ||= 'text/xml'
29
+ post 'update', data, params, headers
36
30
  end
37
31
 
38
32
  #
@@ -43,10 +37,10 @@ class RSolr::Client
43
37
  # solr.update([{:id=>1, :name=>'one'}, {:id=>2, :name=>'two'}])
44
38
  #
45
39
  def add(doc, params={}, &block)
46
- update message.add(doc, params, &block)
40
+ update xml.add(doc, params, &block)
47
41
  end
48
42
 
49
- # send "commit" message with options
43
+ # send "commit" xml with options
50
44
  #
51
45
  # Options recognized by solr
52
46
  #
@@ -58,10 +52,10 @@ class RSolr::Client
58
52
  # *NOTE* :expungeDeletes is Solr 1.4 only
59
53
  #
60
54
  def commit( options = {} )
61
- update message.commit( options )
55
+ update xml.commit( options )
62
56
  end
63
57
 
64
- # send "optimize" message with options.
58
+ # send "optimize" xml with options.
65
59
  #
66
60
  # Options recognized by solr
67
61
  #
@@ -73,61 +67,89 @@ class RSolr::Client
73
67
  # *NOTE* :expungeDeletes is Solr 1.4 only
74
68
  #
75
69
  def optimize( options = {} )
76
- update message.optimize( options )
70
+ update xml.optimize( options )
77
71
  end
78
72
 
79
73
  # send </rollback>
80
74
  # NOTE: solr 1.4 only
81
75
  def rollback
82
- update message.rollback
76
+ update xml.rollback
83
77
  end
84
78
 
85
79
  # Delete one or many documents by id
86
80
  # solr.delete_by_id 10
87
81
  # solr.delete_by_id([12, 41, 199])
88
82
  def delete_by_id(id)
89
- update message.delete_by_id(id)
83
+ update xml.delete_by_id(id)
90
84
  end
91
85
 
92
86
  # delete one or many documents by query
93
87
  # solr.delete_by_query 'available:0'
94
88
  # solr.delete_by_query ['quantity:0', 'manu:"FQ"']
95
89
  def delete_by_query(query)
96
- update message.delete_by_query(query)
90
+ update xml.delete_by_query(query)
97
91
  end
98
92
 
99
93
  # shortcut to RSolr::Message::Generator
100
- def message
101
- @message ||= RSolr::Message::Generator.new
94
+ def xml
95
+ @xml ||= RSolr::Xml::Generator.new
102
96
  end
103
97
 
104
- protected
98
+ def send_request method, path, params, data, headers
99
+ params = map_params params
100
+ uri, data, headers = build_request path, params, data, headers
101
+ request_context = {:connection=>connection, :method => method, :uri => uri, :data => data, :headers => headers, :params => params}
102
+ begin
103
+ response = data ? connection.send(method, uri, data, headers) : connection.send(method, uri, headers)
104
+ rescue
105
+ $!.extend(RSolr::Error::SolrContext).request = request_context
106
+ raise $!
107
+ end
108
+ raise "The connection adapter returned an unexpected object" unless response.is_a?(Hash)
109
+ raise RSolr::Error::Http.new request_context, response unless [200,302].include?(response[:status])
110
+ adapt_response request_context, response
111
+ end
105
112
 
106
- # sets default params etc.. - could be used as a mapping hook
107
- # type of request should be passed in here? -> map_params(:query, {})
108
- def map_params(params)
109
- params||={}
110
- {:wt=>:ruby}.merge(params)
113
+ def map_params params
114
+ params = params.nil? ? {} : params.dup
115
+ params[:wt] ||= :ruby
116
+ params
111
117
  end
112
-
113
- # "connection_response" must be a hash with the following keys:
114
- # :params - a sub hash of standard solr params
115
- # : body - the raw response body from the solr server
116
- # This method will evaluate the :body value if the params[:wt] == :ruby
117
- # otherwise, the body is returned
118
- # The return object has a special method attached called #raw
119
- # This method gives you access to the original response from the connection,
120
- # so you can access things like the actual :url sent to solr,
121
- # the raw :body, original :params and original :data
122
- def adapt_response(connection_response)
123
- data = connection_response[:body]
124
- # if the wt is :ruby, evaluate the ruby string response
125
- if connection_response[:params][:wt] == :ruby
126
- data = Kernel.eval(data)
118
+
119
+ def build_request path, params, data, headers
120
+ params ||= {}
121
+ headers ||= {}
122
+ request_uri = params.any? ? "#{path}?#{RSolr::Uri.params_to_solr params}" : path
123
+ if data
124
+ if data.is_a? Hash
125
+ data = RSolr::Uri.params_to_solr data
126
+ headers['Content-Type'] ||= 'application/x-www-form-urlencoded'
127
+ end
128
+ end
129
+ [request_uri, data, headers]
130
+ end
131
+
132
+ # This method will evaluate the :body value
133
+ # if the params[:uri].params[:wt] == :ruby
134
+ # ... otherwise, the body is returned as is.
135
+ # The return object has a special method attached called #context.
136
+ # This method gives you access to the original
137
+ # request and response from the connection.
138
+ # This method will raise an InvalidRubyResponse
139
+ # if the :wt => :ruby and the body
140
+ # couldn't be evaluated.
141
+ def adapt_response request, response
142
+ data = response[:body]
143
+ if request[:params][:wt] == :ruby
144
+ begin
145
+ data = Kernel.eval data.to_s
146
+ rescue SyntaxError
147
+ raise RSolr::Error::InvalidRubyResponse.new request, response
148
+ end
127
149
  end
128
- # attach a method called #raw that returns the original connection response value
129
- def data.raw; @raw end
130
- data.send(:instance_variable_set, '@raw', connection_response)
150
+ data.extend Module.new.instance_eval{attr_accessor :request, :response; self}
151
+ data.request = request
152
+ data.response = response
131
153
  data
132
154
  end
133
155
 
@@ -0,0 +1,122 @@
1
+ module RSolr::Error
2
+
3
+ module SolrContext
4
+
5
+ attr_accessor :request, :response
6
+
7
+ def to_s
8
+ m = "#{super.to_s}"
9
+
10
+ if response
11
+ m << " - #{response[:status]} #{Http::STATUS_CODES[response[:status].to_i]}"
12
+ details = parse_solr_error_response response[:body]
13
+ m << "Error: #{details}\n" if details
14
+ end
15
+
16
+ m << "\n" + self.backtrace[0..10].join("\n")
17
+ m << "\n\nSolr Request:"
18
+ m << "\n Method: #{request[:method].to_s.upcase}"
19
+ m << "\n Base URL: #{request[:connection].uri.to_s}"
20
+ m << "\n URL: #{request[:uri]}"
21
+ m << "\n Params: #{request[:params].inspect}"
22
+ m << "\n Data: #{request[:data].inspect}" if request[:data]
23
+ m << "\n Headers: #{request[:headers].inspect}"
24
+ if response
25
+ m << "\n\nSolr Response:"
26
+ m << "\n Code: #{response[:status]}"
27
+ m << "\n Headers: #{response[:headers].inspect}"
28
+ end
29
+ m
30
+ end
31
+
32
+ protected
33
+
34
+ def parse_solr_error_response body
35
+ begin
36
+ info = body.scan(/<pre>(.*)<\/pre>/mi)[0]
37
+ partial = info.to_s.split("\n")[0..10]
38
+ partial.join("\n").gsub("&gt;", ">").gsub("&lt;", "<")
39
+ rescue
40
+ nil
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ class Http < RuntimeError
47
+
48
+ include SolrContext
49
+
50
+ # ripped right from ActionPack
51
+ # Defines the standard HTTP status codes, by integer, with their
52
+ # corresponding default message texts.
53
+ # Source: http://www.iana.org/assignments/http-status-codes
54
+ STATUS_CODES = {
55
+ 100 => "Continue",
56
+ 101 => "Switching Protocols",
57
+ 102 => "Processing",
58
+
59
+ 200 => "OK",
60
+ 201 => "Created",
61
+ 202 => "Accepted",
62
+ 203 => "Non-Authoritative Information",
63
+ 204 => "No Content",
64
+ 205 => "Reset Content",
65
+ 206 => "Partial Content",
66
+ 207 => "Multi-Status",
67
+ 226 => "IM Used",
68
+
69
+ 300 => "Multiple Choices",
70
+ 301 => "Moved Permanently",
71
+ 302 => "Found",
72
+ 303 => "See Other",
73
+ 304 => "Not Modified",
74
+ 305 => "Use Proxy",
75
+ 307 => "Temporary Redirect",
76
+
77
+ 400 => "Bad Request",
78
+ 401 => "Unauthorized",
79
+ 402 => "Payment Required",
80
+ 403 => "Forbidden",
81
+ 404 => "Not Found",
82
+ 405 => "Method Not Allowed",
83
+ 406 => "Not Acceptable",
84
+ 407 => "Proxy Authentication Required",
85
+ 408 => "Request Timeout",
86
+ 409 => "Conflict",
87
+ 410 => "Gone",
88
+ 411 => "Length Required",
89
+ 412 => "Precondition Failed",
90
+ 413 => "Request Entity Too Large",
91
+ 414 => "Request-URI Too Long",
92
+ 415 => "Unsupported Media Type",
93
+ 416 => "Requested Range Not Satisfiable",
94
+ 417 => "Expectation Failed",
95
+ 422 => "Unprocessable Entity",
96
+ 423 => "Locked",
97
+ 424 => "Failed Dependency",
98
+ 426 => "Upgrade Required",
99
+
100
+ 500 => "Internal Server Error",
101
+ 501 => "Not Implemented",
102
+ 502 => "Bad Gateway",
103
+ 503 => "Service Unavailable",
104
+ 504 => "Gateway Timeout",
105
+ 505 => "HTTP Version Not Supported",
106
+ 507 => "Insufficient Storage",
107
+ 510 => "Not Extended"
108
+ }
109
+
110
+ def initialize request, response
111
+ @request, @response = request, response
112
+ end
113
+
114
+ end
115
+
116
+ # Thrown if the :wt is :ruby
117
+ # but the body wasn't succesfully parsed/evaluated
118
+ class InvalidRubyResponse < Http
119
+
120
+ end
121
+
122
+ end
data/lib/rsolr/http.rb ADDED
@@ -0,0 +1,106 @@
1
+ module RSolr
2
+
3
+ class Http
4
+
5
+ attr_reader :uri, :proxy, :options
6
+
7
+ def initialize base_url, options = {}
8
+ @options = options
9
+ @uri = base_url
10
+ @proxy = options[:proxy]
11
+ end
12
+
13
+ def base_uri
14
+ @proxy ? @proxy.request_uri : @uri.request_uri
15
+ end
16
+
17
+ def http
18
+ @http ||= (
19
+ http = if proxy
20
+ proxy_user, proxy_pass = proxy.userinfo.split(/:/) if proxy.userinfo
21
+ Net::HTTP.Proxy(proxy.host, proxy.port, proxy_user, proxy_pass).new uri.host, uri.port
22
+ else
23
+ Net::HTTP.new uri.host, uri.port
24
+ end
25
+
26
+ http.use_ssl = uri.port == 443 || uri.instance_of?(URI::HTTPS)
27
+
28
+ if options[:timeout] && options[:timeout].is_a?(Integer)
29
+ http.open_timeout = options[:timeout]
30
+ http.read_timeout = options[:timeout]
31
+ end
32
+
33
+ if options[:pem] && http.use_ssl?
34
+ http.cert = OpenSSL::X509::Certificate.new(options[:pem])
35
+ http.key = OpenSSL::PKey::RSA.new(options[:pem])
36
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
37
+ else
38
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
39
+ end
40
+
41
+ if options[:debug_output]
42
+ http.set_debug_output(options[:debug_output])
43
+ end
44
+
45
+ http
46
+ )
47
+ end
48
+
49
+ # send in path w/query
50
+ def get request_uri, headers = {}
51
+ req = setup_raw_request Net::HTTP::Get, request_uri, headers
52
+ perform_request http, req
53
+ end
54
+
55
+ # send in path w/query
56
+ def head request_uri, headers = {}
57
+ req = setup_raw_request Net::HTTP::Head, request_uri, headers
58
+ perform_request http, req
59
+ end
60
+
61
+ # send in path w/query
62
+ def post request_uri, data, headers = {}
63
+ req = setup_raw_request Net::HTTP::Post, request_uri, headers
64
+ req.body = data if data
65
+ perform_request http, req
66
+ end
67
+
68
+ def perform_request http, request
69
+ begin
70
+ response = http.request request
71
+ {:status => response.code.to_i, :headers => response.to_hash, :body => response.body}
72
+ rescue NoMethodError
73
+ $!.message == "undefined method `closed?' for nil:NilClass" ?
74
+ raise(Errno::ECONNREFUSED.new) :
75
+ raise($!)
76
+ end
77
+ end
78
+
79
+ def setup_raw_request http_method, request_uri, headers = {}
80
+ raw_request = http_method.new "#{base_uri}#{request_uri}"
81
+ raw_request.initialize_http_header headers
82
+ raw_request.basic_auth username, password if options[:basic_auth]
83
+ if options[:digest_auth]
84
+ res = http.head(request_uri, headers)
85
+ if res['www-authenticate'] != nil && res['www-authenticate'].length > 0
86
+ raw_request.digest_auth username, password, res
87
+ end
88
+ end
89
+ raw_request
90
+ end
91
+
92
+ def credentials
93
+ options[:basic_auth] || options[:digest_auth]
94
+ end
95
+
96
+ def username
97
+ credentials[:username]
98
+ end
99
+
100
+ def password
101
+ credentials[:password]
102
+ end
103
+
104
+ end
105
+
106
+ end
data/lib/rsolr/uri.rb ADDED
@@ -0,0 +1,55 @@
1
+ module RSolr::Uri
2
+
3
+ def self.create url
4
+ ::URI.parse url[-1] == ?/ ? url : "#{url}/"
5
+ end
6
+
7
+ # Returns a query string param pair as a string.
8
+ # Both key and value are escaped.
9
+ def build_param(k,v)
10
+ "#{escape_query_value(k)}=#{escape_query_value(v)}"
11
+ end
12
+
13
+ # Return the bytesize of String; uses String#size under Ruby 1.8 and
14
+ # String#bytesize under 1.9.
15
+ if ''.respond_to?(:bytesize)
16
+ def bytesize(string)
17
+ string.bytesize
18
+ end
19
+ else
20
+ def bytesize(string)
21
+ string.size
22
+ end
23
+ end
24
+
25
+ # Creates a Solr based query string.
26
+ # Keys that have arrays values are set multiple times:
27
+ # params_to_solr(:q => 'query', :fq => ['a', 'b'])
28
+ # is converted to:
29
+ # ?q=query&fq=a&fq=b
30
+ def params_to_solr(params)
31
+ mapped = params.map do |k, v|
32
+ next if v.to_s.empty?
33
+ if v.class == Array
34
+ params_to_solr(v.map { |x| [k, x] })
35
+ else
36
+ build_param k, v
37
+ end
38
+ end
39
+ mapped.compact.join("&")
40
+ end
41
+
42
+ # Performs URI escaping so that you can construct proper
43
+ # query strings faster. Use this rather than the cgi.rb
44
+ # version since it's faster.
45
+ # (Stolen from Rack).
46
+ def escape_query_value(s)
47
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
48
+ #'%'+$1.unpack('H2'*$1.size).join('%').upcase
49
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
50
+ }.tr(' ', '+')
51
+ end
52
+
53
+ extend self
54
+
55
+ end