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 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,7 @@
1
+ module Thoom
2
+ class Constants
3
+ VERSION = '1.0'.freeze
4
+ MIME_JSON = 'application/json'.freeze
5
+ MIME_XML = 'application/xml'.freeze
6
+ end
7
+ end
@@ -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: []