rest-man 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 (78) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/multi-matrix-test.yml +35 -0
  3. data/.github/workflows/single-matrix-test.yml +27 -0
  4. data/.gitignore +13 -0
  5. data/.mailmap +10 -0
  6. data/.rspec +2 -0
  7. data/.rubocop +2 -0
  8. data/.rubocop-disables.yml +386 -0
  9. data/.rubocop.yml +8 -0
  10. data/AUTHORS +106 -0
  11. data/CHANGELOG.md +7 -0
  12. data/Gemfile +11 -0
  13. data/LICENSE +21 -0
  14. data/README.md +843 -0
  15. data/Rakefile +140 -0
  16. data/exe/restman +92 -0
  17. data/lib/rest-man.rb +2 -0
  18. data/lib/rest_man.rb +2 -0
  19. data/lib/restman/abstract_response.rb +252 -0
  20. data/lib/restman/exceptions.rb +238 -0
  21. data/lib/restman/params_array.rb +72 -0
  22. data/lib/restman/payload.rb +234 -0
  23. data/lib/restman/platform.rb +49 -0
  24. data/lib/restman/raw_response.rb +49 -0
  25. data/lib/restman/request.rb +859 -0
  26. data/lib/restman/resource.rb +178 -0
  27. data/lib/restman/response.rb +90 -0
  28. data/lib/restman/utils.rb +274 -0
  29. data/lib/restman/version.rb +8 -0
  30. data/lib/restman/windows/root_certs.rb +105 -0
  31. data/lib/restman/windows.rb +8 -0
  32. data/lib/restman.rb +183 -0
  33. data/matrixeval.yml +73 -0
  34. data/rest-man.gemspec +41 -0
  35. data/spec/ISS.jpg +0 -0
  36. data/spec/cassettes/request_httpbin_with_basic_auth.yml +83 -0
  37. data/spec/cassettes/request_httpbin_with_cookies.yml +49 -0
  38. data/spec/cassettes/request_httpbin_with_cookies_2.yml +94 -0
  39. data/spec/cassettes/request_httpbin_with_cookies_3.yml +49 -0
  40. data/spec/cassettes/request_httpbin_with_encoding_deflate.yml +45 -0
  41. data/spec/cassettes/request_httpbin_with_encoding_deflate_and_accept_headers.yml +44 -0
  42. data/spec/cassettes/request_httpbin_with_encoding_gzip.yml +45 -0
  43. data/spec/cassettes/request_httpbin_with_encoding_gzip_and_accept_headers.yml +44 -0
  44. data/spec/cassettes/request_httpbin_with_user_agent.yml +44 -0
  45. data/spec/cassettes/request_mozilla_org.yml +151 -0
  46. data/spec/cassettes/request_mozilla_org_callback_returns_true.yml +178 -0
  47. data/spec/cassettes/request_mozilla_org_with_system_cert.yml +152 -0
  48. data/spec/cassettes/request_mozilla_org_with_system_cert_and_callback.yml +151 -0
  49. data/spec/helpers.rb +54 -0
  50. data/spec/integration/_lib.rb +1 -0
  51. data/spec/integration/capath_digicert/README +8 -0
  52. data/spec/integration/capath_digicert/ce5e74ef.0 +1 -0
  53. data/spec/integration/capath_digicert/digicert.crt +20 -0
  54. data/spec/integration/capath_digicert/update +1 -0
  55. data/spec/integration/capath_verisign/415660c1.0 +14 -0
  56. data/spec/integration/capath_verisign/7651b327.0 +14 -0
  57. data/spec/integration/capath_verisign/README +8 -0
  58. data/spec/integration/capath_verisign/verisign.crt +14 -0
  59. data/spec/integration/certs/digicert.crt +20 -0
  60. data/spec/integration/certs/verisign.crt +14 -0
  61. data/spec/integration/httpbin_spec.rb +137 -0
  62. data/spec/integration/integration_spec.rb +118 -0
  63. data/spec/integration/request_spec.rb +134 -0
  64. data/spec/spec_helper.rb +40 -0
  65. data/spec/unit/_lib.rb +1 -0
  66. data/spec/unit/abstract_response_spec.rb +145 -0
  67. data/spec/unit/exceptions_spec.rb +108 -0
  68. data/spec/unit/params_array_spec.rb +36 -0
  69. data/spec/unit/payload_spec.rb +295 -0
  70. data/spec/unit/raw_response_spec.rb +22 -0
  71. data/spec/unit/request2_spec.rb +54 -0
  72. data/spec/unit/request_spec.rb +1205 -0
  73. data/spec/unit/resource_spec.rb +134 -0
  74. data/spec/unit/response_spec.rb +252 -0
  75. data/spec/unit/restclient_spec.rb +80 -0
  76. data/spec/unit/utils_spec.rb +147 -0
  77. data/spec/unit/windows/root_certs_spec.rb +22 -0
  78. metadata +336 -0
data/Rakefile ADDED
@@ -0,0 +1,140 @@
1
+ # load `rake build/install/release tasks'
2
+ require 'bundler/setup'
3
+ require_relative './lib/restman/version'
4
+
5
+ namespace :ruby do
6
+ Bundler::GemHelper.install_tasks(:name => 'rest-man')
7
+ end
8
+
9
+ require "rspec/core/rake_task"
10
+
11
+ desc "Run all specs"
12
+ RSpec::Core::RakeTask.new('spec')
13
+
14
+ desc "Run unit specs"
15
+ RSpec::Core::RakeTask.new('spec:unit') do |t|
16
+ t.pattern = 'spec/unit/*_spec.rb'
17
+ end
18
+
19
+ desc "Run integration specs"
20
+ RSpec::Core::RakeTask.new('spec:integration') do |t|
21
+ t.pattern = 'spec/integration/*_spec.rb'
22
+ end
23
+
24
+ desc "Print specdocs"
25
+ RSpec::Core::RakeTask.new(:doc) do |t|
26
+ t.rspec_opts = ["--format", "specdoc", "--dry-run"]
27
+ t.pattern = 'spec/**/*_spec.rb'
28
+ end
29
+
30
+ desc "Run all examples with RCov"
31
+ RSpec::Core::RakeTask.new('rcov') do |t|
32
+ t.pattern = 'spec/*_spec.rb'
33
+ t.rcov = true
34
+ t.rcov_opts = ['--exclude', 'examples']
35
+ end
36
+
37
+ desc 'Regenerate authors file'
38
+ task :authors do
39
+ Dir.chdir(File.dirname(__FILE__)) do
40
+ File.open('AUTHORS', 'w') do |f|
41
+ f.write <<-EOM
42
+ The Ruby REST Client would not be what it is today without the help of
43
+ the following kind souls:
44
+
45
+ EOM
46
+ end
47
+
48
+ sh 'git shortlog -s | cut -f 2 >> AUTHORS'
49
+ end
50
+ end
51
+
52
+ task :default do
53
+ sh 'rake -T'
54
+ end
55
+
56
+ def alias_task(alias_task, original)
57
+ desc "Alias for rake #{original}"
58
+ task alias_task, Rake.application[original].arg_names => original
59
+ end
60
+ alias_task(:test, :spec)
61
+
62
+ ############################
63
+
64
+ WindowsPlatforms = %w{x86-mingw32 x64-mingw32 x86-mswin32}
65
+
66
+ namespace :all do
67
+
68
+ desc "Build rest-man #{RestMan::VERSION} for all platforms"
69
+ task :build => ['ruby:build'] + \
70
+ WindowsPlatforms.map {|p| "windows:#{p}:build"}
71
+
72
+ desc "Create tag v#{RestMan::VERSION} and for all platforms build and " \
73
+ "push rest-man #{RestMan::VERSION} to Rubygems"
74
+ task :release => ['build', 'ruby:release'] + \
75
+ WindowsPlatforms.map {|p| "windows:#{p}:push"}
76
+
77
+ end
78
+
79
+ namespace :windows do
80
+ spec_path = File.join(File.dirname(__FILE__), 'rest-man.windows.gemspec')
81
+
82
+ WindowsPlatforms.each do |platform|
83
+ namespace platform do
84
+ gem_filename = "rest-man-#{RestMan::VERSION}-#{platform}.gem"
85
+ base = File.dirname(__FILE__)
86
+ pkg_dir = File.join(base, 'pkg')
87
+ gem_file_path = File.join(pkg_dir, gem_filename)
88
+
89
+ desc "Build #{gem_filename} into the pkg directory"
90
+ task 'build' do
91
+ orig_platform = ENV['BUILD_PLATFORM']
92
+ begin
93
+ ENV['BUILD_PLATFORM'] = platform
94
+
95
+ sh("gem build -V #{spec_path}") do |ok, res|
96
+ if ok
97
+ FileUtils.mkdir_p(pkg_dir)
98
+ FileUtils.mv(File.join(base, gem_filename), pkg_dir)
99
+ Bundler.ui.confirm("rest-man #{RestMan::VERSION} " \
100
+ "built to pkg/#{gem_filename}")
101
+ else
102
+ abort "Command `gem build` failed: #{res}"
103
+ end
104
+ end
105
+
106
+ ensure
107
+ ENV['BUILD_PLATFORM'] = orig_platform
108
+ end
109
+ end
110
+
111
+ desc "Push #{gem_filename} to Rubygems"
112
+ task 'push' do
113
+ sh("gem push #{gem_file_path}")
114
+ end
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ ############################
121
+
122
+ require 'rdoc/task'
123
+
124
+ Rake::RDocTask.new do |t|
125
+ t.rdoc_dir = 'rdoc'
126
+ t.title = "rest-man, fetch RESTful resources effortlessly"
127
+ t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
128
+ t.options << '--charset' << 'utf-8'
129
+ t.rdoc_files.include('README.md')
130
+ t.rdoc_files.include('lib/*.rb')
131
+ end
132
+
133
+ ############################
134
+
135
+ require 'rubocop/rake_task'
136
+
137
+ RuboCop::RakeTask.new(:rubocop) do |t|
138
+ t.options = ['--display-cop-names']
139
+ end
140
+ alias_task(:lint, :rubocop)
data/exe/restman ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
4
+
5
+ require 'rubygems'
6
+ require 'restman'
7
+ require 'yaml'
8
+
9
+ def usage(why = nil)
10
+ puts "failed for reason: #{why}" if why
11
+ puts "usage: restman [get|put|post|delete] url|name [username] [password]"
12
+ puts " The verb is optional, if you leave it off you'll get an interactive shell."
13
+ puts " put and post both take the input body on stdin."
14
+ exit(1)
15
+ end
16
+
17
+ POSSIBLE_VERBS = ['get', 'put', 'post', 'delete']
18
+
19
+ if POSSIBLE_VERBS.include? ARGV.first
20
+ @verb = ARGV.shift
21
+ else
22
+ @verb = nil
23
+ end
24
+
25
+ @url = ARGV.shift || 'http://localhost:4567'
26
+
27
+ config = YAML.load(File.read(ENV['HOME'] + "/.restman")) rescue {}
28
+
29
+ if (c = config[@url])
30
+ @url, @username, @password = [c['url'], c['username'], c['password']]
31
+ else
32
+ @url, @username, @password = [@url, * ARGV]
33
+ end
34
+
35
+ usage("invalid url '#{@url}") unless @url =~ /^https?/
36
+ usage("too few args") unless ARGV.size < 3
37
+
38
+ def r
39
+ @r ||= RestMan::Resource.new(@url, @username, @password)
40
+ end
41
+
42
+ r # force rc to load
43
+
44
+ if @verb
45
+ begin
46
+ if %w( put post ).include? @verb
47
+ puts r.send(@verb, STDIN.read)
48
+ else
49
+ puts r.send(@verb)
50
+ end
51
+ exit 0
52
+ rescue RestMan::Exception => e
53
+ puts e.response.body if e.respond_to?(:response) && e.response
54
+ raise
55
+ end
56
+ end
57
+
58
+ POSSIBLE_VERBS.each do |m|
59
+ define_method(m.to_sym) do |path, *args, &b|
60
+ r[path].public_send(m.to_sym, *args, &b)
61
+ end
62
+ end
63
+
64
+ def method_missing(s, * args, & b)
65
+ if POSSIBLE_VERBS.include? s
66
+ begin
67
+ r.send(s, *args, & b)
68
+ rescue RestMan::RequestFailed => e
69
+ print STDERR, e.response.body
70
+ raise e
71
+ end
72
+ else
73
+ super
74
+ end
75
+ end
76
+
77
+ require 'irb'
78
+ require 'irb/completion'
79
+
80
+ if File.exist? ".irbrc"
81
+ ENV['IRBRC'] = ".irbrc"
82
+ end
83
+
84
+ rcfile = File.expand_path("~/.restmanrc")
85
+ if File.exist?(rcfile)
86
+ load(rcfile)
87
+ end
88
+
89
+ ARGV.clear
90
+
91
+ IRB.start
92
+ exit!
data/lib/rest-man.rb ADDED
@@ -0,0 +1,2 @@
1
+ # More logical way to require 'rest-man'
2
+ require File.dirname(__FILE__) + '/restman'
data/lib/rest_man.rb ADDED
@@ -0,0 +1,2 @@
1
+ # This file exists for backward compatbility with require 'rest_man'
2
+ require File.dirname(__FILE__) + '/restman'
@@ -0,0 +1,252 @@
1
+ require 'cgi'
2
+ require 'http-cookie'
3
+
4
+ module RestMan
5
+
6
+ module AbstractResponse
7
+
8
+ attr_reader :net_http_res, :request, :start_time, :end_time, :duration
9
+
10
+ def inspect
11
+ raise NotImplementedError.new('must override in subclass')
12
+ end
13
+
14
+ # Logger from the request, potentially nil.
15
+ def log
16
+ request.log
17
+ end
18
+
19
+ def log_response
20
+ return unless log
21
+
22
+ code = net_http_res.code
23
+ res_name = net_http_res.class.to_s.gsub(/\ANet::HTTP/, '')
24
+ content_type = (net_http_res['Content-type'] || '').gsub(/;.*\z/, '')
25
+
26
+ log << "# => #{code} #{res_name} | #{content_type} #{size} bytes, #{sprintf('%.2f', duration)}s\n"
27
+ end
28
+
29
+ # HTTP status code
30
+ def code
31
+ @code ||= @net_http_res.code.to_i
32
+ end
33
+
34
+ def history
35
+ @history ||= request.redirection_history || []
36
+ end
37
+
38
+ # A hash of the headers, beautified with symbols and underscores.
39
+ # e.g. "Content-type" will become :content_type.
40
+ def headers
41
+ @headers ||= AbstractResponse.beautify_headers(@net_http_res.to_hash)
42
+ end
43
+
44
+ # The raw headers.
45
+ def raw_headers
46
+ @raw_headers ||= @net_http_res.to_hash
47
+ end
48
+
49
+ # @param [Net::HTTPResponse] net_http_res
50
+ # @param [RestMan::Request] request
51
+ # @param [Time] start_time
52
+ def response_set_vars(net_http_res, request, start_time)
53
+ @net_http_res = net_http_res
54
+ @request = request
55
+ @start_time = start_time
56
+ @end_time = Time.now
57
+
58
+ if @start_time
59
+ @duration = @end_time - @start_time
60
+ else
61
+ @duration = nil
62
+ end
63
+
64
+ # prime redirection history
65
+ history
66
+ end
67
+
68
+ # Hash of cookies extracted from response headers.
69
+ #
70
+ # NB: This will return only cookies whose domain matches this request, and
71
+ # may not even return all of those cookies if there are duplicate names.
72
+ # Use the full cookie_jar for more nuanced access.
73
+ #
74
+ # @see #cookie_jar
75
+ #
76
+ # @return [Hash]
77
+ #
78
+ def cookies
79
+ hash = {}
80
+
81
+ cookie_jar.cookies(@request.uri).each do |cookie|
82
+ hash[cookie.name] = cookie.value
83
+ end
84
+
85
+ hash
86
+ end
87
+
88
+ # Cookie jar extracted from response headers.
89
+ #
90
+ # @return [HTTP::CookieJar]
91
+ #
92
+ def cookie_jar
93
+ return @cookie_jar if defined?(@cookie_jar) && @cookie_jar
94
+
95
+ jar = @request.cookie_jar.dup
96
+ headers.fetch(:set_cookie, []).each do |cookie|
97
+ jar.parse(cookie, @request.uri)
98
+ end
99
+
100
+ @cookie_jar = jar
101
+ end
102
+
103
+ # Return the default behavior corresponding to the response code:
104
+ #
105
+ # For 20x status codes: return the response itself
106
+ #
107
+ # For 30x status codes:
108
+ # 301, 302, 307: redirect GET / HEAD if there is a Location header
109
+ # 303: redirect, changing method to GET, if there is a Location header
110
+ #
111
+ # For all other responses, raise a response exception
112
+ #
113
+ def return!(&block)
114
+ case code
115
+ when 200..207
116
+ self
117
+ when 301, 302, 307
118
+ case request.method
119
+ when 'get', 'head'
120
+ check_max_redirects
121
+ follow_redirection(&block)
122
+ else
123
+ raise exception_with_response
124
+ end
125
+ when 303
126
+ check_max_redirects
127
+ follow_get_redirection(&block)
128
+ else
129
+ raise exception_with_response
130
+ end
131
+ end
132
+
133
+ def to_i
134
+ warn('warning: calling Response#to_i is not recommended')
135
+ super
136
+ end
137
+
138
+ def description
139
+ "#{code} #{STATUSES[code]} | #{(headers[:content_type] || '').gsub(/;.*$/, '')} #{size} bytes\n"
140
+ end
141
+
142
+ # Follow a redirection response by making a new HTTP request to the
143
+ # redirection target.
144
+ def follow_redirection(&block)
145
+ _follow_redirection(request.args.dup, &block)
146
+ end
147
+
148
+ # Follow a redirection response, but change the HTTP method to GET and drop
149
+ # the payload from the original request.
150
+ def follow_get_redirection(&block)
151
+ new_args = request.args.dup
152
+ new_args[:method] = :get
153
+ new_args.delete(:payload)
154
+
155
+ _follow_redirection(new_args, &block)
156
+ end
157
+
158
+ # Convert headers hash into canonical form.
159
+ #
160
+ # Header names will be converted to lowercase symbols with underscores
161
+ # instead of hyphens.
162
+ #
163
+ # Headers specified multiple times will be joined by comma and space,
164
+ # except for Set-Cookie, which will always be an array.
165
+ #
166
+ # Per RFC 2616, if a server sends multiple headers with the same key, they
167
+ # MUST be able to be joined into a single header by a comma. However,
168
+ # Set-Cookie (RFC 6265) cannot because commas are valid within cookie
169
+ # definitions. The newer RFC 7230 notes (3.2.2) that Set-Cookie should be
170
+ # handled as a special case.
171
+ #
172
+ # http://tools.ietf.org/html/rfc2616#section-4.2
173
+ # http://tools.ietf.org/html/rfc7230#section-3.2.2
174
+ # http://tools.ietf.org/html/rfc6265
175
+ #
176
+ # @param headers [Hash]
177
+ # @return [Hash]
178
+ #
179
+ def self.beautify_headers(headers)
180
+ headers.inject({}) do |out, (key, value)|
181
+ key_sym = key.tr('-', '_').downcase.to_sym
182
+
183
+ # Handle Set-Cookie specially since it cannot be joined by comma.
184
+ if key.downcase == 'set-cookie'
185
+ out[key_sym] = value
186
+ else
187
+ out[key_sym] = value.join(', ')
188
+ end
189
+
190
+ out
191
+ end
192
+ end
193
+
194
+ private
195
+
196
+ # Follow a redirection
197
+ #
198
+ # @param new_args [Hash] Start with this hash of arguments for the
199
+ # redirection request. The hash will be mutated, so be sure to dup any
200
+ # existing hash that should not be modified.
201
+ #
202
+ def _follow_redirection(new_args, &block)
203
+
204
+ # parse location header and merge into existing URL
205
+ url = headers[:location]
206
+
207
+ # cannot follow redirection if there is no location header
208
+ unless url
209
+ raise exception_with_response
210
+ end
211
+
212
+ # handle relative redirects
213
+ unless url.start_with?('http')
214
+ url = URI.parse(request.url).merge(url).to_s
215
+ end
216
+ new_args[:url] = url
217
+
218
+ new_args[:password] = request.password
219
+ new_args[:user] = request.user
220
+ new_args[:headers] = request.headers
221
+ new_args[:max_redirects] = request.max_redirects - 1
222
+
223
+ # pass through our new cookie jar
224
+ new_args[:cookies] = cookie_jar
225
+
226
+ # prepare new request
227
+ new_req = Request.new(new_args)
228
+
229
+ # append self to redirection history
230
+ new_req.redirection_history = history + [self]
231
+
232
+ # execute redirected request
233
+ new_req.execute(&block)
234
+ end
235
+
236
+ def check_max_redirects
237
+ if request.max_redirects <= 0
238
+ raise exception_with_response
239
+ end
240
+ end
241
+
242
+ def exception_with_response
243
+ begin
244
+ klass = Exceptions::EXCEPTIONS_MAP.fetch(code)
245
+ rescue KeyError
246
+ raise RequestFailed.new(self, code)
247
+ end
248
+
249
+ raise klass.new(self, code)
250
+ end
251
+ end
252
+ end