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