htty 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. data/MIT-LICENSE.rdoc +9 -0
  2. data/README.rdoc +199 -0
  3. data/VERSION +1 -0
  4. data/app/htty.rb +14 -0
  5. data/app/htty/cli.rb +77 -0
  6. data/app/htty/cli/command.rb +185 -0
  7. data/app/htty/cli/commands.rb +43 -0
  8. data/app/htty/cli/commands/address.rb +84 -0
  9. data/app/htty/cli/commands/body_clear.rb +20 -0
  10. data/app/htty/cli/commands/body_request.rb +51 -0
  11. data/app/htty/cli/commands/body_response.rb +59 -0
  12. data/app/htty/cli/commands/body_set.rb +58 -0
  13. data/app/htty/cli/commands/body_unset.rb +45 -0
  14. data/app/htty/cli/commands/cd.rb +20 -0
  15. data/app/htty/cli/commands/cookie_add.rb +20 -0
  16. data/app/htty/cli/commands/cookie_remove.rb +20 -0
  17. data/app/htty/cli/commands/cookies.rb +68 -0
  18. data/app/htty/cli/commands/cookies_add.rb +62 -0
  19. data/app/htty/cli/commands/cookies_clear.rb +20 -0
  20. data/app/htty/cli/commands/cookies_remove.rb +60 -0
  21. data/app/htty/cli/commands/cookies_remove_all.rb +50 -0
  22. data/app/htty/cli/commands/cookies_use.rb +57 -0
  23. data/app/htty/cli/commands/delete.rb +20 -0
  24. data/app/htty/cli/commands/exit.rb +20 -0
  25. data/app/htty/cli/commands/follow.rb +56 -0
  26. data/app/htty/cli/commands/form.rb +20 -0
  27. data/app/htty/cli/commands/form_add.rb +20 -0
  28. data/app/htty/cli/commands/form_clear.rb +26 -0
  29. data/app/htty/cli/commands/form_remove.rb +20 -0
  30. data/app/htty/cli/commands/form_remove_all.rb +20 -0
  31. data/app/htty/cli/commands/fragment_clear.rb +20 -0
  32. data/app/htty/cli/commands/fragment_set.rb +59 -0
  33. data/app/htty/cli/commands/fragment_unset.rb +48 -0
  34. data/app/htty/cli/commands/get.rb +20 -0
  35. data/app/htty/cli/commands/header_set.rb +20 -0
  36. data/app/htty/cli/commands/header_unset.rb +20 -0
  37. data/app/htty/cli/commands/headers_clear.rb +20 -0
  38. data/app/htty/cli/commands/headers_request.rb +70 -0
  39. data/app/htty/cli/commands/headers_response.rb +65 -0
  40. data/app/htty/cli/commands/headers_set.rb +57 -0
  41. data/app/htty/cli/commands/headers_unset.rb +54 -0
  42. data/app/htty/cli/commands/headers_unset_all.rb +48 -0
  43. data/app/htty/cli/commands/help.rb +100 -0
  44. data/app/htty/cli/commands/history.rb +60 -0
  45. data/app/htty/cli/commands/history_verbose.rb +81 -0
  46. data/app/htty/cli/commands/host_set.rb +59 -0
  47. data/app/htty/cli/commands/http_delete.rb +40 -0
  48. data/app/htty/cli/commands/http_get.rb +42 -0
  49. data/app/htty/cli/commands/http_head.rb +36 -0
  50. data/app/htty/cli/commands/http_options.rb +36 -0
  51. data/app/htty/cli/commands/http_post.rb +34 -0
  52. data/app/htty/cli/commands/http_put.rb +29 -0
  53. data/app/htty/cli/commands/http_trace.rb +36 -0
  54. data/app/htty/cli/commands/path_set.rb +54 -0
  55. data/app/htty/cli/commands/port_set.rb +55 -0
  56. data/app/htty/cli/commands/post.rb +20 -0
  57. data/app/htty/cli/commands/put.rb +20 -0
  58. data/app/htty/cli/commands/query_clear.rb +20 -0
  59. data/app/htty/cli/commands/query_set.rb +59 -0
  60. data/app/htty/cli/commands/query_unset.rb +56 -0
  61. data/app/htty/cli/commands/query_unset_all.rb +50 -0
  62. data/app/htty/cli/commands/quit.rb +24 -0
  63. data/app/htty/cli/commands/reuse.rb +74 -0
  64. data/app/htty/cli/commands/scheme_set.rb +69 -0
  65. data/app/htty/cli/commands/status.rb +52 -0
  66. data/app/htty/cli/commands/undo.rb +13 -0
  67. data/app/htty/cli/commands/userinfo_clear.rb +20 -0
  68. data/app/htty/cli/commands/userinfo_set.rb +56 -0
  69. data/app/htty/cli/commands/userinfo_unset.rb +47 -0
  70. data/app/htty/cli/cookie_clearing_command.rb +26 -0
  71. data/app/htty/cli/display.rb +182 -0
  72. data/app/htty/cli/http_method_command.rb +75 -0
  73. data/app/htty/cli/url_escaping.rb +27 -0
  74. data/app/htty/cookies_util.rb +36 -0
  75. data/app/htty/no_location_header_error.rb +12 -0
  76. data/app/htty/no_response_error.rb +12 -0
  77. data/app/htty/no_set_cookie_header_error.rb +13 -0
  78. data/app/htty/ordered_hash.rb +69 -0
  79. data/app/htty/payload.rb +48 -0
  80. data/app/htty/request.rb +471 -0
  81. data/app/htty/requests_util.rb +92 -0
  82. data/app/htty/response.rb +34 -0
  83. data/app/htty/session.rb +29 -0
  84. data/bin/htty +5 -0
  85. data/spec/unit/htty/cli_spec.rb +27 -0
  86. data/spec/unit/htty/ordered_hash_spec.rb +54 -0
  87. data/spec/unit/htty/request_spec.rb +1236 -0
  88. data/spec/unit/htty/response_spec.rb +0 -0
  89. data/spec/unit/htty/session_spec.rb +13 -0
  90. metadata +158 -0
@@ -0,0 +1,75 @@
1
+ # Defines HTTY::CLI::HTTPMethodCommand.
2
+
3
+ require File.expand_path("#{File.dirname __FILE__}/../request")
4
+ require File.expand_path("#{File.dirname __FILE__}/display")
5
+ require File.expand_path("#{File.dirname __FILE__}/commands/cookies_use")
6
+ # This 'require' statement leads to an unresolvable circular dependency.
7
+ # require File.expand_path("#{File.dirname __FILE__}/commands/follow")
8
+
9
+ module HTTY; end
10
+
11
+ class HTTY::CLI; end
12
+
13
+ # Encapsulates behavior common to all HTTP-method-oriented HTTY::CLI::Command
14
+ # subclasses.
15
+ module HTTY::CLI::HTTPMethodCommand
16
+
17
+ include HTTY::CLI::Display
18
+
19
+ # Returns the name of a category under which help for the _http-get_ command
20
+ # should appear.
21
+ def self.category
22
+ 'Issuing Requests'
23
+ end
24
+
25
+ # Performs the command.
26
+ def perform
27
+ add_request_if_has_response do |request|
28
+ request = request.send("#{method}!", *arguments)
29
+ unless body? || request.body.to_s.empty?
30
+ puts notice("The body of your #{method.upcase} request is not being " +
31
+ 'sent')
32
+ end
33
+ notify_if_cookies
34
+ notify_if_follow
35
+ request
36
+ end
37
+ show_response session.last_response
38
+ self
39
+ end
40
+
41
+ private
42
+
43
+ # Returns true if the command sends the request body.
44
+ def body?
45
+ HTTY::Request::METHODS_SENDING_BODY.include? method
46
+ end
47
+
48
+ def method
49
+ self.class.name.split('::').last.gsub(/^http/i, '').downcase.to_sym
50
+ end
51
+
52
+ def notify_if_cookies
53
+ request = session.requests.last
54
+ response = session.last_response
55
+ unless response.cookies.empty? || (request.cookies == response.cookies)
56
+ puts notice('Type ' +
57
+ "#{strong HTTY::CLI::Commands::CookiesUse.command_line} to " +
58
+ 'use cookies offered in the response')
59
+ end
60
+ self
61
+ end
62
+
63
+ def notify_if_follow
64
+ location_header = session.last_response.headers.detect do |header|
65
+ header.first == 'Location'
66
+ end
67
+ if location_header
68
+ puts notice('Type ' +
69
+ "#{strong HTTY::CLI::Commands::Follow.command_line} to " +
70
+ "follow the 'Location' header received in the response")
71
+ end
72
+ self
73
+ end
74
+
75
+ end
@@ -0,0 +1,27 @@
1
+ # Defines HTTY::CLI::UrlEscaping.
2
+
3
+ require 'uri'
4
+ require File.expand_path("#{File.dirname __FILE__}/display")
5
+
6
+ module HTTY; end
7
+
8
+ class HTTY::CLI; end
9
+
10
+ # Encapsulates the URL escaping logic of _htty_'s command-line interface.
11
+ module HTTY::CLI::UrlEscaping
12
+
13
+ include HTTY::CLI::Display
14
+
15
+ def escape_or_warn_of_escape_sequences(arguments)
16
+ arguments.collect do |a|
17
+ if a =~ /%[0-9a-f]{2}/i
18
+ say "Argument '#{a}' was not URL-escaped because it contains escape " +
19
+ 'sequences'
20
+ a
21
+ else
22
+ URI.escape a
23
+ end
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,36 @@
1
+ # Defines HTTY::CookiesUtil.
2
+
3
+ module HTTY; end
4
+
5
+ # Provides support for marshaling HTTP cookies to and from strings.
6
+ module HTTY::CookiesUtil
7
+
8
+ # Returns the specified _cookies_string_ HTTP header value deserialized to an
9
+ # array of cookies.
10
+ def self.cookies_from_string(cookies_string)
11
+ return [] unless cookies_string
12
+ cookies_string.split(COOKIES_DELIMITER).collect do |name_value_string|
13
+ name_and_value = name_value_string.split(COOKIE_NAME_VALUE_DELIMITER, 2)
14
+ name_and_value << nil if (name_and_value.length < 2)
15
+ name_and_value
16
+ end
17
+ end
18
+
19
+ # Returns the specified array of _cookies_ serialized to an HTTP header value.
20
+ # Returns +nil+ if _cookies_ is +nil+ or empty or if it contains only +nil+
21
+ # cookie values.
22
+ def self.cookies_to_string(cookies)
23
+ cookies = Array(cookies)
24
+ return nil if cookies.empty?
25
+
26
+ cookies.collect do |name, value|
27
+ [name, value].compact.join COOKIE_NAME_VALUE_DELIMITER
28
+ end.join COOKIES_DELIMITER
29
+ end
30
+
31
+ protected
32
+
33
+ COOKIE_NAME_VALUE_DELIMITER = '='
34
+ COOKIES_DELIMITER = '; '
35
+
36
+ end
@@ -0,0 +1,12 @@
1
+ # Defines HTTY::NoLocationHeaderError.
2
+
3
+ module HTTY; end
4
+
5
+ # Indicates that the _Location_ header was missing from HTTY::Request#response.
6
+ class HTTY::NoLocationHeaderError < StandardError
7
+
8
+ def initialize
9
+ super "response does not have a 'Location' header"
10
+ end
11
+
12
+ end
@@ -0,0 +1,12 @@
1
+ # Defines HTTY::NoResponseError.
2
+
3
+ module HTTY; end
4
+
5
+ # Indicates that HTTY::Request#response was missing.
6
+ class HTTY::NoResponseError < StandardError
7
+
8
+ def initialize
9
+ super 'request does not have a response'
10
+ end
11
+
12
+ end
@@ -0,0 +1,13 @@
1
+ # Defines HTTY::NoSetCookieHeaderError.
2
+
3
+ module HTTY; end
4
+
5
+ # Indicates that the _Set-Cookie_ header was missing from
6
+ # HTTY::Request#response.
7
+ class HTTY::NoSetCookieHeaderError < StandardError
8
+
9
+ def initialize
10
+ super "response does not have a 'Set-Cookie' header"
11
+ end
12
+
13
+ end
@@ -0,0 +1,69 @@
1
+ # Defines HTTY::OrderedHash.
2
+
3
+ module HTTY; end
4
+
5
+ # Represents a Hash that preserves the insertion order of values. This class
6
+ # exists because Hash did not have this behavior in Ruby v1.8.7 and earlier.
7
+ class HTTY::OrderedHash
8
+
9
+ def initialize(hash={})
10
+ @inner_hash = {}
11
+ hash.each_pair do |key, value|
12
+ @inner_hash[key] = value
13
+ end
14
+ @inner_keys = []
15
+ @inner_hash.each_key do |k|
16
+ @inner_keys << k
17
+ end
18
+ end
19
+
20
+ def initialize_copy(source) #:nodoc:
21
+ super
22
+ @inner_hash = @inner_hash.dup
23
+ @inner_keys = @inner_keys.dup
24
+ end
25
+
26
+ def [](key)
27
+ @inner_hash[key]
28
+ end
29
+
30
+ def []=(key, value)
31
+ @inner_keys << key unless @inner_hash.key?(key)
32
+ @inner_hash[key] = value
33
+ self
34
+ end
35
+
36
+ def ==(other_hash)
37
+ if other_hash.kind_of?(self.class)
38
+ return (other_hash.instance_variable_get('@inner_hash') == @inner_hash) &&
39
+ (other_hash.instance_variable_get('@inner_keys') == @inner_keys)
40
+ end
41
+ if other_hash.kind_of?(@inner_hash.class)
42
+ return other_hash.keys.all? do |k|
43
+ other_hash[k] == @inner_hash[k]
44
+ end
45
+ end
46
+ false
47
+ end
48
+
49
+ def clear
50
+ @inner_hash.clear
51
+ @inner_keys.clear
52
+ end
53
+
54
+ def delete(key)
55
+ @inner_keys.delete(key) if @inner_hash.key?(key)
56
+ @inner_hash.delete key
57
+ end
58
+
59
+ def empty?
60
+ @inner_hash.empty?
61
+ end
62
+
63
+ def to_a
64
+ @inner_keys.inject([]) do |result, key|
65
+ result + [[key, @inner_hash[key]]]
66
+ end
67
+ end
68
+
69
+ end
@@ -0,0 +1,48 @@
1
+ # Defines HTTY::Payload.
2
+
3
+ require File.expand_path("#{File.dirname __FILE__}/ordered_hash")
4
+
5
+ module HTTY; end
6
+
7
+ # Encapsulates the headers and body of an HTTP(S) request or response.
8
+ class HTTY::Payload
9
+
10
+ # Returns the body of the payload.
11
+ attr_reader :body
12
+
13
+ # Returns +true+ if _other_payload_ is equivalent to the payload.
14
+ def ==(other_payload)
15
+ return false unless other_payload.kind_of?(HTTY::Payload)
16
+ (other_payload.body == body) && (other_payload.headers == headers)
17
+ end
18
+ alias :eql? :==
19
+
20
+ # Returns an array of the headers belonging to the payload.
21
+ def headers
22
+ @headers.to_a
23
+ end
24
+
25
+ protected
26
+
27
+ # Initializes a new HTTY::Payload with attribute values specified in the
28
+ # _attributes_ hash.
29
+ #
30
+ # Valid _attributes_ keys include:
31
+ #
32
+ # * <tt>:body</tt>
33
+ # * <tt>:headers</tt>
34
+ def initialize(attributes={})
35
+ @body = attributes[:body]
36
+ @headers = HTTY::OrderedHash.new
37
+ Array(attributes[:headers]).each do |name, value|
38
+ @headers[name] = value
39
+ end
40
+ end
41
+
42
+ def initialize_copy(source) #:nodoc:
43
+ super
44
+ @body = @body.dup if @body
45
+ @headers = @headers.dup
46
+ end
47
+
48
+ end
@@ -0,0 +1,471 @@
1
+ # Defines HTTY::Request.
2
+
3
+ require 'pathname'
4
+ require 'uri'
5
+ require File.expand_path("#{File.dirname __FILE__}/../htty")
6
+ require File.expand_path("#{File.dirname __FILE__}/cookies_util")
7
+ require File.expand_path("#{File.dirname __FILE__}/no_location_header_error")
8
+ require File.expand_path("#{File.dirname __FILE__}/no_response_error")
9
+ require File.expand_path("#{File.dirname __FILE__}/no_set_cookie_header_error")
10
+ require File.expand_path("#{File.dirname __FILE__}/payload")
11
+ require File.expand_path("#{File.dirname __FILE__}/requests_util")
12
+ require File.expand_path("#{File.dirname __FILE__}/response")
13
+
14
+ module HTTY; end
15
+
16
+ # Encapsulates an HTTP(S) request.
17
+ class HTTY::Request < HTTY::Payload
18
+
19
+ COOKIES_HEADER_NAME = 'Cookie'
20
+
21
+ METHODS_SENDING_BODY = [:post, :put]
22
+
23
+ # Returns a URI authority (a combination of userinfo, host, and port)
24
+ # corresponding to the specified _components_ hash. Valid _components_ keys
25
+ # include:
26
+ #
27
+ # * <tt>:userinfo</tt>
28
+ # * <tt>:host</tt>
29
+ # * <tt>:port</tt>
30
+ def self.build_authority(components)
31
+ userinfo_and_host = [components[:userinfo],
32
+ components[:host]].compact.join('@')
33
+ all = [userinfo_and_host, components[:port]].compact.join(':')
34
+ return nil if (all == '')
35
+ all
36
+ end
37
+
38
+ # Returns a combination of a URI path, query, and fragment, corresponding to
39
+ # the specified _components_ hash. Valid _components_ keys include:
40
+ #
41
+ # * <tt>:path</tt>
42
+ # * <tt>:query</tt>
43
+ # * <tt>:fragment</tt>
44
+ def self.build_path_query_and_fragment(components)
45
+ path = components[:path]
46
+ query = components[:query] ? "?#{components[:query]}" : nil
47
+ fragment = components[:fragment] ? "##{components[:fragment]}" : nil
48
+ all = [path, query, fragment].compact.join
49
+ return nil if (all == '')
50
+ all
51
+ end
52
+
53
+ # Returns a URI corresponding to the specified _components_ hash, or raises
54
+ # URI::InvalidURIError. Valid _components_ keys include:
55
+ #
56
+ # * <tt>:scheme</tt>
57
+ # * <tt>:userinfo</tt>
58
+ # * <tt>:host</tt>
59
+ # * <tt>:port</tt>
60
+ # * <tt>:path</tt>
61
+ # * <tt>:query</tt>
62
+ # * <tt>:fragment</tt>
63
+ def self.build_uri(components)
64
+ scheme = (components[:scheme] || 'http') + '://'
65
+ authority = build_authority(components)
66
+ path_query_and_fragment = build_path_query_and_fragment(components)
67
+ path_query_and_fragment ||= '/' if authority
68
+ unless scheme == 'http://'
69
+ raise ArgumentError, "#{scheme.inspect} is not yet supported"
70
+ end
71
+ URI.parse([scheme, authority, path_query_and_fragment].join)
72
+ end
73
+
74
+ # Returns a URI corresponding to the specified _address_, or raises
75
+ # URI::InvalidURIError.
76
+ def self.parse_uri(address)
77
+ address = '0.0.0.0' if address.nil? || (address == '')
78
+
79
+ scheme_missing = false
80
+ if (address !~ /^[a-z]+:\/\//) && (address !~ /^mailto:/)
81
+ scheme_missing = true
82
+ address = 'http://' + address
83
+ end
84
+
85
+ scheme,
86
+ userinfo,
87
+ host,
88
+ port,
89
+ registry, # Not used by HTTP
90
+ path,
91
+ opaque, # Not used by HTTP
92
+ query,
93
+ fragment = URI.split(address)
94
+
95
+ scheme = nil if scheme_missing
96
+ path = nil if (path == '')
97
+
98
+ unless scheme
99
+ scheme = (port.to_i == URI::HTTPS::DEFAULT_PORT) ? 'https' : 'http'
100
+ end
101
+
102
+ build_uri :scheme => scheme,
103
+ :userinfo => userinfo,
104
+ :host => host,
105
+ :port => port,
106
+ :path => path,
107
+ :query => query,
108
+ :fragment => fragment
109
+ end
110
+
111
+ protected
112
+
113
+ def self.clear_cookies_if_host_changes(request)
114
+ previous_host = request.uri.host
115
+ yield
116
+ request.cookies_remove_all unless request.uri.host == previous_host
117
+ request
118
+ end
119
+
120
+ public
121
+
122
+ # Returns the HTTP method of the request, if any.
123
+ attr_reader :request_method
124
+
125
+ # Returns the response received for the request, if any.
126
+ attr_reader :response
127
+
128
+ # Returns the URI of the request.
129
+ attr_reader :uri
130
+
131
+ # Initializes a new HTTY::Request with a #uri corresponding to the specified
132
+ # _address_.
133
+ def initialize(address)
134
+ super({:headers => [['User-Agent', "htty/#{HTTY::VERSION}"]]})
135
+ @uri = self.class.parse_uri(address)
136
+ establish_content_length
137
+ end
138
+
139
+ def initialize_copy(source) #:nodoc:
140
+ super
141
+ @response = @response.dup if @response
142
+ @uri = @uri.dup
143
+ end
144
+
145
+ # Returns +true+ if _other_request_ is equivalent to the request.
146
+ def ==(other_request)
147
+ return false unless super(other_request)
148
+ return false unless other_request.kind_of?(self.class)
149
+ (other_request.response == response) && (other_request.uri == uri)
150
+ end
151
+ alias :eql? :==
152
+
153
+ # Establishes a new #uri corresponding to the specified _address_. If the host
154
+ # of the _address_ is different from the host of #uri, then #cookies are
155
+ # cleared.
156
+ def address(address)
157
+ uri = self.class.parse_uri(address)
158
+ if response
159
+ dup = dup_without_response
160
+ return self.class.clear_cookies_if_host_changes(dup) do
161
+ dup.uri = uri
162
+ end
163
+ end
164
+
165
+ self.class.clear_cookies_if_host_changes self do
166
+ @uri = uri
167
+ end
168
+ end
169
+
170
+ # Sets the body of the request.
171
+ def body_set(body)
172
+ return dup_without_response.body_set(body) if response
173
+
174
+ @body = body ? body.to_s : nil
175
+ establish_content_length
176
+ end
177
+
178
+ # Clears the body of the request.
179
+ def body_unset
180
+ body_set nil
181
+ end
182
+
183
+ # Makes an HTTP +CONNECT+ request using the path of #uri.
184
+ def connect!
185
+ request! :connect
186
+ end
187
+
188
+ # Appends to #cookies using the specified _name_ (required) and _value_
189
+ # (optional).
190
+ def cookie_add(name, value=nil)
191
+ return dup_without_response.cookie_add(name, value) if response
192
+
193
+ cookies_string = HTTY::CookiesUtil.cookies_to_string(cookies +
194
+ [[name.to_s, value]])
195
+ if cookies_string
196
+ @headers[COOKIES_HEADER_NAME] = cookies_string
197
+ else
198
+ @headers.delete COOKIES_HEADER_NAME
199
+ end
200
+ self
201
+ end
202
+
203
+ # Removes the last element of #cookies having the specified _name_.
204
+ def cookie_remove(name)
205
+ return dup_without_response.cookie_remove(name) if response
206
+
207
+ # Remove just one matching cookie from the end.
208
+ rejected = false
209
+ new_cookies = cookies.reverse.reject do |cookie_name, cookie_value|
210
+ if !rejected && (cookie_name == name)
211
+ rejected = true
212
+ else
213
+ false
214
+ end
215
+ end.reverse
216
+
217
+ cookies_string = HTTY::CookiesUtil.cookies_to_string(new_cookies)
218
+ if cookies_string
219
+ @headers[COOKIES_HEADER_NAME] = cookies_string
220
+ else
221
+ @headers.delete COOKIES_HEADER_NAME
222
+ end
223
+ self
224
+ end
225
+
226
+ # Returns an array of the cookies belonging to the request.
227
+ def cookies
228
+ HTTY::CookiesUtil.cookies_from_string @headers[COOKIES_HEADER_NAME]
229
+ end
230
+
231
+ # Removes all #cookies.
232
+ def cookies_remove_all
233
+ return dup_without_response.cookies_remove_all if response
234
+
235
+ @headers.delete COOKIES_HEADER_NAME
236
+ self
237
+ end
238
+
239
+ # Sets #cookies according to the _Set-Cookie_ header of the specified
240
+ # _response_, or raises either HTTY::NoResponseError or
241
+ # HTTY::NoSetCookieHeaderError.
242
+ def cookies_use(response)
243
+ raise HTTY::NoResponseError unless response
244
+
245
+ cookies_header = response.headers.detect do |name, value|
246
+ name == HTTY::Response::COOKIES_HEADER_NAME
247
+ end
248
+ unless cookies_header && cookies_header.last
249
+ raise HTTY::NoSetCookieHeaderError
250
+ end
251
+ header_set COOKIES_HEADER_NAME, cookies_header.last
252
+ end
253
+
254
+ # Makes an HTTP +DELETE+ request using the path of #uri.
255
+ def delete!
256
+ request! :delete
257
+ end
258
+
259
+ # Establishes a new #uri according to the _Location_ header of the specified
260
+ # _response_, or raises either HTTY::NoResponseError or
261
+ # HTTY::NoLocationHeaderError.
262
+ def follow(response)
263
+ raise HTTY::NoResponseError unless response
264
+
265
+ location_header = response.headers.detect do |name, value|
266
+ name == 'Location'
267
+ end
268
+ unless location_header && location_header.last
269
+ raise HTTY::NoLocationHeaderError
270
+ end
271
+ address location_header.last
272
+ end
273
+
274
+ # Establishes a new #uri with the specified _fragment_.
275
+ def fragment_set(fragment)
276
+ rebuild_uri :fragment => fragment
277
+ end
278
+
279
+ # Establishes a new #uri without a fragment.
280
+ def fragment_unset
281
+ fragment_set nil
282
+ end
283
+
284
+ # Makes an HTTP +GET+ request using the path of #uri.
285
+ def get!
286
+ request! :get
287
+ end
288
+
289
+ # Makes an HTTP +HEAD+ request using the path of #uri.
290
+ def head!
291
+ request! :head
292
+ end
293
+
294
+ # Appends to #headers or changes the element of #headers using the specified
295
+ # _name_ and _value_.
296
+ def header_set(name, value)
297
+ return dup_without_response.header_set(name, value) if response
298
+
299
+ name = name.to_s
300
+ if value.nil?
301
+ @headers.delete name
302
+ return self
303
+ end
304
+
305
+ @headers[name] = value.to_s
306
+ self
307
+ end
308
+
309
+ # Removes the element of #headers having the specified _name_.
310
+ def header_unset(name)
311
+ header_set name, nil
312
+ end
313
+
314
+ # Returns an array of the headers belonging to the payload. If
315
+ # _include_content_length_ is +false+, then a 'Content Length' header will be
316
+ # omitted. If _include_content_length_ is not specified, then it will be
317
+ # +true+ if #request_method is an HTTP method for which body content is
318
+ # expected.
319
+ def headers(include_content_length=
320
+ METHODS_SENDING_BODY.include?(request_method))
321
+ unless include_content_length
322
+ return super().reject do |name, value|
323
+ name == 'Content-Length'
324
+ end
325
+ end
326
+
327
+ super()
328
+ end
329
+
330
+ # Removes all #headers.
331
+ def headers_unset_all
332
+ return dup_without_response.headers_unset_all if response
333
+
334
+ @headers.clear
335
+ self
336
+ end
337
+
338
+ # Establishes a new #uri with the specified _host_.
339
+ def host_set(host)
340
+ rebuild_uri :host => host
341
+ end
342
+
343
+ # Makes an HTTP +OPTIONS+ request using the path of #uri.
344
+ def options!
345
+ request! :options
346
+ end
347
+
348
+ # Makes an HTTP +PATCH+ request using the path of #uri.
349
+ def patch!
350
+ request! :patch
351
+ end
352
+
353
+ # Establishes a new #uri with the specified _path_ which may be absolute or
354
+ # relative.
355
+ def path_set(path)
356
+ absolute_path = (Pathname.new(uri.path) + path).to_s
357
+ rebuild_uri :path => absolute_path
358
+ end
359
+
360
+ # Establishes a new #uri with the specified _port_.
361
+ def port_set(port)
362
+ rebuild_uri :port => port
363
+ end
364
+
365
+ # Makes an HTTP +POST+ request using the path of #uri.
366
+ def post!
367
+ request! :post
368
+ end
369
+
370
+ # Makes an HTTP +PUT+ request using the path of #uri.
371
+ def put!
372
+ request! :put
373
+ end
374
+
375
+ # Establishes a new #uri, with the specified _value_ for the query-string
376
+ # parameter specified by _name_.
377
+ def query_set(name, value)
378
+ query = uri.query ? "&#{uri.query}&" : ''
379
+ parameter = Regexp.new("&#{Regexp.escape name}=.+?&")
380
+ if query =~ parameter
381
+ new_query = value.nil? ?
382
+ query.gsub(parameter, '&') :
383
+ query.gsub(parameter, "&#{name}=#{value}&")
384
+ else
385
+ new_query = value.nil? ? query : "#{query}#{name}=#{value}"
386
+ end
387
+ new_query = new_query.gsub(/^&/, '').gsub(/&$/, '')
388
+ new_query = nil if (new_query == '')
389
+ rebuild_uri :query => new_query
390
+ end
391
+
392
+ # Establishes a new #uri, without the query-string parameter specified by
393
+ # _name_.
394
+ def query_unset(name)
395
+ query_set name, nil
396
+ end
397
+
398
+ # Establishes a new #uri without a query string.
399
+ def query_unset_all
400
+ rebuild_uri :query => nil
401
+ end
402
+
403
+ # Establishes a new #uri with the specified _scheme_.
404
+ def scheme_set(scheme)
405
+ rebuild_uri :scheme => scheme
406
+ end
407
+
408
+ # Makes an HTTP +TRACE+ request using the path of #uri.
409
+ def trace!
410
+ request! :trace
411
+ end
412
+
413
+ # Establishes a new #uri with the specified _userinfo_.
414
+ def userinfo_set(userinfo)
415
+ rebuild_uri :userinfo => userinfo
416
+ end
417
+
418
+ # Establishes a new #uri without userinfo.
419
+ def userinfo_unset
420
+ userinfo_set nil
421
+ end
422
+
423
+ protected
424
+
425
+ def dup_without_response
426
+ request = self.dup
427
+ request.response = nil
428
+ request.instance_variable_set '@request_method', nil
429
+ request
430
+ end
431
+
432
+ def establish_content_length
433
+ header_set 'Content-Length', body.to_s.length
434
+ end
435
+
436
+ def path_query_and_fragment
437
+ self.class.build_path_query_and_fragment :path => uri.path,
438
+ :query => uri.query,
439
+ :fragment => uri.fragment
440
+ end
441
+
442
+ def rebuild_uri(changed_components)
443
+ return dup_without_response.rebuild_uri(changed_components) if response
444
+
445
+ components = URI::HTTP::COMPONENT.inject({}) do |result, c|
446
+ result.merge c => uri.send(c)
447
+ end
448
+ self.class.clear_cookies_if_host_changes self do
449
+ @uri = self.class.build_uri(components.merge(changed_components))
450
+ end
451
+ end
452
+
453
+ attr_writer :response
454
+
455
+ attr_writer :uri
456
+
457
+ private
458
+
459
+ def authority
460
+ self.class.build_authority :userinfo => uri.userinfo,
461
+ :host => uri.host,
462
+ :port => uri.port
463
+ end
464
+
465
+ def request!(method)
466
+ request = response ? dup_without_response : self
467
+ request.instance_variable_set '@request_method', method
468
+ HTTY::RequestsUtil.send method, request
469
+ end
470
+
471
+ end