yobi-http 0.6.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/README.md +1 -0
- data/exe/yobi +74 -77
- data/lib/yobi/cli/arguments.rb +52 -0
- data/lib/yobi/cli.rb +3 -0
- data/lib/yobi/extensions/net_http/parsing.rb +23 -0
- data/lib/yobi/extensions/net_http.rb +3 -0
- data/lib/yobi/extensions.rb +5 -0
- data/lib/yobi/http.rb +23 -0
- data/lib/yobi/version.rb +6 -0
- data/lib/yobi.rb +22 -11
- data/screenshot.png +0 -0
- data/spec/extensions/net_http/parsing_spec.rb +44 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/support/webmock.rb +15 -0
- data/spec/yobi/cli/arguments_spec.rb +108 -0
- data/spec/yobi/http_spec.rb +40 -0
- data/spec/yobi_spec.rb +40 -0
- metadata +30 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56563ef37ac915d9ea34678799ac53ae2f8d91355dfd383e47929ea17e0814b0
|
|
4
|
+
data.tar.gz: cc47d7c2010c5177685cd75462e4c2c387befa2d31d117acaf5e99f23dc91e59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90392c2e87b514c76c3f5b1eaa1049140f38ae2b0c87937379f2f767ba6fff1809b2143433479e99f18a5cb550165aa3003860b173247ddac08ab124a448423e
|
|
7
|
+
data.tar.gz: 63f13abd60c7e4580d504e2e5cf6b39fa919708309e21dc2371bc78750a7b7f52d9635c2a07561cb043fb5181b66975dde63d3a793e5e29563ab1d2fb8559aa8
|
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--require ./spec/spec_helper
|
data/README.md
CHANGED
|
@@ -6,6 +6,7 @@ and flexible, allowing you to customize your requests as needed.
|
|
|
6
6
|
Its a lightweight implementation of the HTTPie tool, which is a command-line HTTP client that allows you to make
|
|
7
7
|
HTTP requests and view the responses in a human-friendly format.
|
|
8
8
|
|
|
9
|
+

|
|
9
10
|
|
|
10
11
|
## Installation
|
|
11
12
|
|
data/exe/yobi
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
5
5
|
|
|
6
|
-
require "base64"
|
|
7
6
|
require "erb"
|
|
8
7
|
require "json"
|
|
9
8
|
require "net/http"
|
|
@@ -15,7 +14,9 @@ require "uri"
|
|
|
15
14
|
require "yobi"
|
|
16
15
|
|
|
17
16
|
# parsing arguments
|
|
18
|
-
@options = {
|
|
17
|
+
@options = {
|
|
18
|
+
print: "HB", auth: nil, auth_type: "basic", verbose: false, raw: false, offline: false, follow: false, debug: false
|
|
19
|
+
}
|
|
19
20
|
|
|
20
21
|
parser = OptionParser.new do |opts|
|
|
21
22
|
opts.banner = %(
|
|
@@ -60,12 +61,22 @@ parser = OptionParser.new do |opts|
|
|
|
60
61
|
@options[:offline] = true
|
|
61
62
|
end
|
|
62
63
|
|
|
64
|
+
opts.on("--follow", "Automatically follow HTTP redirects") do
|
|
65
|
+
@options[:follow] = true
|
|
66
|
+
end
|
|
67
|
+
|
|
63
68
|
opts.on("-o", "--output FILE", "Write output to a file instead of STDOUT") do |file|
|
|
64
69
|
@options[:output] = file
|
|
65
70
|
end
|
|
66
71
|
|
|
67
72
|
opts.on("-v", "--verbose", "Print detailed request and response information") do
|
|
68
73
|
@options[:verbose] = true
|
|
74
|
+
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
opts.on("--debug", "Print debug information") do
|
|
78
|
+
@options[:debug] = true
|
|
79
|
+
@options[:verbose] = true
|
|
69
80
|
end
|
|
70
81
|
|
|
71
82
|
opts.on("--version", "Print app version") do
|
|
@@ -77,106 +88,92 @@ end
|
|
|
77
88
|
parser.parse!(ARGV)
|
|
78
89
|
|
|
79
90
|
# resolve the HTTP method
|
|
80
|
-
method = Yobi
|
|
91
|
+
method = Yobi.args.http_method?(ARGV[0]) ? ARGV.shift : "GET"
|
|
81
92
|
|
|
82
93
|
# resolve the URL
|
|
83
|
-
url =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"http://localhost#{ARGV.shift}"
|
|
88
|
-
else
|
|
89
|
-
"http://#{ARGV.shift}"
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
data =
|
|
93
|
-
ARGV.select { |arg| arg.include?("=") }.map.to_h { |arg| arg.split("=", 2).map(&:strip) }
|
|
94
|
-
|
|
95
|
-
headers =
|
|
96
|
-
{ "Content-Type" => "application/json", "User-Agent" => "#{Yobi.name}/#{Yobi::VERSION}" }
|
|
97
|
-
.merge(ARGV.select { |arg| arg.include?(":") }.map.to_h { |arg| arg.split(":", 2).map(&:strip) })
|
|
94
|
+
url = Yobi.args.url(ARGV.shift)
|
|
95
|
+
|
|
96
|
+
data = Yobi.args.parse_data(ARGV)
|
|
97
|
+
headers = Yobi.args.parse_headers(ARGV)
|
|
98
98
|
|
|
99
99
|
# prepare authentication header if auth is provided
|
|
100
|
-
if @options[:auth]
|
|
101
|
-
auth_type = @options[:auth_type] || "basic"
|
|
102
|
-
case auth_type.downcase
|
|
103
|
-
when "basic"
|
|
104
|
-
headers["Authorization"] = "Basic #{Base64.strict_encode64(@options[:auth])}"
|
|
105
|
-
when "bearer"
|
|
106
|
-
headers["Authorization"] = "Bearer #{@options[:auth]}"
|
|
107
|
-
else
|
|
108
|
-
warn "Unsupported authentication type: #{auth_type}"
|
|
109
|
-
end
|
|
110
|
-
end
|
|
100
|
+
Yobi.args.auth_header(headers, @options) if @options[:auth]
|
|
111
101
|
|
|
112
102
|
pp [method, url, data, headers, @options, ARGV] if ENV["YOBI_DEBUG"]
|
|
113
103
|
|
|
114
|
-
|
|
104
|
+
# Extend Net::HTTPResponse to add util behaviors
|
|
105
|
+
def offline_mode(request, options)
|
|
115
106
|
Net::HTTP.class_eval do
|
|
116
107
|
def connect; end
|
|
117
108
|
end
|
|
109
|
+
|
|
110
|
+
options[:verbose] = true
|
|
111
|
+
|
|
112
|
+
response = Net::HTTPResponse.new("1.1", "200", "OK")
|
|
113
|
+
response["Content-Type"] = "application/json"
|
|
114
|
+
response["Access-Control-Allow-Credentials"] = true
|
|
115
|
+
response["Access-Control-Allow-Origin"] = "*"
|
|
116
|
+
response["Connection"] = "close"
|
|
117
|
+
response["Date"] = Time.now.httpdate
|
|
118
|
+
response["Server"] = "yobi-offline/#{Yobi::VERSION}"
|
|
119
|
+
response["X-Powered-By"] = "Yobi/#{Yobi::VERSION}"
|
|
120
|
+
response.body = body = JSON.pretty_generate({ message: "Offline mode enabled" })
|
|
121
|
+
|
|
122
|
+
view = Yobi.view(:output)
|
|
123
|
+
puts TTY::Markdown.parse(view.result(binding), color: :always)
|
|
124
|
+
|
|
125
|
+
exit 0
|
|
118
126
|
end
|
|
119
127
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
headers.each { |key, value| request[key] = value }
|
|
126
|
-
request["Content-Type"] ||= "application/json"
|
|
127
|
-
|
|
128
|
-
request.body = data.to_json unless data.empty?
|
|
129
|
-
|
|
130
|
-
if options[:offline]
|
|
131
|
-
options[:verbose] = true
|
|
132
|
-
# In offline mode, we prepare the request but do not send it. Instead, we print the request details for debugging
|
|
133
|
-
# purposes.
|
|
134
|
-
response = Net::HTTPResponse.new("1.1", "200", "OK")
|
|
135
|
-
response["Content-Type"] = "application/json"
|
|
136
|
-
response["Access-Control-Allow-Credentials"] = true
|
|
137
|
-
response["Access-Control-Allow-Origin"] = "*"
|
|
138
|
-
response["Connection"] = "close"
|
|
139
|
-
response["Date"] = Time.now.httpdate
|
|
140
|
-
response["Server"] = "yobi-offline/#{Yobi::VERSION}"
|
|
141
|
-
response["X-Powered-By"] = "Yobi/#{Yobi::VERSION}"
|
|
142
|
-
response.body = body = JSON.pretty_generate({ message: "Offline mode enabled" })
|
|
143
|
-
|
|
144
|
-
view = Yobi.view(:output)
|
|
145
|
-
puts TTY::Markdown.parse(ERB.new(view).result(binding), color: :always)
|
|
146
|
-
break
|
|
128
|
+
def raw_mode(_request, response, options)
|
|
129
|
+
if options[:print].include?("H")
|
|
130
|
+
puts "HTTP/#{response.http_version} #{response.code} #{response.message}"
|
|
131
|
+
response.each_header { |key, value| puts "#{key}: #{value}" }
|
|
132
|
+
puts
|
|
147
133
|
end
|
|
148
134
|
|
|
149
|
-
|
|
135
|
+
body = response.parsed_body
|
|
136
|
+
puts body if options[:print].include?("B") && body
|
|
150
137
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
138
|
+
exit 0
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def follow_redirects(response, url, method, data, headers, options)
|
|
142
|
+
return response unless response.is_a?(Net::HTTPRedirection)
|
|
143
|
+
|
|
144
|
+
location = response["location"]
|
|
145
|
+
warn "Redirected to #{location}" if options[:debug]
|
|
146
|
+
new_url = URI.join(url, location).to_s
|
|
147
|
+
|
|
148
|
+
Yobi.request(method, new_url, data: data, headers: headers, options: options) do |new_http, new_request|
|
|
149
|
+
response = new_http.request(new_request)
|
|
150
|
+
return follow_redirects(response, new_url, method, data, headers, options)
|
|
160
151
|
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
Yobi.request(method, url, data: data, headers: headers, options: @options) do |http, request|
|
|
155
|
+
options = @options
|
|
156
|
+
|
|
157
|
+
offline_mode(request, options) if options[:offline]
|
|
161
158
|
|
|
162
|
-
|
|
163
|
-
if options[:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
159
|
+
response =
|
|
160
|
+
if options[:follow]
|
|
161
|
+
follow_redirects(http.request(request), url, method, data, headers, options)
|
|
162
|
+
else
|
|
163
|
+
http.request(request)
|
|
167
164
|
end
|
|
168
165
|
|
|
169
|
-
|
|
166
|
+
raw_mode(request, response, options) if options[:raw]
|
|
170
167
|
|
|
171
|
-
|
|
172
|
-
end
|
|
168
|
+
body = JSON.pretty_generate(response.parsed_body)
|
|
173
169
|
|
|
174
170
|
view = Yobi.view(:output)
|
|
171
|
+
output_result = view.result(binding)
|
|
175
172
|
|
|
176
173
|
if options[:output]
|
|
177
174
|
file = File.expand_path(options[:output], Dir.pwd)
|
|
178
|
-
File.write(file,
|
|
175
|
+
File.write(file, output_result, mode: "w")
|
|
179
176
|
else
|
|
180
|
-
puts TTY::Markdown.parse(
|
|
177
|
+
puts TTY::Markdown.parse(output_result, color: :always)
|
|
181
178
|
end
|
|
182
179
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module Yobi
|
|
6
|
+
module CLI
|
|
7
|
+
# Command-line argument utilities
|
|
8
|
+
module Arguments
|
|
9
|
+
class << self
|
|
10
|
+
def http_method?(value)
|
|
11
|
+
Yobi::Http::METHODS.include? value.upcase
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def url(value)
|
|
15
|
+
case value
|
|
16
|
+
when %r{\Ahttps?://}
|
|
17
|
+
value
|
|
18
|
+
when /\A:\d+/
|
|
19
|
+
"http://localhost#{value}"
|
|
20
|
+
else
|
|
21
|
+
"http://#{value}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def parse_data(args)
|
|
26
|
+
args.select { |arg| arg.include?("=") }.map.to_h { |arg| arg.split("=", 2).map(&:strip) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def parse_headers(args)
|
|
30
|
+
{ "Content-Type" => "application/json", "User-Agent" => "#{Yobi.name}/#{Yobi::VERSION}" }
|
|
31
|
+
.merge(args.select { |arg| arg.include?(":") }.map.to_h { |arg| arg.split(":", 2).map(&:strip) })
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def auth_header(headers, options)
|
|
35
|
+
raise ArgumentError, "Authentication credentials must be provided with --auth" unless options[:auth]
|
|
36
|
+
|
|
37
|
+
auth_type = options.fetch(:auth_type, "basic")
|
|
38
|
+
|
|
39
|
+
case auth_type.downcase
|
|
40
|
+
when "basic"
|
|
41
|
+
headers["Authorization"] = "Basic #{Base64.strict_encode64(options[:auth])}"
|
|
42
|
+
when "bearer"
|
|
43
|
+
headers["Authorization"] = "Bearer #{options[:auth]}"
|
|
44
|
+
else
|
|
45
|
+
warn "Unsupported authentication type: #{auth_type}"
|
|
46
|
+
exit 1
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/yobi/cli.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
# Extend the Net::HTTPResponse class to add parsing methods
|
|
5
|
+
class HTTPResponse
|
|
6
|
+
# Parses the response body as JSON if the Content-Type is application/json, otherwise returns the raw body.
|
|
7
|
+
#
|
|
8
|
+
# @return [Object, String, nil] The parsed JSON object, raw body string, or nil if there is no body.
|
|
9
|
+
def parsed_body
|
|
10
|
+
return @parsed_body if instance_variable_defined?(:@parsed_body)
|
|
11
|
+
|
|
12
|
+
if body && content_type&.include?("application/json")
|
|
13
|
+
begin
|
|
14
|
+
@parsed_body = JSON.parse(body)
|
|
15
|
+
rescue JSON::ParserError
|
|
16
|
+
@parsed_body = body
|
|
17
|
+
end
|
|
18
|
+
else
|
|
19
|
+
@parsed_body = body
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/yobi/http.rb
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "net/http"
|
|
4
|
+
|
|
3
5
|
module Yobi
|
|
4
6
|
# Yobi Http behaviors and constants
|
|
5
7
|
module Http
|
|
6
8
|
METHODS = %w[GET POST PUT DELETE PATCH HEAD OPTIONS].freeze
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# rubocop:disable Metrics/AbcSize
|
|
12
|
+
def request(method, url, data: {}, headers: {}, options: {})
|
|
13
|
+
@uri = URI(url)
|
|
14
|
+
@options = options
|
|
15
|
+
@method = method.capitalize
|
|
16
|
+
|
|
17
|
+
Net::HTTP.start(@uri.host, @uri.port, use_ssl: @uri.scheme == "https") do |http|
|
|
18
|
+
request_class = Net::HTTP.const_get(@method)
|
|
19
|
+
request = request_class.new(@uri)
|
|
20
|
+
|
|
21
|
+
headers.each { |key, value| request[key] = value }
|
|
22
|
+
|
|
23
|
+
request.body = data.to_json unless data.empty?
|
|
24
|
+
|
|
25
|
+
yield(http, request) if block_given?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
# rubocop:enable Metrics/AbcSize
|
|
29
|
+
end
|
|
7
30
|
end
|
|
8
31
|
end
|
data/lib/yobi/version.rb
ADDED
data/lib/yobi.rb
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "erb"
|
|
4
|
+
|
|
5
|
+
require "yobi/cli"
|
|
6
|
+
require "yobi/extensions"
|
|
3
7
|
require "yobi/http"
|
|
8
|
+
require "yobi/version"
|
|
4
9
|
|
|
5
10
|
# Yobi Http CLI client namespace
|
|
6
11
|
module Yobi
|
|
7
12
|
# Standard Yobi error class
|
|
8
13
|
class Error < StandardError; end
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
class << self
|
|
16
|
+
def name
|
|
17
|
+
"yobi"
|
|
18
|
+
end
|
|
12
19
|
|
|
20
|
+
def description
|
|
21
|
+
"A simple HTTP client for testing APIs from the command line."
|
|
22
|
+
end
|
|
13
23
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
# Load project templates
|
|
25
|
+
def view(name)
|
|
26
|
+
ERB.new(File.read(File.join(__dir__, "views/#{name}.md.erb")))
|
|
27
|
+
end
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
def request(...)
|
|
30
|
+
Yobi::Http.request(...)
|
|
31
|
+
end
|
|
21
32
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
def args
|
|
34
|
+
Yobi::CLI::Arguments
|
|
35
|
+
end
|
|
25
36
|
end
|
|
26
37
|
end
|
data/screenshot.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Net::HTTPResponse do
|
|
6
|
+
describe "#parsed_body" do
|
|
7
|
+
subject(:response) do
|
|
8
|
+
Yobi::Http.request(:get, "http://example.com") do |http, request|
|
|
9
|
+
http.request(request)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
before do
|
|
14
|
+
stub_request(:get, "http://example.com")
|
|
15
|
+
.to_return(status: 200, body: '{"name": "John", "age": 30}', headers: { "Content-Type" => "application/json" })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "parses JSON response bodies into Ruby objects" do
|
|
19
|
+
expect(response.parsed_body).to eq({ "name" => "John", "age" => 30 })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context "when the response body is not valid JSON" do
|
|
23
|
+
before do
|
|
24
|
+
stub_request(:get, "http://example.com")
|
|
25
|
+
.to_return(status: 200, body: "Not a JSON string", headers: { "Content-Type" => "application/json" })
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "returns the raw body string if JSON parsing fails" do
|
|
29
|
+
expect(response.parsed_body).to eq("Not a JSON string")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context "when the Content-Type is not application/json" do
|
|
34
|
+
before do
|
|
35
|
+
stub_request(:get, "http://example.com")
|
|
36
|
+
.to_return(status: 200, body: "Just a plain text response", headers: { "Content-Type" => "text/plain" })
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "returns the raw body string if Content-Type is not application/json" do
|
|
40
|
+
expect(response.parsed_body).to eq("Just a plain text response")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
if ENV["COVERAGE"]
|
|
2
|
+
require "simplecov"
|
|
3
|
+
require "simplecov-console"
|
|
4
|
+
|
|
5
|
+
SimpleCov.root(File.expand_path("..", __dir__))
|
|
6
|
+
|
|
7
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
|
|
8
|
+
[SimpleCov::Formatter::HTMLFormatter,
|
|
9
|
+
SimpleCov::Formatter::Console]
|
|
10
|
+
)
|
|
11
|
+
SimpleCov.coverage_dir("coverage")
|
|
12
|
+
|
|
13
|
+
SimpleCov.start do
|
|
14
|
+
add_filter "/spec/"
|
|
15
|
+
add_group "Lib", "lib"
|
|
16
|
+
|
|
17
|
+
track_files "lib/**/*.rb"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
SimpleCov.minimum_coverage 90
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# puts "ROOT: #{SimpleCov.root}"
|
|
24
|
+
# puts "TRACKED: #{SimpleCov.tracked_files.inspect}"
|
|
25
|
+
# puts "LOADED BEFORE START: #{$LOADED_FEATURES.grep(/lib/).size}"
|
|
26
|
+
|
|
27
|
+
Dir[File.expand_path("../lib/**/*.rb", __dir__)].sort.each { |f| require f }
|
|
28
|
+
|
|
29
|
+
RSpec.configure do |config|
|
|
30
|
+
config.expect_with :rspec do |expectations|
|
|
31
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
config.mock_with :rspec do |mocks|
|
|
35
|
+
mocks.verify_partial_doubles = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
|
39
|
+
config.filter_run_when_matching :focus unless ENV["CI"]
|
|
40
|
+
|
|
41
|
+
if ENV["META"]
|
|
42
|
+
config.example_status_persistence_file_path = "spec/examples.txt"
|
|
43
|
+
config.profile_examples = 10
|
|
44
|
+
end
|
|
45
|
+
# config.warnings = true
|
|
46
|
+
|
|
47
|
+
# if config.files_to_run.one?
|
|
48
|
+
# config.default_formatter = "doc"
|
|
49
|
+
# end
|
|
50
|
+
|
|
51
|
+
# config.order = :random
|
|
52
|
+
# Kernel.srand config.seed
|
|
53
|
+
|
|
54
|
+
config.after(:suite) do
|
|
55
|
+
SimpleCov.result.format! if ENV["COVERAGE"]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# load support configuration files
|
|
60
|
+
Dir[File.expand_path("support/**/*.rb", __dir__)].sort.each { |f| require f }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webmock/rspec"
|
|
4
|
+
|
|
5
|
+
RSpec.configure do |config|
|
|
6
|
+
# Enable WebMock
|
|
7
|
+
config.before(:each) do
|
|
8
|
+
WebMock.enable!
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Disable WebMock after each test to allow real HTTP connections if needed
|
|
12
|
+
config.after(:each) do
|
|
13
|
+
WebMock.disable!
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Yobi::CLI::Arguments do
|
|
6
|
+
describe ".http_method?" do
|
|
7
|
+
context "when given valid HTTP methods" do
|
|
8
|
+
Yobi::Http::METHODS.each do |method|
|
|
9
|
+
it "recognizes #{method} as a valid HTTP method" do
|
|
10
|
+
expect(described_class.http_method?(method)).to be_truthy
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "is case-insensitive for #{method}" do
|
|
14
|
+
expect(described_class.http_method?(method.downcase)).to be_truthy
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
context "when given invalid HTTP methods" do
|
|
20
|
+
it "does not recognize FOO as a valid HTTP method" do
|
|
21
|
+
expect(described_class.http_method?("Foo")).to be_falsey
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe ".url" do
|
|
27
|
+
context "when given a URL that starts with http://" do
|
|
28
|
+
it "returns the http URL unchanged if it looks completed and valid" do
|
|
29
|
+
expect(described_class.url("http://example.com")).to eq("http://example.com")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "returns the https URL unchanged if it looks completed and valid" do
|
|
33
|
+
expect(described_class.url("https://example.com")).to eq("https://example.com")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context "when given a URL that does not start with http://" do
|
|
38
|
+
it "prepends http:// to URLs" do
|
|
39
|
+
expect(described_class.url("example.com")).to eq("http://example.com")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
context "when given a URL that starts with :port" do
|
|
44
|
+
it "returns a URL with http://localhost prepended" do
|
|
45
|
+
expect(described_class.url(":3000")).to eq("http://localhost:3000")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe ".parse_data" do
|
|
51
|
+
it "parses key=value pairs into a hash" do
|
|
52
|
+
args = ["name=John", "age=30", "city=New York"]
|
|
53
|
+
|
|
54
|
+
expect(described_class.parse_data(args)).to eq({ "name" => "John", "age" => "30", "city" => "New York" })
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "ignores arguments that do not contain an equals sign" do
|
|
58
|
+
args = ["name=John", "invalid_arg", "age=30"]
|
|
59
|
+
|
|
60
|
+
expect(described_class.parse_data(args)).to eq({ "name" => "John", "age" => "30" })
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe ".parse_headers" do
|
|
65
|
+
it "parses key:value pairs into a headers hash" do
|
|
66
|
+
args = ["Content-Type: text/http", "User-Agent: Yobi/1.0"]
|
|
67
|
+
|
|
68
|
+
expect(described_class.parse_headers(args))
|
|
69
|
+
.to eq({ "Content-Type" => "text/http", "User-Agent" => "Yobi/1.0" })
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "includes default headers" do
|
|
73
|
+
args = []
|
|
74
|
+
|
|
75
|
+
expect(described_class.parse_headers(args))
|
|
76
|
+
.to eq({ "Content-Type" => "application/json", "User-Agent" => "#{Yobi.name}/#{Yobi::VERSION}" })
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe ".auth_header" do
|
|
81
|
+
it "raises an error if auth credentials are missing" do
|
|
82
|
+
expect { described_class.auth_header({}, {}) }.to raise_error(ArgumentError)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "adds a Basic Authorization header when auth_type is basic" do
|
|
86
|
+
headers = {}
|
|
87
|
+
options = { auth: "user:pass", auth_type: "basic" }
|
|
88
|
+
|
|
89
|
+
described_class.auth_header(headers, options)
|
|
90
|
+
expect(headers["Authorization"]).to eq("Basic #{Base64.strict_encode64("user:pass")}")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "adds a Bearer Authorization header when auth_type is bearer" do
|
|
94
|
+
headers = {}
|
|
95
|
+
options = { auth: "mytoken", auth_type: "bearer" }
|
|
96
|
+
|
|
97
|
+
described_class.auth_header(headers, options)
|
|
98
|
+
expect(headers["Authorization"]).to eq("Bearer mytoken")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "exits with an error for unsupported auth types" do
|
|
102
|
+
headers = {}
|
|
103
|
+
options = { auth: "mytoken", auth_type: "unsupported" }
|
|
104
|
+
|
|
105
|
+
expect { described_class.auth_header(headers, options) }.to raise_error(SystemExit)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Yobi::Http do
|
|
6
|
+
describe "METHODS" do
|
|
7
|
+
it "includes standard HTTP methods" do
|
|
8
|
+
expect(Yobi::Http::METHODS).to include("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe ".request" do
|
|
13
|
+
subject(:response) { described_class.request(*meta) { |http, request| http.request(request) } }
|
|
14
|
+
|
|
15
|
+
let(:meta) { [:get, "http://example.com"] }
|
|
16
|
+
let(:result) { { status: 201, body: "Created" } }
|
|
17
|
+
let(:arguments) { { body: {} } }
|
|
18
|
+
|
|
19
|
+
before { stub_request(*meta).to_return(**result) }
|
|
20
|
+
|
|
21
|
+
it { expect(Yobi::Http).to respond_to(:request) }
|
|
22
|
+
|
|
23
|
+
context "when making a GET request" do
|
|
24
|
+
let(:result) { { status: 200, body: "OK" } }
|
|
25
|
+
let(:arguments) { { body: nil } }
|
|
26
|
+
|
|
27
|
+
it { expect(response.code).to eq("200") }
|
|
28
|
+
it { expect(response.body).to eq("OK") }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context "when making a POST request with data" do
|
|
32
|
+
let(:meta) { [:post, "http://example.com"] }
|
|
33
|
+
let(:result) { { status: 201, body: "Created" } }
|
|
34
|
+
let(:arguments) { { body: { "name" => "John" }.to_json } }
|
|
35
|
+
|
|
36
|
+
it { expect(response.code).to eq("201") }
|
|
37
|
+
it { expect(response.body).to eq("Created") }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/spec/yobi_spec.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Yobi do
|
|
6
|
+
describe "VERSION" do
|
|
7
|
+
it { expect(Yobi.constants).to include(:VERSION) }
|
|
8
|
+
|
|
9
|
+
it "follows semantic versioning format" do
|
|
10
|
+
expect(Yobi::VERSION).to match(/^\d+\.\d+\.\d+$/)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe ".name" do
|
|
15
|
+
it { expect(Yobi).to respond_to(:name) }
|
|
16
|
+
it { expect(Yobi.name).to eq("yobi") }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe ".description" do
|
|
20
|
+
it { expect(Yobi).to respond_to(:description) }
|
|
21
|
+
it { expect(Yobi.description).to eq("A simple HTTP client for testing APIs from the command line.") }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe ".view" do
|
|
25
|
+
it { expect(Yobi).to respond_to(:view) }
|
|
26
|
+
|
|
27
|
+
it "loads a template by name" do
|
|
28
|
+
expect(Yobi.view(:output).src).to include("HTTP/1.1")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "raises an error if the template does not exist" do
|
|
32
|
+
expect { Yobi.view("nonexistent") }.to raise_error(Errno::ENOENT)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe ".args" do
|
|
37
|
+
it { expect(Yobi).to respond_to(:args) }
|
|
38
|
+
it { expect(Yobi.args).to be(Yobi::CLI::Arguments) }
|
|
39
|
+
end
|
|
40
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yobi-http
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Vinciguerra
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-02-
|
|
10
|
+
date: 2026-02-22 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.3'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: tty-markdown
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -32,6 +46,7 @@ executables:
|
|
|
32
46
|
extensions: []
|
|
33
47
|
extra_rdoc_files: []
|
|
34
48
|
files:
|
|
49
|
+
- ".rspec"
|
|
35
50
|
- Agents.md
|
|
36
51
|
- CHANGELOG.md
|
|
37
52
|
- CODE_OF_CONDUCT.md
|
|
@@ -40,7 +55,20 @@ files:
|
|
|
40
55
|
- exe/yobi
|
|
41
56
|
- lib/views/output.md.erb
|
|
42
57
|
- lib/yobi.rb
|
|
58
|
+
- lib/yobi/cli.rb
|
|
59
|
+
- lib/yobi/cli/arguments.rb
|
|
60
|
+
- lib/yobi/extensions.rb
|
|
61
|
+
- lib/yobi/extensions/net_http.rb
|
|
62
|
+
- lib/yobi/extensions/net_http/parsing.rb
|
|
43
63
|
- lib/yobi/http.rb
|
|
64
|
+
- lib/yobi/version.rb
|
|
65
|
+
- screenshot.png
|
|
66
|
+
- spec/extensions/net_http/parsing_spec.rb
|
|
67
|
+
- spec/spec_helper.rb
|
|
68
|
+
- spec/support/webmock.rb
|
|
69
|
+
- spec/yobi/cli/arguments_spec.rb
|
|
70
|
+
- spec/yobi/http_spec.rb
|
|
71
|
+
- spec/yobi_spec.rb
|
|
44
72
|
homepage: https://github.com/dvinciguerra/yobi-http
|
|
45
73
|
licenses: []
|
|
46
74
|
metadata:
|