restup 1.0.1

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