right_support 1.4.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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