htty 1.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.
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