yobi-http 0.6.0 → 0.8.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/exe/yobi +45 -78
- 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/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 +29 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2cd8930bcfe4c6abc945f290f82f4dbca27cbbbe7fb18592e76b0a78f7541325
|
|
4
|
+
data.tar.gz: dddad8c7efc3aac7d4aaa5a95a67585ee62896f2721bd017fc4f15f8f348761a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32b78e79f26f6e9a1c58165fc0cf50c1d299505a35135cb592c5a4c8e1a1355a16c483cb4486cada273623b1d7f56821a719326d33e536216cd73310abce8057
|
|
7
|
+
data.tar.gz: eded11c1ac57fc5cd10c029e294465891ec352fbfa332c7de84cef47a1e3a9024742deb60a52ffbcd6f705699c3ea552ad668de40f7ffb1c230d12fcd2d82b65
|
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--require ./spec/spec_helper
|
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"
|
|
@@ -77,106 +76,74 @@ end
|
|
|
77
76
|
parser.parse!(ARGV)
|
|
78
77
|
|
|
79
78
|
# resolve the HTTP method
|
|
80
|
-
method = Yobi
|
|
79
|
+
method = Yobi.args.http_method?(ARGV[0]) ? ARGV.shift : "GET"
|
|
81
80
|
|
|
82
81
|
# 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) })
|
|
82
|
+
url = Yobi.args.url(ARGV.shift)
|
|
83
|
+
|
|
84
|
+
data = Yobi.args.parse_data(ARGV)
|
|
85
|
+
headers = Yobi.args.parse_headers(ARGV)
|
|
98
86
|
|
|
99
87
|
# 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
|
|
88
|
+
Yobi.args.auth_header(headers, @options) if @options[:auth]
|
|
111
89
|
|
|
112
90
|
pp [method, url, data, headers, @options, ARGV] if ENV["YOBI_DEBUG"]
|
|
113
91
|
|
|
114
|
-
|
|
92
|
+
# Extend Net::HTTPResponse to add util behaviors
|
|
93
|
+
def offline_mode(request, options)
|
|
115
94
|
Net::HTTP.class_eval do
|
|
116
95
|
def connect; end
|
|
117
96
|
end
|
|
97
|
+
|
|
98
|
+
options[:verbose] = true
|
|
99
|
+
|
|
100
|
+
response = Net::HTTPResponse.new("1.1", "200", "OK")
|
|
101
|
+
response["Content-Type"] = "application/json"
|
|
102
|
+
response["Access-Control-Allow-Credentials"] = true
|
|
103
|
+
response["Access-Control-Allow-Origin"] = "*"
|
|
104
|
+
response["Connection"] = "close"
|
|
105
|
+
response["Date"] = Time.now.httpdate
|
|
106
|
+
response["Server"] = "yobi-offline/#{Yobi::VERSION}"
|
|
107
|
+
response["X-Powered-By"] = "Yobi/#{Yobi::VERSION}"
|
|
108
|
+
response.body = body = JSON.pretty_generate({ message: "Offline mode enabled" })
|
|
109
|
+
|
|
110
|
+
view = Yobi.view(:output)
|
|
111
|
+
puts TTY::Markdown.parse(view.result(binding), color: :always)
|
|
112
|
+
|
|
113
|
+
exit 0
|
|
118
114
|
end
|
|
119
115
|
|
|
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
|
|
116
|
+
def raw_mode(_request, response, options)
|
|
117
|
+
if options[:print].include?("H")
|
|
118
|
+
puts "HTTP/#{response.http_version} #{response.code} #{response.message}"
|
|
119
|
+
response.each_header { |key, value| puts "#{key}: #{value}" }
|
|
120
|
+
puts
|
|
147
121
|
end
|
|
148
122
|
|
|
149
|
-
|
|
123
|
+
body = response.parsed_body
|
|
124
|
+
puts body if options[:print].include?("B") && body
|
|
150
125
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
begin
|
|
154
|
-
body ||= JSON.pretty_generate(JSON.parse(response.body))
|
|
155
|
-
rescue StandardError
|
|
156
|
-
body ||= response.body
|
|
157
|
-
end
|
|
158
|
-
else
|
|
159
|
-
body ||= response.body
|
|
160
|
-
end
|
|
126
|
+
exit 0
|
|
127
|
+
end
|
|
161
128
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
puts "HTTP/#{response.http_version} #{response.code} #{response.message}"
|
|
165
|
-
response.each_header { |key, value| puts "#{key}: #{value}" }
|
|
166
|
-
puts
|
|
167
|
-
end
|
|
129
|
+
Yobi.request(method, url, data: data, headers: headers, options: @options) do |http, request|
|
|
130
|
+
options = @options
|
|
168
131
|
|
|
169
|
-
|
|
132
|
+
offline_mode(request, options) if options[:offline]
|
|
170
133
|
|
|
171
|
-
|
|
172
|
-
|
|
134
|
+
response = http.request(request)
|
|
135
|
+
|
|
136
|
+
raw_mode(request, response, options) if options[:raw]
|
|
137
|
+
|
|
138
|
+
body = JSON.pretty_generate(response.parsed_body)
|
|
173
139
|
|
|
174
140
|
view = Yobi.view(:output)
|
|
141
|
+
output_result = view.result(binding)
|
|
175
142
|
|
|
176
143
|
if options[:output]
|
|
177
144
|
file = File.expand_path(options[:output], Dir.pwd)
|
|
178
|
-
File.write(file,
|
|
145
|
+
File.write(file, output_result, mode: "w")
|
|
179
146
|
else
|
|
180
|
-
puts TTY::Markdown.parse(
|
|
147
|
+
puts TTY::Markdown.parse(output_result, color: :always)
|
|
181
148
|
end
|
|
182
149
|
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
|
|
@@ -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.8.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-20 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,19 @@ 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
|
+
- spec/extensions/net_http/parsing_spec.rb
|
|
66
|
+
- spec/spec_helper.rb
|
|
67
|
+
- spec/support/webmock.rb
|
|
68
|
+
- spec/yobi/cli/arguments_spec.rb
|
|
69
|
+
- spec/yobi/http_spec.rb
|
|
70
|
+
- spec/yobi_spec.rb
|
|
44
71
|
homepage: https://github.com/dvinciguerra/yobi-http
|
|
45
72
|
licenses: []
|
|
46
73
|
metadata:
|