right_support 1.4.1 → 2.0.0

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
@@ -9,57 +9,76 @@ SystemLogger is a rewrite of the seattle.rb SyslogLogger class that features the
9
9
  * Inherits from standard Ruby Logger class for guaranteed compatibility
10
10
  * Can be configured with options about how to handle newlines, ANSI escape codes, etc
11
11
 
12
- It is very similar to SystemLogger, with a few differences (e.g. it is a child class of Logger instead of
13
- merely being duck-type compatible). You use it as follows.
12
+ We also provide Log::Mixin, a powerful tool that lets you sprinkle a logger
13
+ into any object or class, and supports per-class or per-instance custom
14
+ loggers!
14
15
 
15
- @logger = SystemLogger.new('my_cool_app', :split=>true, :color=>false)
16
- @logger.info "Hello world\nThis will appear on separate lines\nand without \e[33;0mbeautiful colors"
16
+ class Jet < Aircraft
17
+ include RightSupport::Log.Mixin
18
+ end
17
19
 
18
- CustomLogger is a Rack middleware that allows a Rack app to use any log sink it wishes. The
19
- stock Rack logger middleware is inflexible and gives the end user no control over which logger is used
20
- or where the log entries go.
20
+ #most jets log to the syslog
21
+ Jet.logger = RightSupport::Log::SystemLogger.new('black box',
22
+ :facility=>'local0')
21
23
 
22
- require 'right_support/rack/custom_logger'
24
+ #but MY jet
25
+ @my_jet.logger = Logger.new(StringIO.new)
23
26
 
24
- my_logger = MyAwesomeLogger.new
25
- use RightSupport::Rack::CustomLogger, Logger::INFO, my_logger
27
+ ==== Logging for Rack Applications
26
28
 
27
- === String Manipulation
29
+ RightSupport also provides Rack middleware that allows a Rack app to use any log sink it wishes. The
30
+ stock Rack logger middleware is inflexible and gives the end user no control over which logger is used
31
+ or where the log entries go. Example of usage on Sinatra based application:
32
+
33
+ class MyApp < Sinatra::Base
34
+ ...
35
+ # Specify which logger will be used as default logger
36
+ LOGGER =\
37
+ if ENV['RACK_ENV'].downcase == 'development'
38
+ Logger.new(STDOUT)
39
+ else
40
+ RightSupport::Log::SystemLogger.new("MyApp", {:facility => "local0"})
41
+ end
42
+
43
+ # use the RequestLogger via LogSetter to log requests for application
44
+ use RightSupport::Rack::LogSetter, {:logger => LOGGER}
45
+ use RightSupport::Rack::RequestLogger
46
+
47
+ # set logger and mix Log::Mixin
48
+ RightSupport::Log::Mixin.default_logger = LOGGER
49
+ include RightSupport::Log::Mixin
50
+ ...
51
+ end
28
52
 
29
- StringExtensions contains String#camelize, which is only defined if the
30
- ActiveSupport gem cannot be loaded. It also has some RightScale-specific
31
- methods:
32
- * String#snake_case
33
- * String#to_const
34
- * String#to_const_path
53
+ After that all rack requests to MyApp will be logged, and since we mixed Log::Mixin we can use:
35
54
 
36
- Net::StringEncoder applies and removes URL-escape, base64 and other encodings.
55
+ logger.info "Hello world\nThis will appear on separate lines\nand without \e[33;0mbeautiful colors"
37
56
 
38
- === Input Validation
57
+ === Networking Stuff
39
58
 
40
- Validation contains several format-checkers that can be used to validate your
41
- web app's models before saving, check for preconditions in your controllers,
42
- and so forth.
59
+ ==== HTTP Client
43
60
 
44
- You can use it as a mixin by including the appropriate child module of
45
- RightSupport::Validation.
61
+ We provide a very thin wrapper around the rest-client gem that enables simple but
62
+ robust rest requests with a timeout, headers, etc.
46
63
 
47
- class AwesomenessGenerator < ActiveRecord::Base
48
- include RightSupport::Validation::OpenSSL
64
+ HTTPClient is interface-compatible with the RestClient module, but allows an
65
+ optional timeout to be specified as an extra parameter.
49
66
 
50
- before_save do |record|
51
- errors[:foo] = 'Hey, that's not a key!' unless pem_public_key?(record.foo)
52
- end
53
- end
67
+ # Create a wrapper object
68
+ @client = RightSupport::Net::HTTPClient.new
54
69
 
55
- But you really don't want to do that, do you? Instead, you want to call the module
56
- methods of RightSupport::Validation, which contains all of the same mixin methods,
57
- but does not pollute the dispatch table of your application classes.
70
+ # Default timeout is 5 seconds
71
+ @client.get('http://localhost')
58
72
 
59
- the_key = STDIN.read
60
- raise ArgumentError unless RightSupport::Validation.ssh_public_key?(the_key)
73
+ # Make sure the HTTPClient request fails after 1 second so we can report an error
74
+ # and move on!
75
+ @client.get('http://localhost', {:headers => {'X-Hello'=>'hi!'}, :timeout => 1)}
61
76
 
62
- === Client-Side Load Balancer
77
+ # HTTPClient transforms String or Hash :query into query string, for example;
78
+ @client.get('http://localhost/moo', {:query=>{:a=>{:b=>:c}}} )
79
+ # the url that would be requested is http://localhost/moo?a[b]=c
80
+
81
+ ==== Client-Side Load Balancer
63
82
 
64
83
  RequestBalancer randomly chooses endpoints for a network request, which lets
65
84
  you perform easy client-side load balancing:
@@ -71,26 +90,23 @@ you perform easy client-side load balancing:
71
90
  REST.get(url)
72
91
  end
73
92
 
74
- The balancer will keep trying requests until one of them succeeds without throwing
75
- any exceptions. (NB: a nil return value counts as success!!)
93
+ The balancer will keep trying requests until one of them succeeds without
94
+ throwing any exceptions. (NB: a nil return value counts as success!!)
76
95
 
77
- === HTTP Client
96
+ ==== HTTP Request Tracking for Rack
78
97
 
79
- We provide a very thin wrapper around the rest-client gem that enables simple but
80
- robust rest requests with a timeout, headers, etc.
98
+ Correlate data flows across your entire architecture with the RequestTracker
99
+ middleware, which uses custom HTTP headers to "tag" every Web request with
100
+ a UUID, and works with RequestLogger to log "begin" and "end" lines containing
101
+ the UUID.
81
102
 
82
- HTTPClient is interface-compatible with the RestClient module, but allows an
83
- optional timeout to be specified as an extra parameter.
84
-
85
- # Create a wrapper object
86
- @client = RightSupport::Net::HTTPClient.new
87
-
88
- # Default timeout is 5 seconds
89
- @client.get('http://localhost')
103
+ If your app consumes the UUID and passes it on to HTTP services that it
104
+ invokes, the same request UUID will appear in all logs and you can grep for
105
+ the "big picture" instead of wasting time paging between logs.
90
106
 
91
- # Make sure the HTTPClient request fails after 1 second so we can report an error
92
- # and move on!
93
- @client.get('http://localhost', {:headers => {'X-Hello'=>'hi!'}, :timeout => 1)}
107
+ To use this functionality you need:
108
+
109
+ use RightSupport::Rack::RequestTracker
94
110
 
95
111
  === Statistics Gathering
96
112
 
@@ -107,27 +123,37 @@ Profile your compute-heavy and network activities using a Stats::Activity counte
107
123
 
108
124
  puts "Only %.1f lines/sec? You are a slow typist!" % [stats.avg_rate]
109
125
 
110
- === Configuration
126
+ === Input Validation
111
127
 
112
- RightSupport::Config contains functionality, which provides possibility
113
- to use human-readable yaml configuration files. For example, you have following yaml
114
- file '/tmp/features_config.yml', with content:
128
+ Validation contains several format-checkers that can be used to validate your
129
+ web app's models before saving, check for preconditions in your controllers,
130
+ and so forth.
115
131
 
116
- eat:
117
- khlav kalash: YES!
118
- speak:
119
- klingonese: false
120
- belarusian: true
132
+ You can use it as a mixin by including the appropriate child module of
133
+ RightSupport::Validation.
121
134
 
122
- Then, if you would add configuration in you class, like:
135
+ class AwesomenessGenerator < ActiveRecord::Base
136
+ include RightSupport::Validation::OpenSSL
123
137
 
124
- class SweetestClass
125
- CONFIG = RightSupport::Config.features('/tmp/features_config.yml')
138
+ before_save do |record|
139
+ errors[:foo] = 'Hey, that's not a key!' unless pem_public_key?(record.foo)
140
+ end
126
141
  end
127
142
 
128
- * RightSupport::Config.features receives file` path, io or string.
143
+ But you really don't want to do that, do you? Instead, you want to call the module
144
+ methods of RightSupport::Validation, which contains all of the same mixin methods,
145
+ but does not pollute the dispatch table of your application classes.
129
146
 
130
- Now you can call CONFIG['feature_group']['feature'], like:
147
+ the_key = STDIN.read
148
+ raise ArgumentError unless RightSupport::Validation.ssh_public_key?(the_key)
131
149
 
132
- * SweetestClass::CONFIG['eat']['khlav kalash'] would return 'YES!'
133
- * SweetestClass::CONFIG['speak']['belarusian'] would return true
150
+ === String Manipulation
151
+
152
+ StringExtensions contains String#camelize, which is only defined if the
153
+ ActiveSupport gem cannot be loaded. It also has some RightScale-specific
154
+ methods:
155
+ * String#snake_case
156
+ * String#to_const
157
+ * String#to_const_path
158
+
159
+ Net::StringEncoder applies and removes URL-escape, base64 and other encodings.
data/lib/right_support.rb CHANGED
@@ -2,8 +2,8 @@
2
2
  require 'thread'
3
3
 
4
4
  require 'right_support/ruby'
5
+ require 'right_support/data'
5
6
  require 'right_support/crypto'
6
- require 'right_support/config'
7
7
  require 'right_support/db'
8
8
  require 'right_support/log'
9
9
  require 'right_support/net'
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2012 RightScale Inc
2
+ # Copyright (c) 2011 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -22,13 +22,11 @@
22
22
 
23
23
  module RightSupport
24
24
  #
25
- # A namespace for configuration tools.
25
+ # A namespace for data-processing tools.
26
26
  #
27
- module Config
27
+ module Data
28
28
 
29
29
  end
30
30
  end
31
31
 
32
- require 'right_support/config/feature_set'
33
- require 'right_support/config/yaml_config'
34
- require 'right_support/config/recursive_true_class'
32
+ require 'right_support/data/uuid'
@@ -0,0 +1,175 @@
1
+ #
2
+ # Copyright (c) 2011 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require 'set'
24
+
25
+ module RightSupport::Data
26
+ module UUID
27
+ # Exception that indicates a given implementation is unavailable.
28
+ class Unavailable < Exception; end
29
+
30
+ # The base class for all UUID implementations. Subclasses must override
31
+ # #initialize such that they raise an exception if the implementation is
32
+ # not available, and #generate such that it returns a String containing
33
+ # a UUID.
34
+ class Implementation
35
+ @subclasses = Set.new
36
+
37
+ def self.inherited(klass)
38
+ @subclasses << klass
39
+ end
40
+
41
+ # Determine which UUID implementations are available in this Ruby VM.
42
+ #
43
+ # === Return
44
+ # available(Array):: the available implementation classes
45
+ def self.available
46
+ avail = []
47
+
48
+ @subclasses.each do |eng|
49
+ begin
50
+ eng.new #if it initializes, it's available!
51
+ avail << eng
52
+ rescue Exception => e
53
+ #must not be available!
54
+ end
55
+ end
56
+ end
57
+
58
+ # Determine the default UUID implementation in this Ruby VM.
59
+ #
60
+ # === Return
61
+ # available(Array):: the available implementation classes
62
+ def self.default
63
+ #completely arbitrary
64
+ available.first
65
+ end
66
+
67
+ def generate
68
+ raise Unavailable, "This implementation is currently not available"
69
+ end
70
+ end
71
+
72
+ # An implementation that uses the SimpleUUID gem.
73
+ class SimpleUUID < Implementation
74
+ def initialize
75
+ require 'simple_uuid'
76
+ generate
77
+ end
78
+
79
+ def generate
80
+ ::SimpleUUID::UUID.new.to_guid
81
+ end
82
+ end
83
+
84
+ # An implementation that uses UUIDTools v1.
85
+ class UUIDTools1 < Implementation
86
+ def initialize
87
+ require 'uuidtools'
88
+ generate
89
+ end
90
+
91
+ def generate
92
+ ::UUID.timestamp_create.to_s
93
+ end
94
+ end
95
+
96
+ # An implementation that uses UUIDTools v2.
97
+ class UUIDTools2 < Implementation
98
+ def initialize
99
+ require 'uuidtools'
100
+ generate
101
+ end
102
+
103
+ def generate
104
+ ::UUIDTools::UUID.timestamp_create.to_s
105
+ end
106
+ end
107
+
108
+ # An implementation that uses the "uuid" gem.
109
+ class UUIDGem < Implementation
110
+ def initialize
111
+ require 'uuid'
112
+ generate
113
+ end
114
+
115
+ def generate
116
+ ::UUID.new.to_s
117
+ end
118
+ end
119
+
120
+ module_function
121
+
122
+ def generate
123
+ impl = implementation
124
+ raise Unavailable, "No implementation is available" unless impl
125
+ impl.generate
126
+ end
127
+
128
+ # Return the implementation that will be used to generate UUIDs.
129
+ #
130
+ # === Return
131
+ # The implementation object.
132
+ #
133
+ # === Raise
134
+ # Unavailable:: if no suitable implementation is available
135
+ #
136
+ def implementation
137
+ return @implementation if @implementation
138
+
139
+ if (defl = Implementation.default)
140
+ self.implementation = defl
141
+ else
142
+ raise Unavailable, "No implementation is available"
143
+ end
144
+
145
+ @implementation
146
+ end
147
+
148
+ # Set the implementation that will be used to generate UUIDs.
149
+ #
150
+ # === Parameters
151
+ # klass(inherits-from Implementation):: the implementation class
152
+ #
153
+ # === Return
154
+ # The class that was passed as a parameter
155
+ #
156
+ # === Raise
157
+ # Unavailable:: if the specified implementation is not available
158
+ # ArgumentError:: if klass is not a subclass of Implementation, or is not available
159
+ #
160
+ def implementation=(klass)
161
+ if klass.is_a?(Implementation)
162
+ @implementation = klass
163
+ elsif klass.is_a?(Class) && klass.ancestors.include?(Implementation)
164
+ @implementation = klass.new
165
+ elsif klass.nil?
166
+ @implementation = nil
167
+ else
168
+ raise ArgumentError, "Expected Implementation instance or subclass, got #{klass}"
169
+ end
170
+ rescue Exception => e
171
+ raise Unavailable, "#{klass} is not available due to a #{e.class}: #{e.message}"
172
+ end
173
+ end
174
+
175
+ end
@@ -31,7 +31,6 @@ end
31
31
 
32
32
  require 'right_support/log/system_logger'
33
33
  require 'right_support/log/filter_logger'
34
- require 'right_support/log/tag_logger'
35
34
  require 'right_support/log/null_logger'
36
35
  require 'right_support/log/multiplexer'
37
36
  require 'right_support/log/exception_logger'
@@ -23,7 +23,7 @@
23
23
  require 'logger'
24
24
 
25
25
  module RightSupport::Log
26
- if_require_succeeds('syslog') do
26
+ if require_succeeds?('syslog')
27
27
  # A logger that forwards log entries to the Unix syslog facility, but complies
28
28
  # with the interface of the Ruby Logger object and faithfully translates log
29
29
  # severities and other concepts. Provides optional cleanup/filtering in order
@@ -32,7 +32,7 @@ end
32
32
  require 'right_support/net/address_helper'
33
33
  require 'right_support/net/http_client'
34
34
  require 'right_support/net/string_encoder'
35
- require 'right_support/net/balancing'
35
+ require 'right_support/net/lb'
36
36
  require 'right_support/net/request_balancer'
37
37
  require 'right_support/net/ssl'
38
38
  require 'right_support/net/dns'
@@ -1,12 +1,12 @@
1
1
  module RightSupport::Net
2
- if_require_succeeds('right_http_connection') do
2
+ if require_succeeds?('right_http_connection')
3
3
  #nothing, nothing at all! just need to make sure
4
4
  #that RightHttpConnection gets loaded before
5
5
  #rest-client, so the Net::HTTP monkey patches
6
6
  #take effect.
7
7
  end
8
8
 
9
- if_require_succeeds('restclient') do
9
+ if require_succeeds?('restclient')
10
10
  HAS_REST_CLIENT = true
11
11
  end
12
12
 
@@ -24,31 +24,74 @@ module RightSupport::Net
24
24
  #
25
25
  #
26
26
  # HTTPClient is a thin wrapper around the RestClient::Request class, with a few minor changes to its
27
- # interface:
28
- # * initializer accepts some default request options that can be overridden per-request
29
- # * it has discrete methods for get/put/post/delete, instead of a single "request" method
30
- #
31
- # # create an instance ot HTTPClient with some default request options
27
+ # interface, namely:
28
+ # * initializer accepts some default request options that can be overridden
29
+ # per-request
30
+ # * it has discrete methods for get/put/post/delete, instead of a single
31
+ # "request" method
32
+ # * it supports explicit :query and :payload options for query-string and
33
+ # request-body, and understands the Rails convention for encoding a
34
+ # nested Hash into request parameters.
35
+ #
36
+ # == Request Parameters
37
+ # You can include a query-string with your request by passing the :query
38
+ # option to any of the request methods. You can pass a Hash, which will
39
+ # be translated to a URL-encoded query string using the Rails convention
40
+ # for nesting. Or, you can pass a String which will be appended verbatim
41
+ # to the URL. (In this case, don't forget to CGI.escape your string!)
42
+ #
43
+ # To include a form with your request, pass the :payload option to any
44
+ # request method. You can pass a Hash, which will be translated to an
45
+ # x-www-form-urlencoded request body using the Rails convention for
46
+ # nesting. Or, you can pass a String which will be appended verbatim
47
+ # to the URL. You can even use a binary String combined with a
48
+ # suitable request-content-type header to pass binary data in the
49
+ # payload. (In this case, be very careful about String encoding under
50
+ # Ruby 1.9!)
51
+ #
52
+ # == Usage Examples
53
+ #
54
+ # # Create an instance ot HTTPClient with some default request options
32
55
  # @client = HTTPClient.new()
33
56
  #
34
- # # GET
57
+ # # Simple GET
35
58
  # xml = @client.get 'http://example.com/resource'
36
- # # and, with timeout of 5 seconds...
37
- # jpg = @client.get 'http://example.com/resource', {:accept => 'image/jpg', :timeout => 5}
38
- #
39
- # # authentication and SSL
40
- # @client.get 'https://user:password@example.com/private/resource'
41
59
  #
42
- # # POST or PUT with a hash sends parameters as a urlencoded form body
43
- # @client.post 'http://example.com/resource', {:param1 => 'one'}
60
+ # # And, with timeout of 5 seconds...
61
+ # jpg = @client.get 'http://example.com/resource',
62
+ # {:accept => 'image/jpg', :timeout => 5}
44
63
  #
45
- # # nest hash parameters, add a timeout of 10 seconds (and specify "no extra headers")
46
- # @client.post 'http://example.com/resource', {:payload => {:nested => {:param1 => 'one'}}, :timeout => 10}
64
+ # # Doing some client authentication and SSL.
65
+ # @client.get 'https://user:password@example.com/private/resource'
66
+ #
67
+ # # The :query option will be encoded as a URL query-string using Rails
68
+ # # nesting convention (e.g. "a[b]=c" for this case).
69
+ # @client.get 'http://example.com', :query=>{:a=>{:b=>'c'}}
70
+ #
71
+ # # The :payload option specifies the request body. You can specify a raw
72
+ # # payload:
73
+ # @client.post 'http://example.com/resource', :payload=>'hi hi hi lol lol'
74
+ #
75
+ # # Or, you can specify a Hash payload which will be translated to a
76
+ # # x-www-form-urlencoded request body using the Rails nesting convention.
77
+ # # (e.g. "a[b]=c" for this case)
78
+ # @client.post 'http://example.com/resource', :payload=>{:d=>{:e=>'f'}}
79
+ #
80
+ # # You can specify query and/or payload for any HTTP verb, even if it
81
+ # # defies convention (be careful!)
82
+ # @client.post 'http://example.com/resource',
83
+ # :query => {:query_string_param=>'hi'}
84
+ # :payload => {:form_param=>'hi'}, :timeout => 10
47
85
  #
48
86
  # # POST and PUT with raw payloads
49
- # @client.post 'http://example.com/resource', {:payload => 'the post body', :headers => {:content_type => 'text/plain'}}
50
- # @client.post 'http://example.com/resource.xml', {:payload => xml_doc}
51
- # @client.put 'http://example.com/resource.pdf', {:payload => File.read('my.pdf'), :headers => {:content_type => 'application/pdf'}}
87
+ # @client.post 'http://example.com/resource',
88
+ # {:payload => 'the post body',
89
+ # :headers => {:content_type => 'text/plain'}}
90
+ # @client.post 'http://example.com/resource.xml',
91
+ # {:payload => xml_doc}
92
+ # @client.put 'http://example.com/resource.pdf',
93
+ # {:payload => File.read('my.pdf'),
94
+ # :headers => {:content_type => 'application/pdf'}}
52
95
  #
53
96
  # # DELETE
54
97
  # @client.delete 'http://example.com/resource'
@@ -58,18 +101,20 @@ module RightSupport::Net
58
101
  # res.code # => 200
59
102
  # res.headers[:content_type] # => 'image/jpg'
60
103
  class HTTPClient
61
-
62
- DEFAULT_TIMEOUT = 5
63
- DEFAULT_OPEN_TIMEOUT = 2
64
-
104
+ # The default options for every request; can be overridden by options
105
+ # passed to #initialize or to the individual request methods (#get,
106
+ # #post, and so forth).
107
+ DEFAULT_OPTIONS = {
108
+ :timeout => 5,
109
+ :open_timeout => 2,
110
+ :headers => {}
111
+ }
112
+
65
113
  def initialize(defaults = {})
66
- @defaults = defaults.clone
67
- @defaults[:timeout] ||= DEFAULT_TIMEOUT
68
- @defaults[:open_timeout] ||= DEFAULT_OPEN_TIMEOUT
69
- @defaults[:headers] ||= {}
114
+ @defaults = DEFAULT_OPTIONS.merge(defaults)
70
115
  end
71
116
 
72
- def get(*args)
117
+ def get(*args)
73
118
  request(:get, *args)
74
119
  end
75
120
 
@@ -94,7 +139,8 @@ module RightSupport::Net
94
139
  # === Options
95
140
  # This method can accept any of the options that RestClient::Request can accept, since
96
141
  # all options are proxied through after merging in defaults, etc. Interesting options:
97
- # * :payload - hash containing the request body (e.g. POST or PUT parameters)
142
+ # * :query - hash containing a query string (GET parameters) as a Hash or String
143
+ # * :payload - hash containing the request body (POST or PUT parameters) as a Hash or String
98
144
  # * :headers - hash containing additional HTTP request headers
99
145
  # * :cookies - will replace possible cookies in the :headers
100
146
  # * :user and :password - for basic auth, will be replaced by a user/password available in the url
@@ -108,13 +154,53 @@ module RightSupport::Net
108
154
  #
109
155
  def request(type, url, options={}, &block)
110
156
  options = @defaults.merge(options)
157
+
158
+ # Handle query-string option which is implemented by us, not by rest-client.
159
+ # (rest-client version of this, :headers={:params=>...}) but it
160
+ # is broken in many ways and not suitable for use!)
161
+ if query = options.delete(:query)
162
+ url = process_query_string(url, query)
163
+ end
164
+
111
165
  options.merge!(:method => type, :url => url)
112
166
 
113
167
  request_internal(options, &block)
114
168
  end
115
169
 
116
- protected
117
-
170
+ private
171
+
172
+ # Process a query-string option and append it to the URL as a properly
173
+ # encoded query string. The input must be either a String or Hash.
174
+ #
175
+ # === Parameters
176
+ # url(String):: the URL to request, including any query-string parameters
177
+ # query(Hash|String):: the URL params, that will be added to URL, Hash or String
178
+ #
179
+ # === Return
180
+ # Returns url with concated with parameters.
181
+ def process_query_string(url='', query={})
182
+ url_params = ''
183
+
184
+ if query.kind_of?(String)
185
+ url_params = query.gsub(/^\?/, '')
186
+ elsif query.kind_of?(Hash)
187
+ if require_succeeds?('addressable/uri')
188
+ uri = Addressable::URI.new
189
+ uri.query_values = query
190
+ url_params = uri.query
191
+ else
192
+ url_params = query.collect { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&')
193
+ end
194
+ else
195
+ raise ArgumentError.new("Parameter query should be String or Hash")
196
+ end
197
+ unless (url+url_params)[/\?/]
198
+ url_params = '?' + url_params unless url_params.empty?
199
+ end
200
+
201
+ url + url_params
202
+ end
203
+
118
204
  # Wrapper around RestClient::Request.execute -- see class documentation for details.
119
205
  def request_internal(options, &block)
120
206
  if HAS_REST_CLIENT