restup 1.0.1
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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +173 -0
- data/bin/restup +230 -0
- data/lib/config.rb +268 -0
- data/lib/constants.rb +7 -0
- data/lib/output_builder.rb +302 -0
- data/lib/rest_up.rb +154 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 821def49d7ed7b1bfe9fece4bdccf8711439f3dd
|
4
|
+
data.tar.gz: 22a09bc07b0613224205ef9b03a499f3f05fe0e4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 053726b4acc83c478528d144d47da2b8423f2da294f1c52ed35b9ab7dd3a20f97d580962c6e9a37fb555e5a82a36b577cfd591f5600232606f8bae36f20a5721
|
7
|
+
data.tar.gz: 66ef657596c00a2255485f48847c575262702167df22b4ef7014e1c2c1d6db4b57a84b366b294403d8d4b642db2ace5f2018ea47d5a1ed32747518845e44e146
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Z. D. Peacock
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
Thoom::RestUp
|
2
|
+
=============
|
3
|
+
|
4
|
+
After many years in beta, RestClient is now RestUp version 1.
|
5
|
+
It's mainly the same client as before, but some breaking changes have been made based on
|
6
|
+
feedback and experience.
|
7
|
+
|
8
|
+
RestUp works out of the box with APIs that use Basic Authentication (though this is not required).
|
9
|
+
To use other forms of authentication, custom headers can either be passed with each request
|
10
|
+
or stored in the config file as described below.
|
11
|
+
|
12
|
+
If a YAML configuration file exists (by default `.restup.yml`), the client will pull in defaults and provide several shortcut methods that can simplify using a REST-based API.
|
13
|
+
|
14
|
+
If the API uses form encoded input, you can define your post in JSON format. The client
|
15
|
+
will encode it automatically.
|
16
|
+
|
17
|
+
Installation
|
18
|
+
------------
|
19
|
+
|
20
|
+
#### Gem
|
21
|
+
For convenience, the executable and class are available as a gem on RubyGems.
|
22
|
+
|
23
|
+
gem install restup
|
24
|
+
|
25
|
+
#### Docker
|
26
|
+
The client is also available as a Docker image.
|
27
|
+
|
28
|
+
To install:
|
29
|
+
|
30
|
+
docker pull thoom/restup
|
31
|
+
|
32
|
+
To run:
|
33
|
+
|
34
|
+
docker run --rm -v $PWD:/usr/src/restup thoom/restup
|
35
|
+
|
36
|
+
A sample shell script `restup`:
|
37
|
+
|
38
|
+
#!/bin/bash
|
39
|
+
docker run --rm -v $PWD:/usr/src/restup thoom/restup "@"
|
40
|
+
|
41
|
+
Console
|
42
|
+
-------
|
43
|
+
|
44
|
+
Usage: restclient [options] ENDPOINT
|
45
|
+
--concise Disables verbose output
|
46
|
+
--content-disposition For responses with a filename in the Content Disposition, save the response using that filename
|
47
|
+
--form Converts JSON-formatted input and encode it as x-www-form-urlencoded
|
48
|
+
--response-only Only outputs the response body
|
49
|
+
--response-code-only Only outputs the response code
|
50
|
+
--success-only Only outputs whether or not the request was successful
|
51
|
+
--cert CERTFILE Imports cert for Client-Authentication endpoints
|
52
|
+
-c, --config FILE Config file to use. Defaults to .restup.yml -e, --env ENVIRONMENT Sets YAML environment for the request
|
53
|
+
-h, --header HEADER Sets arbitrary header passed in format "HEADER: VALUE"
|
54
|
+
-j, --json [c|a] Sets the Content-Type and/or Accept Headers to use JSON mime types (i.e. -ja)
|
55
|
+
-m, --method The HTTP method to use (defaults to GET)
|
56
|
+
-o, --output FILE Save output to file passed
|
57
|
+
-p, --password PASSWORD Password for Basic Authentication
|
58
|
+
-u, --username USERNAME Username for Basic Authentication
|
59
|
+
-x, --xml [c|a] Sets the Content-Type and/or Accept Headers to use XML mime types (i.e. -xc)
|
60
|
+
--verbose Provides a nice UI for your API responses
|
61
|
+
--version Shows client version
|
62
|
+
--help [details] Shows this message
|
63
|
+
|
64
|
+
YAML config
|
65
|
+
-----------
|
66
|
+
|
67
|
+
If a file is not passed in with the `-c, --config` flag, then it will use the default `.restup.yml`.
|
68
|
+
The client uses two different methods to find the YAML configuration file.
|
69
|
+
It will first look in the current directory.
|
70
|
+
If it is not present, it will then look in the current user's home directory.
|
71
|
+
|
72
|
+
This makes it possible to use restup to connect to different APIs simply by changing
|
73
|
+
folders.
|
74
|
+
|
75
|
+
KEY DESC
|
76
|
+
---- -----
|
77
|
+
user: The username. Default: blank, so disable Basic Authentication
|
78
|
+
pass: The password. Default: blank, so disable Basic Authentication
|
79
|
+
|
80
|
+
url: The base REST url
|
81
|
+
json: The default JSON MIME type. Default: "application/json"
|
82
|
+
xml: The default XML MIME type. Default: "application/xml"
|
83
|
+
|
84
|
+
colors: Hash of default color values
|
85
|
+
success: Color to highlight successful messages. Default: :green
|
86
|
+
warning: Color to highlight warning messages. Default: :yellow
|
87
|
+
info: Color to highlight info messages. Default: :yellow
|
88
|
+
error: Color to highlight error messages. Default :red
|
89
|
+
|
90
|
+
flags: Default command line options
|
91
|
+
display: What to display by default.
|
92
|
+
Values: concise, response_only, response_code_only, succcess_only, verbose
|
93
|
+
Default: response_only
|
94
|
+
|
95
|
+
headers: Hash of default headers. Useful for custom headers or headers used in every request.
|
96
|
+
The keys for this hash are strings, not symbols like the other keys
|
97
|
+
|
98
|
+
timeout: The number of seconds to wait for a response before timing out. Default: 300
|
99
|
+
|
100
|
+
tls_verify: When using TLS, the verify mode to use. Values: true, false. Default: true
|
101
|
+
|
102
|
+
xmethods: Array of nonstandard methods that are accepted by the API. To use these methods the
|
103
|
+
API must support X-HTTP-Method-Override.
|
104
|
+
|
105
|
+
Examples
|
106
|
+
--------
|
107
|
+
|
108
|
+
### GET Request
|
109
|
+
|
110
|
+
The YAML config:
|
111
|
+
|
112
|
+
url: http://example.com/api
|
113
|
+
user: myname
|
114
|
+
pass: P@ssWord
|
115
|
+
|
116
|
+
The command line:
|
117
|
+
|
118
|
+
restup -j /hello/world
|
119
|
+
|
120
|
+
To use without the config:
|
121
|
+
|
122
|
+
restclient -u myname -p P@ssWord -j http://example.com/api/hello/world
|
123
|
+
|
124
|
+
Submits a GET request to `http://example/api/hello/world` with Basic Auth header using the
|
125
|
+
user and pass values in the config.
|
126
|
+
|
127
|
+
It would return JSON values. If successful, the JSON would be parsed and highlighted in __:colors::success:__. If
|
128
|
+
the an error was returned (an HTTP response code >= 400), the body would be in __:colors::error:__.
|
129
|
+
|
130
|
+
### POST Request
|
131
|
+
|
132
|
+
The YAML config:
|
133
|
+
|
134
|
+
url: http://example.com/api
|
135
|
+
user: myname
|
136
|
+
pass: P@ssWord
|
137
|
+
headers:
|
138
|
+
X-Custom-Id: abc12345
|
139
|
+
|
140
|
+
The command line:
|
141
|
+
|
142
|
+
restclient -m post -j /hello/world < salutation.json
|
143
|
+
|
144
|
+
OR
|
145
|
+
|
146
|
+
cat salutation.json | restclient -m post /hello/world
|
147
|
+
|
148
|
+
Submits a POST request to `http://example/api/hello/world` with Basic Auth header using the
|
149
|
+
user and pass values in the config. It imports the salutation.json and passes it to the API as application/json
|
150
|
+
content type. It would also set the `X-Custom-Id` header with every request.
|
151
|
+
|
152
|
+
It would return JSON values. If successful, the JSON would be parsed and highlighted in __:colors::success:__. If
|
153
|
+
the an error was returned (an HTTP response code >= 400), the body would be in __:colors::error:__.
|
154
|
+
|
155
|
+
|
156
|
+
Migration From RestClient
|
157
|
+
-------------------------
|
158
|
+
|
159
|
+
To migrate:
|
160
|
+
|
161
|
+
1. Rename your `.restclient.yml` file to `.restup.yml`.
|
162
|
+
2. The CLI format changed from `restclient METHOD ENDPOINT [options]` to `restup [options] ENDPOINT`.
|
163
|
+
3. The `-c` option is no longer available. You must use `--cert` instead.
|
164
|
+
4. The `-m` option was created for specifying methods. So `restup -m POST ENDPOINT` instead of `restclient POST ENDPOINT`.
|
165
|
+
5. Add `flags: { display: verbose }` to the config file return to the previous API output.
|
166
|
+
|
167
|
+
License
|
168
|
+
-------
|
169
|
+
[MIT](LICENSE)
|
170
|
+
|
171
|
+
Version History
|
172
|
+
---------------
|
173
|
+
[Changelog](CHANGELOG.md)
|
data/bin/restup
ADDED
@@ -0,0 +1,230 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(File.dirname(__FILE__))) + '/lib')
|
4
|
+
|
5
|
+
require 'constants'
|
6
|
+
require 'config'
|
7
|
+
require 'output_builder'
|
8
|
+
require 'rest_up'
|
9
|
+
|
10
|
+
require 'optparse'
|
11
|
+
require 'json'
|
12
|
+
require 'yaml'
|
13
|
+
|
14
|
+
if ARGV.empty?
|
15
|
+
Thoom::SimpleOutputBuilder.new.quit(
|
16
|
+
'Missing required options. Use "--help" OR "--help details" for more information', false
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
DEFAULT_CONFIG = '.restup.yml'.freeze
|
21
|
+
|
22
|
+
opts = {
|
23
|
+
cert: '',
|
24
|
+
config: DEFAULT_CONFIG,
|
25
|
+
content_disposition: false,
|
26
|
+
display: :default,
|
27
|
+
env: :default,
|
28
|
+
endpoint: ARGV.last,
|
29
|
+
form: false,
|
30
|
+
headers: {},
|
31
|
+
help: false,
|
32
|
+
method: 'get'
|
33
|
+
}
|
34
|
+
|
35
|
+
opts_builder = nil
|
36
|
+
|
37
|
+
parser = OptionParser.new do |o|
|
38
|
+
o.banner = 'Usage: restup [options] ENDPOINT'
|
39
|
+
o.on('--concise', 'Disables verbose output') do
|
40
|
+
opts[:display] = :concise
|
41
|
+
end
|
42
|
+
|
43
|
+
o.on('--content-disposition', 'For responses with a filename in the Content Disposition, save the response using that filename') do
|
44
|
+
opts[:content_disposition] = true
|
45
|
+
end
|
46
|
+
|
47
|
+
o.on('--form', 'Converts JSON-formatted input and encode it as x-www-form-urlencoded') do
|
48
|
+
opts[:form] = true
|
49
|
+
end
|
50
|
+
|
51
|
+
o.on('--response-only', 'Only outputs the response body') do
|
52
|
+
opts[:display] = :response_only
|
53
|
+
end
|
54
|
+
o.on('--response-code-only', 'Only outputs the response code') do
|
55
|
+
opts[:display] = :response_code_only
|
56
|
+
end
|
57
|
+
|
58
|
+
o.on('--success-only', 'Only outputs whether or not the request was successful') do
|
59
|
+
opts[:display] = :success_only
|
60
|
+
end
|
61
|
+
|
62
|
+
o.on('--cert CERTFILE', 'Imports cert for Client-Authentication endpoints') do |cert|
|
63
|
+
opts[:cert] = cert
|
64
|
+
end
|
65
|
+
|
66
|
+
o.on('-c', '--config FILE', "Config file to use. Defaults to #{DEFAULT_CONFIG}") do |config|
|
67
|
+
opts[:config] = config
|
68
|
+
end
|
69
|
+
|
70
|
+
o.on('-e', '--env ENVIRONMENT', 'Sets YAML environment for the request') do |env|
|
71
|
+
opts[:env] = env.to_sym
|
72
|
+
end
|
73
|
+
|
74
|
+
o.on('-h', '--header HEADER', 'Sets arbitrary header passed in format "HEADER: VALUE"') do |header|
|
75
|
+
key, val = header.split(':')
|
76
|
+
opts[:headers][key.downcase.strip] = val.strip
|
77
|
+
end
|
78
|
+
|
79
|
+
o.on('-j', '--json [c|a]', 'Sets the Content-Type and/or Accept Headers to use JSON mime types (i.e. -ja)') do |json|
|
80
|
+
case json
|
81
|
+
when 'c', 'content-type'
|
82
|
+
opts[:headers]['content-type'] = :json
|
83
|
+
when 'a', 'accept'
|
84
|
+
opts[:headers]['accept'] = :json
|
85
|
+
else
|
86
|
+
opts[:headers]['content-type'] = :json
|
87
|
+
opts[:headers]['accept'] = :json
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
o.on('-m', '--method METHOD', 'Save output to file passed') do |method_name|
|
92
|
+
opts[:method] = method_name
|
93
|
+
end
|
94
|
+
|
95
|
+
o.on('-o', '--output FILE', 'Save output to file passed') do |file|
|
96
|
+
opts[:output_file] = file
|
97
|
+
end
|
98
|
+
|
99
|
+
o.on('-p', '--password PASSWORD', 'Password for Basic Authentication') do |password|
|
100
|
+
opts[:pass] = password
|
101
|
+
end
|
102
|
+
|
103
|
+
o.on('-u', '--username USERNAME', 'Username for Basic Authentication') do |username|
|
104
|
+
opts[:user] = username
|
105
|
+
end
|
106
|
+
|
107
|
+
o.on('-x', '--xml [c|a]', 'Sets the Content-Type and/or Accept Headers to use XML mime types (i.e. -xc)') do |xml|
|
108
|
+
case xml
|
109
|
+
when 'c', 'content-type'
|
110
|
+
opts[:headers]['content-type'] = :xml
|
111
|
+
when 'a', 'accept'
|
112
|
+
opts[:headers]['accept'] = :xml
|
113
|
+
else
|
114
|
+
opts[:headers]['content-type'] = :xml
|
115
|
+
opts[:headers]['accept'] = :xml
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
o.on('--verbose', 'Provides a nice UI for your API responses') do
|
120
|
+
opts[:display] = :verbose
|
121
|
+
end
|
122
|
+
|
123
|
+
o.on_tail('--version', 'Shows client version') do
|
124
|
+
Thoom::SimpleOutputBuilder.new.quit('', false)
|
125
|
+
end
|
126
|
+
|
127
|
+
o.on_tail('--help [details]', 'Shows this message') do |details|
|
128
|
+
opts_builder = o
|
129
|
+
opts[:help] = details == 'details' ? :details : :simple
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
begin
|
134
|
+
parser.parse! ARGV
|
135
|
+
ARGV.clear
|
136
|
+
|
137
|
+
begin
|
138
|
+
config = Thoom::YamlConfig.new opts[:config], opts[:env]
|
139
|
+
rescue Thoom::ConfigFileError
|
140
|
+
config = Thoom::HashConfig.new
|
141
|
+
end
|
142
|
+
|
143
|
+
output_builder = Thoom::DefaultOutputBuilder.new
|
144
|
+
new_colors = config.get(:colors, yolo: :cyan)
|
145
|
+
|
146
|
+
if new_colors.nil? || new_colors.empty?
|
147
|
+
output_builder.quit(Paint['Empty color: hash found in YAML configuration', output_builder.colors[:error]])
|
148
|
+
end
|
149
|
+
|
150
|
+
output_builder.colors.merge!(new_colors)
|
151
|
+
|
152
|
+
if opts[:help] == :details
|
153
|
+
output_builder.help(DEFAULT_CONFIG, opts_builder)
|
154
|
+
elsif opts[:help] == :simple
|
155
|
+
output_builder.quit(opts_builder)
|
156
|
+
end
|
157
|
+
|
158
|
+
if opts[:display] == :default
|
159
|
+
display = config.get(:flags, {})[:display]
|
160
|
+
opts[:display] = display.nil? ? :response_only : display.to_sym
|
161
|
+
end
|
162
|
+
|
163
|
+
config.set(:user, opts[:user]) if opts.key? :user
|
164
|
+
config.set(:pass, opts[:pass]) if opts.key? :pass
|
165
|
+
|
166
|
+
client = Thoom::RestUp.new config
|
167
|
+
client.method = opts[:method]
|
168
|
+
client.endpoint = opts[:endpoint]
|
169
|
+
client.cert = File.read(opts[:cert]) unless opts[:cert].empty?
|
170
|
+
|
171
|
+
opts[:headers].each do |key, val|
|
172
|
+
if %w(content-type accept).include? key
|
173
|
+
val = config.get(:json, Thoom::Constants::MIME_JSON) if val == :json
|
174
|
+
val = config.get(:xml, Thoom::Constants::MIME_XML) if val == :xml
|
175
|
+
end
|
176
|
+
client.headers[key] = val
|
177
|
+
end
|
178
|
+
|
179
|
+
if ARGF.filename != '-' || (!STDIN.tty? && !STDIN.closed?)
|
180
|
+
data = ARGF.read
|
181
|
+
|
182
|
+
if !client.headers.key?('content-type') || client.headers['content-type'].include?('json')
|
183
|
+
data = YAML.safe_load(data).to_json
|
184
|
+
end
|
185
|
+
|
186
|
+
if opts[:form]
|
187
|
+
client.headers['content-type'] = 'x-www-form-urlencoded'
|
188
|
+
yaml = YAML.safe_load(data)
|
189
|
+
data = URI.encode_www_form(yaml)
|
190
|
+
end
|
191
|
+
|
192
|
+
client.data = data
|
193
|
+
end
|
194
|
+
|
195
|
+
request = client.request
|
196
|
+
if %i(concise verbose).include? opts[:display]
|
197
|
+
output_builder.request(client, request, opts[:display] == :verbose)
|
198
|
+
end
|
199
|
+
|
200
|
+
# This just sets a default to JSON
|
201
|
+
if %w(post put patch).include?(opts[:method].downcase) && (request.content_type.nil? || request.content_type.empty?)
|
202
|
+
request.content_type = config.get(:json, Thoom::Constants::MIME_JSON)
|
203
|
+
end
|
204
|
+
|
205
|
+
before = Time.now
|
206
|
+
response = client.submit request
|
207
|
+
output_builder.response_time = (Time.now - before).round(2)
|
208
|
+
|
209
|
+
output_builder.quit(response) unless response.respond_to? :each_header
|
210
|
+
|
211
|
+
case opts[:display]
|
212
|
+
when :response_code_only
|
213
|
+
puts response.code
|
214
|
+
when :success_only
|
215
|
+
puts response.code.to_i < 400
|
216
|
+
when :response_only
|
217
|
+
puts response.body
|
218
|
+
else
|
219
|
+
output_builder.response(response, opts[:display] == :verbose)
|
220
|
+
output_builder.save_response(response, opts[:content_disposition], opts[:output_file])
|
221
|
+
puts "\n"
|
222
|
+
end
|
223
|
+
rescue Timeout::Error
|
224
|
+
output_builder.quit Paint['Request timed out', output_builder.colors[:error]]
|
225
|
+
rescue SystemExit
|
226
|
+
puts "\n"
|
227
|
+
rescue StandardError => e
|
228
|
+
output_builder = output_builder.nil? ? Thoom::DefaultOutputBuilder.new : output_builder
|
229
|
+
output_builder.quit "#{Paint[e.message.capitalize, output_builder.colors[:error]]}\n\n"
|
230
|
+
end
|
data/lib/config.rb
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
module Thoom
|
2
|
+
class ConfigError < RuntimeError
|
3
|
+
attr_reader :message
|
4
|
+
|
5
|
+
def initialize(message)
|
6
|
+
@message = message
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class ConfigFileError < ConfigError
|
11
|
+
end
|
12
|
+
|
13
|
+
module Config
|
14
|
+
def config_set(config)
|
15
|
+
@config = config.deep_symbolize_keys
|
16
|
+
end
|
17
|
+
|
18
|
+
def get(key, default_val = nil)
|
19
|
+
key = key.to_sym
|
20
|
+
if @config.key?(@env) && @config[@env].key?(key)
|
21
|
+
@config[@env][key]
|
22
|
+
elsif @config.key?(:default) && @config[:default].key?(key)
|
23
|
+
@config[:default][key]
|
24
|
+
elsif @config.key? key
|
25
|
+
@config[key]
|
26
|
+
elsif !default_val.nil?
|
27
|
+
default_val
|
28
|
+
else
|
29
|
+
raise ConfigError, "Missing required configuration entry for #{key}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def env=(val)
|
34
|
+
@env = val.to_sym
|
35
|
+
end
|
36
|
+
|
37
|
+
def set(key, val, env = :default)
|
38
|
+
env = env.to_sym
|
39
|
+
key = key.to_sym
|
40
|
+
|
41
|
+
@config[env] = {} unless @config.key? env
|
42
|
+
@config[env][key] = val
|
43
|
+
end
|
44
|
+
|
45
|
+
def print
|
46
|
+
@config.to_s
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class HashConfig
|
51
|
+
include Config
|
52
|
+
|
53
|
+
def initialize(hash = {}, env = :default)
|
54
|
+
@env = env
|
55
|
+
config_set(hash)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class YamlConfig
|
60
|
+
require 'yaml'
|
61
|
+
|
62
|
+
include Config
|
63
|
+
|
64
|
+
def initialize(filename, env = :default)
|
65
|
+
file = File.exist?(filename) ? filename : File.expand_path("~/#{filename}")
|
66
|
+
raise ConfigFileError, "Configuration file #{filename} not found" unless File.exist? file
|
67
|
+
|
68
|
+
yaml = YAML.load_file file
|
69
|
+
raise ConfigFileError, "Configuration file #{file} is empty!" unless yaml
|
70
|
+
|
71
|
+
@env = env
|
72
|
+
config_set(yaml)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# pulled from https://raw.githubusercontent.com/rails/rails/f1bad130d0c9bd77c94e43b696adca56c46a66aa/activesupport/lib/active_support/core_ext/hash/keys.rb
|
78
|
+
class Hash
|
79
|
+
# Returns a new hash with all keys converted using the block operation.
|
80
|
+
#
|
81
|
+
# hash = { name: 'Rob', age: '28' }
|
82
|
+
#
|
83
|
+
# hash.transform_keys{ |key| key.to_s.upcase }
|
84
|
+
# # => {"NAME"=>"Rob", "AGE"=>"28"}
|
85
|
+
def transform_keys
|
86
|
+
return enum_for(:transform_keys) unless block_given?
|
87
|
+
result = self.class.new
|
88
|
+
each_key do |key|
|
89
|
+
result[yield(key)] = self[key]
|
90
|
+
end
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
# Destructively convert all keys using the block operations.
|
95
|
+
# Same as transform_keys but modifies +self+.
|
96
|
+
def transform_keys!
|
97
|
+
return enum_for(:transform_keys!) unless block_given?
|
98
|
+
keys.each do |key|
|
99
|
+
self[yield(key)] = delete(key)
|
100
|
+
end
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns a new hash with all keys converted to strings.
|
105
|
+
#
|
106
|
+
# hash = { name: 'Rob', age: '28' }
|
107
|
+
#
|
108
|
+
# hash.stringify_keys
|
109
|
+
# # => {"name"=>"Rob", "age"=>"28"}
|
110
|
+
def stringify_keys
|
111
|
+
transform_keys(&:to_s)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Destructively convert all keys to strings. Same as
|
115
|
+
# +stringify_keys+, but modifies +self+.
|
116
|
+
def stringify_keys!
|
117
|
+
transform_keys!(&:to_s)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns a new hash with all keys converted to symbols, as long as
|
121
|
+
# they respond to +to_sym+.
|
122
|
+
#
|
123
|
+
# hash = { 'name' => 'Rob', 'age' => '28' }
|
124
|
+
#
|
125
|
+
# hash.symbolize_keys
|
126
|
+
# # => {:name=>"Rob", :age=>"28"}
|
127
|
+
def symbolize_keys
|
128
|
+
transform_keys do |key|
|
129
|
+
begin
|
130
|
+
key.to_sym
|
131
|
+
rescue
|
132
|
+
key
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
alias to_options symbolize_keys
|
137
|
+
|
138
|
+
# Destructively convert all keys to symbols, as long as they respond
|
139
|
+
# to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
|
140
|
+
def symbolize_keys!
|
141
|
+
transform_keys! do |key|
|
142
|
+
begin
|
143
|
+
key.to_sym
|
144
|
+
rescue
|
145
|
+
key
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
alias to_options! symbolize_keys!
|
150
|
+
|
151
|
+
# Validate all keys in a hash match <tt>*valid_keys</tt>, raising
|
152
|
+
# ArgumentError on a mismatch.
|
153
|
+
#
|
154
|
+
# Note that keys are treated differently than HashWithIndifferentAccess,
|
155
|
+
# meaning that string and symbol keys will not match.
|
156
|
+
#
|
157
|
+
# { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
|
158
|
+
# { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
|
159
|
+
# { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
|
160
|
+
def assert_valid_keys(*valid_keys)
|
161
|
+
valid_keys.flatten!
|
162
|
+
each_key do |k|
|
163
|
+
unless valid_keys.include?(k)
|
164
|
+
raise ArgumentError, "Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns a new hash with all keys converted by the block operation.
|
170
|
+
# This includes the keys from the root hash and from all
|
171
|
+
# nested hashes and arrays.
|
172
|
+
#
|
173
|
+
# hash = { person: { name: 'Rob', age: '28' } }
|
174
|
+
#
|
175
|
+
# hash.deep_transform_keys{ |key| key.to_s.upcase }
|
176
|
+
# # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
|
177
|
+
def deep_transform_keys(&block)
|
178
|
+
_deep_transform_keys_in_object(self, &block)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Destructively convert all keys by using the block operation.
|
182
|
+
# This includes the keys from the root hash and from all
|
183
|
+
# nested hashes and arrays.
|
184
|
+
def deep_transform_keys!(&block)
|
185
|
+
_deep_transform_keys_in_object!(self, &block)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Returns a new hash with all keys converted to strings.
|
189
|
+
# This includes the keys from the root hash and from all
|
190
|
+
# nested hashes and arrays.
|
191
|
+
#
|
192
|
+
# hash = { person: { name: 'Rob', age: '28' } }
|
193
|
+
#
|
194
|
+
# hash.deep_stringify_keys
|
195
|
+
# # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
|
196
|
+
def deep_stringify_keys
|
197
|
+
deep_transform_keys(&:to_s)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Destructively convert all keys to strings.
|
201
|
+
# This includes the keys from the root hash and from all
|
202
|
+
# nested hashes and arrays.
|
203
|
+
def deep_stringify_keys!
|
204
|
+
deep_transform_keys!(&:to_s)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Returns a new hash with all keys converted to symbols, as long as
|
208
|
+
# they respond to +to_sym+. This includes the keys from the root hash
|
209
|
+
# and from all nested hashes and arrays.
|
210
|
+
#
|
211
|
+
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
|
212
|
+
#
|
213
|
+
# hash.deep_symbolize_keys
|
214
|
+
# # => {:person=>{:name=>"Rob", :age=>"28"}}
|
215
|
+
def deep_symbolize_keys
|
216
|
+
deep_transform_keys do |key|
|
217
|
+
begin
|
218
|
+
key.to_sym
|
219
|
+
rescue
|
220
|
+
key
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# Destructively convert all keys to symbols, as long as they respond
|
226
|
+
# to +to_sym+. This includes the keys from the root hash and from all
|
227
|
+
# nested hashes and arrays.
|
228
|
+
def deep_symbolize_keys!
|
229
|
+
deep_transform_keys! do |key|
|
230
|
+
begin
|
231
|
+
key.to_sym
|
232
|
+
rescue
|
233
|
+
key
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
# support methods for deep transforming nested hashes and arrays
|
241
|
+
def _deep_transform_keys_in_object(object, &block)
|
242
|
+
case object
|
243
|
+
when Hash
|
244
|
+
object.each_with_object({}) do |(key, value), result|
|
245
|
+
result[yield(key)] = _deep_transform_keys_in_object(value, &block)
|
246
|
+
end
|
247
|
+
when Array
|
248
|
+
object.map { |e| _deep_transform_keys_in_object(e, &block) }
|
249
|
+
else
|
250
|
+
object
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def _deep_transform_keys_in_object!(object, &block)
|
255
|
+
case object
|
256
|
+
when Hash
|
257
|
+
object.keys.each do |key|
|
258
|
+
value = object.delete(key)
|
259
|
+
object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
|
260
|
+
end
|
261
|
+
object
|
262
|
+
when Array
|
263
|
+
object.map! { |e| _deep_transform_keys_in_object!(e, &block) }
|
264
|
+
else
|
265
|
+
object
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
data/lib/constants.rb
ADDED
@@ -0,0 +1,302 @@
|
|
1
|
+
require 'constants'
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'paint'
|
5
|
+
require 'rexml/document'
|
6
|
+
require 'rexml/formatters/pretty'
|
7
|
+
|
8
|
+
module Thoom
|
9
|
+
class OutputBuilder
|
10
|
+
attr_accessor :colors, :title_output, :response_time
|
11
|
+
|
12
|
+
def initialize(colors)
|
13
|
+
@colors = colors
|
14
|
+
@output = ''
|
15
|
+
end
|
16
|
+
|
17
|
+
def title(centered = true)
|
18
|
+
return if title_output
|
19
|
+
|
20
|
+
client_copy = "Thoom::RestUp v#{Thoom::Constants::VERSION}"
|
21
|
+
author_copy = '@author Z.d. Peacock <zdp@thoomtech.com>'
|
22
|
+
link_copy = '@link http://github.com/thoom/restup'
|
23
|
+
|
24
|
+
if centered
|
25
|
+
max = [client_copy.length, author_copy.length, link_copy.length].max + 2
|
26
|
+
client_copy = client_copy.center(max, ' ')
|
27
|
+
author_copy = author_copy.center(max, ' ')
|
28
|
+
link_copy = link_copy.center(max, ' ')
|
29
|
+
end
|
30
|
+
|
31
|
+
@title_output = true
|
32
|
+
puts "\n",
|
33
|
+
Paint[client_copy, colors[:title_color], colors[:title_bgcolor]],
|
34
|
+
Paint[author_copy, colors[:subtitle_color], colors[:subtitle_bgcolor]],
|
35
|
+
Paint[link_copy, colors[:subtitle_color], colors[:subtitle_bgcolor]]
|
36
|
+
end
|
37
|
+
|
38
|
+
def header(h)
|
39
|
+
len = Paint.unpaint(h).length
|
40
|
+
l = '-' * len
|
41
|
+
puts "\n#{h}\n#{l}\n"
|
42
|
+
end
|
43
|
+
|
44
|
+
def help(config_file, opts)
|
45
|
+
title
|
46
|
+
section 'How to use RestUp'
|
47
|
+
|
48
|
+
puts <<TEXT
|
49
|
+
RestUp works out of the box with APIs that use Basic Authentication (though this is not required).
|
50
|
+
To use other forms of authentication, custom headers can either be passed with each request
|
51
|
+
or stored in the config file as described below.
|
52
|
+
|
53
|
+
If a #{Paint[config_file, colors[:help_filename]]} file exists, the client will pull in defaults and provide several shortcut methods
|
54
|
+
that can simplify using a REST-based API.
|
55
|
+
|
56
|
+
If the API uses form encoded input, you can define your post in JSON format. The client
|
57
|
+
will encode it automatically.
|
58
|
+
TEXT
|
59
|
+
|
60
|
+
section 'Console'
|
61
|
+
puts opts
|
62
|
+
|
63
|
+
section 'YAML config'
|
64
|
+
puts <<TEXT
|
65
|
+
If a file is not passed in with the `-c, --config` flag, then it will use the default #{Paint[config_file, colors[:help_filename]]}.
|
66
|
+
The client uses two different methods to find the YAML configuration file. It will
|
67
|
+
first look in the current directory. If it is not present, it will then look in the current user's
|
68
|
+
home directory.
|
69
|
+
|
70
|
+
This makes it possible to use restup to connect to different APIs simply by changing folders.
|
71
|
+
|
72
|
+
KEY DESC
|
73
|
+
---- -----
|
74
|
+
user: The username. Default: blank, so disable Basic Authentication
|
75
|
+
pass: The password. Default: blank, so disable Basic Authentication
|
76
|
+
|
77
|
+
url: The base REST url
|
78
|
+
json: The default JSON MIME type. Default: "application/json"
|
79
|
+
xml: The default XML MIME type. Default: "application/xml"
|
80
|
+
|
81
|
+
colors: Hash of default color values
|
82
|
+
success: Color to highlight successful messages. Default: :green
|
83
|
+
warning: Color to highlight warning messages. Default: :yellow
|
84
|
+
info: Color to highlight info messages. Default: :yellow
|
85
|
+
error: Color to highlight error messages. Default :red
|
86
|
+
|
87
|
+
flags: Default command line options
|
88
|
+
display: What to display by default.
|
89
|
+
Values: concise, response_only, response_code_only, succcess_only, verbose
|
90
|
+
Default: response_only
|
91
|
+
|
92
|
+
headers: Hash of default headers. Useful for custom headers or headers used in every request.
|
93
|
+
The keys for this hash are strings, not symbols like the other keys
|
94
|
+
|
95
|
+
timeout: The number of seconds to wait for a response before timing out. Default: 300
|
96
|
+
|
97
|
+
tls_verify: When using TLS, the verify mode to use. Values: true, false. Default: true
|
98
|
+
|
99
|
+
xmethods: Array of nonstandard methods that are accepted by the API. To use these methods the
|
100
|
+
API must support X-HTTP-Method-Override.
|
101
|
+
TEXT
|
102
|
+
|
103
|
+
section 'Examples'
|
104
|
+
|
105
|
+
header 'GET Request'
|
106
|
+
|
107
|
+
puts <<TEXT
|
108
|
+
The YAML config:
|
109
|
+
url: http://example.com/api
|
110
|
+
user: myname
|
111
|
+
pass: P@ssWord
|
112
|
+
|
113
|
+
#{Paint['restup -j /hello/world', colors[:help_sample_request]]}
|
114
|
+
|
115
|
+
To use without the config:
|
116
|
+
#{Paint['restup -u myname -p P@ssWord -j http://example.com/api/hello/world', colors[:help_sample_request]]}
|
117
|
+
|
118
|
+
Submits a GET request to #{Paint['http://example/api/hello/world', colors[:help_sample_url]]} with Basic Auth header using the
|
119
|
+
user and pass values in the config.
|
120
|
+
|
121
|
+
It would return JSON values. If successful, the JSON would be parsed and highlighted in #{Paint[colors[:success].to_s.upcase, colors[:success]]}. If
|
122
|
+
the an error was returned (an HTTP response code >= 400), the body would be in #{Paint[colors[:error].to_s.upcase, colors[:error]]}.
|
123
|
+
TEXT
|
124
|
+
|
125
|
+
header 'POST Request'
|
126
|
+
|
127
|
+
puts <<TEXT
|
128
|
+
The YAML config:
|
129
|
+
url: http://example.com/api
|
130
|
+
user: myname
|
131
|
+
pass: P@ssWord
|
132
|
+
headers:
|
133
|
+
X-Custom-Id: abc12345
|
134
|
+
|
135
|
+
#{Paint['restup -m post -j /hello/world < salutation.json', colors[:help_sample_request]]}
|
136
|
+
|
137
|
+
Submits a POST request to #{Paint['http://example/api/hello/world', colors[:help_sample_url]]} with Basic Auth header
|
138
|
+
using the user and pass values in the config. It imports the salutation.json and passes it to the API as application/json
|
139
|
+
content type. It would also set the X-Custom-Id header with every request.
|
140
|
+
|
141
|
+
It would return JSON values. If successful, the JSON would be parsed and highlighted in #{Paint[colors[:success].to_s.upcase, colors[:success]]}. If
|
142
|
+
the an error was returned (an HTTP response code >= 400), the body would be in #{Paint[colors[:error].to_s.upcase, colors[:error]]}.
|
143
|
+
TEXT
|
144
|
+
exit
|
145
|
+
end
|
146
|
+
|
147
|
+
def section(h)
|
148
|
+
len = Paint.unpaint(h).length
|
149
|
+
l = '-' * (len + 4)
|
150
|
+
puts "\n#{l}\n| #{h} |\n#{l}\n"
|
151
|
+
end
|
152
|
+
|
153
|
+
def xp(xml_text)
|
154
|
+
out = ''
|
155
|
+
|
156
|
+
formatter = REXML::Formatters::Pretty.new
|
157
|
+
formatter.compact = true
|
158
|
+
formatter.write(REXML::Document.new(xml_text), out)
|
159
|
+
out
|
160
|
+
end
|
161
|
+
|
162
|
+
def request(client, request, verbose)
|
163
|
+
path = client.uri.path
|
164
|
+
query = ''
|
165
|
+
query += '?' + client.uri.query if client.uri.query
|
166
|
+
|
167
|
+
port_color = client.uri.port == 80 ? :request_port_http : :request_port_tls
|
168
|
+
request_section = "REQUEST: #{Paint[client.method.upcase, colors[:request_method]]} "
|
169
|
+
request_section += Paint["#{client.uri.host}:", colors[:request_path]]
|
170
|
+
request_section += Paint[client.uri.port.to_s, colors[port_color]]
|
171
|
+
request_section += Paint[path, colors[:request_path]]
|
172
|
+
request_section += Paint[query, colors[:request_endpoint]]
|
173
|
+
|
174
|
+
unless request.respond_to? 'each_header'
|
175
|
+
header 'MALFORMED REQUEST'
|
176
|
+
quit Paint[request, colors[:error]]
|
177
|
+
end
|
178
|
+
|
179
|
+
if verbose
|
180
|
+
section request_section if verbose
|
181
|
+
|
182
|
+
header 'HEADERS'
|
183
|
+
request.each_header { |k, v| puts "#{k}: #{v}\n" }
|
184
|
+
|
185
|
+
if client.data
|
186
|
+
header 'BODY'
|
187
|
+
|
188
|
+
begin
|
189
|
+
puts %w(UTF-8 ASCII-8BIT).include?(client.data.encoding.to_s) ? client.data : client.data.encode('ASCII-8BIT')
|
190
|
+
rescue EncodingError
|
191
|
+
puts "Data posted, but contains non-UTF-8 data, so it's not echoed here."
|
192
|
+
end
|
193
|
+
end
|
194
|
+
else
|
195
|
+
puts "\n#{request_section}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def response(response, verbose)
|
200
|
+
response_color = response.code.to_i < 400 ? colors[:success] : colors[:error]
|
201
|
+
response_section = "RESPONSE: #{Paint[response.code, response_color]} (#{response_time} sec)"
|
202
|
+
|
203
|
+
if verbose || response_color == colors[:error]
|
204
|
+
section response_section
|
205
|
+
header 'HEADERS'
|
206
|
+
response.each_header { |k, v| puts "#{k}: #{v}\n" }
|
207
|
+
else
|
208
|
+
puts response_section
|
209
|
+
end
|
210
|
+
|
211
|
+
header 'BODY' if verbose
|
212
|
+
puts 'BODY:' unless verbose
|
213
|
+
|
214
|
+
if !response.body || response.body.empty?
|
215
|
+
puts Paint['NONE', colors[:info]]
|
216
|
+
else
|
217
|
+
body = response.body
|
218
|
+
begin
|
219
|
+
body.encode!('ASCII-8BIT') if body.encoding.to_s != 'ASCII-8BIT'
|
220
|
+
|
221
|
+
body = if response['content-type'].nil?
|
222
|
+
body
|
223
|
+
elsif response['content-type'].include? 'json'
|
224
|
+
JSON.pretty_unparse(JSON.parse(body))
|
225
|
+
elsif response['content-type'].include? 'xml'
|
226
|
+
xp(body)
|
227
|
+
else
|
228
|
+
body
|
229
|
+
end
|
230
|
+
puts Paint[body, response_color]
|
231
|
+
rescue EncodingError => e
|
232
|
+
puts Paint["RESPONSE contains non-UTF-8 data, so it's not echoed here.", colors[:info]]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def save_response(response, content_disposition, output)
|
238
|
+
if content_disposition && output.nil? && response.to_hash.key?('content-disposition')
|
239
|
+
cd = response['content-disposition']
|
240
|
+
output = cd[cd.index('filename=') + 9..-1]
|
241
|
+
end
|
242
|
+
|
243
|
+
unless output.nil?
|
244
|
+
file = File.expand_path(output)
|
245
|
+
if File.exist?(File.dirname(file))
|
246
|
+
File.open(file, 'w') { |f| f.write response.body }
|
247
|
+
puts Paint["Response written to file: #{file}", colors[:info]]
|
248
|
+
else
|
249
|
+
puts Paint["Could not write to file #{file}", colors[:error]]
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def quit(content, centered = true)
|
255
|
+
title(centered)
|
256
|
+
puts "\n#{content}"
|
257
|
+
exit
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# Sets up the default color set
|
262
|
+
class DefaultOutputBuilder < OutputBuilder
|
263
|
+
def initialize
|
264
|
+
colors = {
|
265
|
+
title_color: '4D7326',
|
266
|
+
title_bgcolor: :white,
|
267
|
+
|
268
|
+
subtitle_color: :white,
|
269
|
+
subtitle_bgcolor: '4D7326',
|
270
|
+
|
271
|
+
help_filename: :yellow,
|
272
|
+
help_sample_request: :magenta,
|
273
|
+
help_sample_url: :blue,
|
274
|
+
|
275
|
+
request_method: :cyan,
|
276
|
+
request_path: '813b5e',
|
277
|
+
request_port_http: '813b5e',
|
278
|
+
request_port_tls: '264d73',
|
279
|
+
request_endpoint: :yellow,
|
280
|
+
|
281
|
+
success: '277326',
|
282
|
+
warning: :yellow,
|
283
|
+
info: :yellow,
|
284
|
+
error: 'c20f12'
|
285
|
+
}
|
286
|
+
super(colors)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Outputs just the basic default colors
|
291
|
+
class SimpleOutputBuilder < OutputBuilder
|
292
|
+
def initialize
|
293
|
+
colors = {
|
294
|
+
subtitle_color: :default,
|
295
|
+
subtitle_bgcolor: :default,
|
296
|
+
title_color: :default,
|
297
|
+
title_bgcolor: :default
|
298
|
+
}
|
299
|
+
super(colors)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
data/lib/rest_up.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'logger'
|
3
|
+
require 'net/http'
|
4
|
+
require 'openssl'
|
5
|
+
require 'uri'
|
6
|
+
require 'config'
|
7
|
+
require 'constants'
|
8
|
+
|
9
|
+
module Thoom
|
10
|
+
# General Error message returned by the class
|
11
|
+
class RestUpError < RuntimeError
|
12
|
+
attr_reader :message
|
13
|
+
|
14
|
+
def initialize(message)
|
15
|
+
@message = message
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Makes the request
|
20
|
+
class RestUp
|
21
|
+
attr_accessor :endpoint, :data, :cert
|
22
|
+
attr_reader :headers, :log, :method
|
23
|
+
|
24
|
+
def initialize(config = nil)
|
25
|
+
@config = config.nil? ? HashConfig.new : config
|
26
|
+
@log = Logger.new STDOUT
|
27
|
+
|
28
|
+
@uri = nil
|
29
|
+
@xmethods = nil
|
30
|
+
@headers = @config.get(:headers, {})
|
31
|
+
@standard_methods = %w(delete get head options patch post put)
|
32
|
+
end
|
33
|
+
|
34
|
+
def headers=(headers)
|
35
|
+
headers.each { |key, val| @headers[key.to_sym] = val }
|
36
|
+
end
|
37
|
+
|
38
|
+
def method=(method)
|
39
|
+
method.downcase!
|
40
|
+
|
41
|
+
unless @standard_methods.include?(method) || xmethods.include?(method)
|
42
|
+
raise RestUpError, 'Invalid Method'
|
43
|
+
end
|
44
|
+
|
45
|
+
if xmethods.include? method
|
46
|
+
headers['x-http-method-override'] = method.upcase
|
47
|
+
method = 'post'
|
48
|
+
end
|
49
|
+
|
50
|
+
@method = method
|
51
|
+
end
|
52
|
+
|
53
|
+
def request
|
54
|
+
raise RestUpError, 'Invalid URL' unless uri.respond_to?(:request_uri)
|
55
|
+
|
56
|
+
request = create_request(uri.request_uri)
|
57
|
+
|
58
|
+
add_request_headers(request)
|
59
|
+
add_request_body(request)
|
60
|
+
|
61
|
+
request
|
62
|
+
end
|
63
|
+
|
64
|
+
def http
|
65
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
66
|
+
http.read_timeout = @config.get(:timeout, 300)
|
67
|
+
|
68
|
+
configure_tls http
|
69
|
+
configure_client_cert http
|
70
|
+
|
71
|
+
http
|
72
|
+
end
|
73
|
+
|
74
|
+
def submit(request)
|
75
|
+
http.request request
|
76
|
+
end
|
77
|
+
|
78
|
+
def uri
|
79
|
+
return @uri if @uri
|
80
|
+
@uri = URI.parse url
|
81
|
+
end
|
82
|
+
|
83
|
+
def url
|
84
|
+
return endpoint if endpoint.start_with?('http')
|
85
|
+
|
86
|
+
@config.get(:url, '') + endpoint
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def create_request(request_uri)
|
92
|
+
request = Net::HTTP.const_get(method.capitalize).new request_uri
|
93
|
+
|
94
|
+
configure_basic_auth(request)
|
95
|
+
|
96
|
+
request['User-Agent'] = 'Thoom::RestUp/' + Constants::VERSION
|
97
|
+
request.content_length = 0
|
98
|
+
|
99
|
+
request
|
100
|
+
end
|
101
|
+
|
102
|
+
def configure_basic_auth(request)
|
103
|
+
user = @config.get(:user, '')
|
104
|
+
pass = @config.get(:pass, '')
|
105
|
+
|
106
|
+
request.basic_auth(user, pass) unless user.to_s.empty? || pass.to_s.empty?
|
107
|
+
end
|
108
|
+
|
109
|
+
def configure_client_cert(http)
|
110
|
+
pem = cert.nil? ? @config.get(:cert, '') : cert
|
111
|
+
return if pem.empty?
|
112
|
+
|
113
|
+
begin
|
114
|
+
http.cert = OpenSSL::X509::Certificate.new pem
|
115
|
+
http.key = OpenSSL::PKey::RSA.new pem
|
116
|
+
rescue OpenSSL::OpenSSLError
|
117
|
+
raise RestUpError, 'Invalid client certificate'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def configure_tls(http)
|
122
|
+
return if uri.scheme != 'https'
|
123
|
+
http.use_ssl = true
|
124
|
+
|
125
|
+
mode = @config.get(:tls_verify, true) ? 'VERIFY_PEER' : 'VERIFY_NONE'
|
126
|
+
http.verify_mode = OpenSSL::SSL.const_get(mode)
|
127
|
+
end
|
128
|
+
|
129
|
+
def add_request_body(request)
|
130
|
+
return if data.nil? || data.empty?
|
131
|
+
|
132
|
+
body = data.clone
|
133
|
+
request.content_length = body.length
|
134
|
+
request.body = body
|
135
|
+
end
|
136
|
+
|
137
|
+
def add_request_headers(request)
|
138
|
+
return unless headers.respond_to? :each
|
139
|
+
|
140
|
+
headers.each { |key, val| request[key.to_s.strip] = val.strip }
|
141
|
+
end
|
142
|
+
|
143
|
+
def xmethods
|
144
|
+
return @xmethods if @xmethods
|
145
|
+
|
146
|
+
xmethods = @config.get(:xmethods, [])
|
147
|
+
unless xmethods.respond_to? :map
|
148
|
+
raise RestUpError, 'Invalid xmethods configuration'
|
149
|
+
end
|
150
|
+
|
151
|
+
@xmethods = xmethods.map(&:downcase)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: restup
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Z.d. Peacock
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-03-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: paint
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
description: A class and executable for interacting with RESTful web services
|
28
|
+
email: zdp@thoomtech.com
|
29
|
+
executables:
|
30
|
+
- restup
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- LICENSE
|
35
|
+
- README.md
|
36
|
+
- bin/restup
|
37
|
+
- lib/config.rb
|
38
|
+
- lib/constants.rb
|
39
|
+
- lib/output_builder.rb
|
40
|
+
- lib/rest_up.rb
|
41
|
+
homepage: http://github.com/thoom/restup
|
42
|
+
licenses:
|
43
|
+
- MIT
|
44
|
+
metadata: {}
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 2.6.8
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: 'Thoom RestUp: A simple REST client'
|
65
|
+
test_files: []
|