yobi-http 0.10.0 → 0.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56563ef37ac915d9ea34678799ac53ae2f8d91355dfd383e47929ea17e0814b0
4
- data.tar.gz: cc47d7c2010c5177685cd75462e4c2c387befa2d31d117acaf5e99f23dc91e59
3
+ metadata.gz: 713eaa43f4787691ddcf301d17652886a9f1141cd92600924a0b4737b87f0637
4
+ data.tar.gz: ec5199666420337034a8f2d17a011dbdb0c5acde3851f2b533ef333c049b9c3a
5
5
  SHA512:
6
- metadata.gz: 90392c2e87b514c76c3f5b1eaa1049140f38ae2b0c87937379f2f767ba6fff1809b2143433479e99f18a5cb550165aa3003860b173247ddac08ab124a448423e
7
- data.tar.gz: 63f13abd60c7e4580d504e2e5cf6b39fa919708309e21dc2371bc78750a7b7f52d9635c2a07561cb043fb5181b66975dde63d3a793e5e29563ab1d2fb8559aa8
6
+ metadata.gz: 8cb41090b4a1b2c2b89795d86d22ec593395429f50a89f9655857ab7ab2cd46b9e03bec8b6843bdc492c625614c6f9a8976566cebe0b77deb06bb6a584fe062b
7
+ data.tar.gz: d1a04f1b46bda0d9786d151477d76c25bd981b60c9c6a2a0e80a39a46b05a64d45da695578d39774fecabdbb6fd3726fb87a7c45fd5ccf231e55becd73b9a617
data/Examples.md ADDED
@@ -0,0 +1,72 @@
1
+
2
+ # Yobi HTTP - Usage examples
3
+
4
+ Some examples of using the
5
+
6
+ Some examples of using the Yobi HTTP CLI.
7
+
8
+ ## Simple HTTP Get request
9
+
10
+ ```bash
11
+ yobi --debug https://httpbin.org/get
12
+ ```
13
+
14
+ ## HTTP POST request with JSON body
15
+
16
+ ```bash
17
+ yobi --debug POST https://httpbin.org/post name=Yobi type="HTTP client"
18
+ ```
19
+
20
+ ## WIP: HTTP POST request with form data
21
+
22
+ ```bash
23
+ yobi --debug POST https://httpbin.org/post name=Yobi type="HTTP client" --form
24
+ ```
25
+
26
+ ## HTTP request with query parameters
27
+
28
+ ```bash
29
+ yobi --debug https://httpbin.org/get name=Yobi type="HTTP client"
30
+ ```
31
+ ## HTTP request with basic authentication
32
+
33
+ ```bash
34
+ yobi --debug https://httpbin.org/basic-auth/user/passwd --auth user:passwd --auth-type basic
35
+ ```
36
+
37
+ ## HTTP request with timeout
38
+
39
+ ```bash
40
+ yobi --debug --timeout 5 https://httpbin.org/delay/10
41
+ ```
42
+
43
+ ## HTTP request with custom headers
44
+
45
+ ```bash
46
+ yobi --debug https://httpbin.org/headers X-Custom-Header:Yobi User-Agent:"Yobi HTTP Client"
47
+ ```
48
+
49
+ ## Download a file
50
+
51
+ ```bash
52
+ yobi --debug --download https://ash-speed.hetzner.com/100MB.bin -o sample.bin
53
+ ```
54
+
55
+ ## HTTP request with follow redirects
56
+
57
+ ```bash
58
+ yobi --debug --follow https://httpbin.org/redirect/3
59
+ ```
60
+
61
+ ## HTTP request with output to a file
62
+
63
+ ```bash
64
+ yobi -p B https://httpbin.org/get -o response.json
65
+ ```
66
+
67
+ ## HTTP request with verbose output
68
+
69
+ ```bash
70
+ yobi --debug --verbose https://httpbin.org/get
71
+ ```
72
+
data/exe/yobi CHANGED
@@ -15,7 +15,8 @@ require "yobi"
15
15
 
16
16
  # parsing arguments
17
17
  @options = {
18
- print: "HB", auth: nil, auth_type: "basic", verbose: false, raw: false, offline: false, follow: false, debug: false
18
+ print: "HB", auth: nil, auth_type: "basic", verbose: false, raw: false, offline: false, follow: false, debug: false,
19
+ timeout: nil, download: false
19
20
  }
20
21
 
21
22
  parser = OptionParser.new do |opts|
@@ -36,8 +37,8 @@ parser = OptionParser.new do |opts|
36
37
  )
37
38
 
38
39
  opts.on("-p", "--print FORMAT", "Specify the output format (e.g., 'H' for headers, 'B' for body)") do |format|
39
- @options[:print] = format.upcase || "HB" # default to printing both headers and body
40
- warn "[Argument Error] Unsupported format: #{format}" unless format.match?(/\A[HB]+\z/i)
40
+ @options[:print] = format || "HB" # default to printing both headers and body
41
+ warn "[Argument Error] Unsupported format: #{format}" unless format.match?(/\A[HBhb]+\z/)
41
42
  end
42
43
 
43
44
  opts.on("-a", "--auth USER:PASS", "Specify basic authentication credentials") do |auth|
@@ -69,9 +70,16 @@ parser = OptionParser.new do |opts|
69
70
  @options[:output] = file
70
71
  end
71
72
 
73
+ opts.on("--timeout SECONDS", "Set a custom timeout for the request (integer or float)") do |timeout|
74
+ @options[:timeout] = timeout.to_f
75
+ end
76
+
77
+ opts.on("--download", "Download response body to a file specified by --output") do
78
+ @options[:download] = true
79
+ end
80
+
72
81
  opts.on("-v", "--verbose", "Print detailed request and response information") do
73
82
  @options[:verbose] = true
74
-
75
83
  end
76
84
 
77
85
  opts.on("--debug", "Print debug information") do
@@ -99,81 +107,25 @@ headers = Yobi.args.parse_headers(ARGV)
99
107
  # prepare authentication header if auth is provided
100
108
  Yobi.args.auth_header(headers, @options) if @options[:auth]
101
109
 
102
- pp [method, url, data, headers, @options, ARGV] if ENV["YOBI_DEBUG"]
103
-
104
- # Extend Net::HTTPResponse to add util behaviors
105
- def offline_mode(request, options)
106
- Net::HTTP.class_eval do
107
- def connect; end
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
126
- end
127
-
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
133
- end
134
-
135
- body = response.parsed_body
136
- puts body if options[:print].include?("B") && body
137
-
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)
151
- end
152
- end
153
-
154
- Yobi.request(method, url, data: data, headers: headers, options: @options) do |http, request|
110
+ Yobi::Http.request(method, url, data: data, headers: headers, options: @options) do |http, request|
155
111
  options = @options
156
112
 
157
- offline_mode(request, options) if options[:offline]
158
-
113
+ # follow redirects, offline, download or standard request mode
159
114
  response =
160
115
  if options[:follow]
161
- follow_redirects(http.request(request), url, method, data, headers, options)
116
+ Yobi::Http.follow_redirects(http.request(request), url, method, data, headers, options)
117
+ elsif options[:offline]
118
+ Yobi::Http.offline_mode(request, options)
119
+ elsif options[:download]
120
+ Yobi::Http.download(request, http, options)
162
121
  else
163
122
  http.request(request)
164
123
  end
165
124
 
166
- raw_mode(request, response, options) if options[:raw]
167
-
168
- body = JSON.pretty_generate(response.parsed_body)
169
-
170
- view = Yobi.view(:output)
171
- output_result = view.result(binding)
172
-
173
- if options[:output]
174
- file = File.expand_path(options[:output], Dir.pwd)
175
- File.write(file, output_result, mode: "w")
125
+ # render output
126
+ if options[:raw]
127
+ Yobi::Renders::Raw.render(request, response, options)
176
128
  else
177
- puts TTY::Markdown.parse(output_result, color: :always)
129
+ Yobi::Renders::Colored.render(request, response, options)
178
130
  end
179
131
  end
data/lib/yobi/http.rb CHANGED
@@ -8,7 +8,7 @@ module Yobi
8
8
  METHODS = %w[GET POST PUT DELETE PATCH HEAD OPTIONS].freeze
9
9
 
10
10
  class << self
11
- # rubocop:disable Metrics/AbcSize
11
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
12
12
  def request(method, url, data: {}, headers: {}, options: {})
13
13
  @uri = URI(url)
14
14
  @options = options
@@ -18,14 +18,82 @@ module Yobi
18
18
  request_class = Net::HTTP.const_get(@method)
19
19
  request = request_class.new(@uri)
20
20
 
21
+ if @options[:timeout]
22
+ http.open_timeout = @options[:timeout]
23
+ http.read_timeout = @options[:timeout]
24
+ end
25
+
21
26
  headers.each { |key, value| request[key] = value }
22
27
 
23
28
  request.body = data.to_json unless data.empty?
24
29
 
25
30
  yield(http, request) if block_given?
31
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
32
+ warn "Request timed out: #{e.message}"
33
+ exit 1
34
+ end
35
+ end
36
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
37
+
38
+ # rubocop:disable Metrics/ParameterLists
39
+ def follow_redirects(response, url, method, data, headers, options)
40
+ return response unless response.is_a?(Net::HTTPRedirection)
41
+
42
+ location = response["location"]
43
+ warn "Redirected to #{location}" if options[:debug]
44
+ new_url = URI.join(url, location).to_s
45
+
46
+ request(method, new_url, data: data, headers: headers, options: options) do |new_http, new_request|
47
+ response = new_http.request(new_request)
48
+ return follow_redirects(response, new_url, method, data, headers, options)
26
49
  end
27
50
  end
28
- # rubocop:enable Metrics/AbcSize
51
+ # rubocop:enable Metrics/ParameterLists
52
+
53
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
54
+ def offline_mode(_request, options)
55
+ Net::HTTP.class_eval do
56
+ def connect; end
57
+ end
58
+
59
+ options[:verbose] = true
60
+
61
+ Net::HTTPResponse.new("1.1", "200", "OK").tap do |response|
62
+ response["Content-Type"] = "application/json"
63
+ response["Access-Control-Allow-Credentials"] = true
64
+ response["Access-Control-Allow-Origin"] = "*"
65
+ response["Connection"] = "close"
66
+ response["Date"] = Time.now.httpdate
67
+ response["Server"] = "yobi-offline/#{Yobi::VERSION}"
68
+ response["X-Powered-By"] = "Yobi/#{Yobi::VERSION}"
69
+
70
+ response.body = JSON.pretty_generate({ message: "Offline mode enabled" })
71
+ response.instance_variable_set(:@read, true)
72
+ end
73
+ end
74
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
75
+
76
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
77
+ def download(request, http, options)
78
+ http.request(request) do |response|
79
+ url = request.uri.to_s
80
+ total_bytes = response["Content-Length"]&.to_i
81
+ progress = Yobi::UI::Progress.new(total_bytes)
82
+
83
+ filename = options[:output] || File.basename(URI.parse(url).path)
84
+ File.open(filename, "wb") do |file|
85
+ response.read_body do |chunk|
86
+ file.write(chunk)
87
+ progress.increment(chunk.size)
88
+ end
89
+ end
90
+
91
+ puts "\nDownload finished: #{filename}"
92
+ end
93
+
94
+ exit 0
95
+ end
96
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
29
97
  end
30
98
  end
31
99
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yobi
4
+ module Renders
5
+ # Render a colored output of the request and response
6
+ module Colored
7
+ class << self
8
+ def compile(request, response, options = {})
9
+ body =
10
+ JSON.pretty_generate(response.parsed_body) rescue response.body # rubocop:disable Style/RescueModifier
11
+
12
+ view = Yobi.view(:output)
13
+ TTY::Markdown.parse(view.result(binding), color: :always)
14
+ end
15
+
16
+ def render(request, response, options = {})
17
+ view = compile(request, response, options)
18
+
19
+ if options[:output]
20
+ file_path = ::File.expand_path(options[:output], Dir.pwd)
21
+ ::File.write(file_path, view, mode: "wb")
22
+ else
23
+ puts view
24
+ end
25
+
26
+ exit 0
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yobi
4
+ module Renders
5
+ # Render a raw output of the request and response
6
+ module Raw
7
+ class << self
8
+ def compile(request, response, options = {})
9
+ arguments request: request, response: response, options: options
10
+
11
+ buffer.tap do
12
+ compile_request
13
+ compile_response
14
+ end
15
+ end
16
+
17
+ def render(request, response, options = {})
18
+ view = compile(request, response, options)
19
+
20
+ if options[:output]
21
+ file_path = ::File.expand_path(options[:output], Dir.pwd)
22
+ ::File.write(file_path, view, mode: "wb")
23
+ else
24
+ puts view
25
+ end
26
+
27
+ exit 0
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :request, :response, :options
33
+
34
+ def buffer
35
+ @buffer ||= String.new
36
+ end
37
+
38
+ def arguments(**args)
39
+ @request = args[:request]
40
+ @response = args[:response]
41
+ @options = args[:options] || {}
42
+ end
43
+
44
+ def show_header?(request_or_response)
45
+ options[:print].include?(request_or_response.is_a?(Net::HTTPResponse) ? "H" : "h")
46
+ end
47
+
48
+ def show_body?(request_or_response)
49
+ options[:print].include?(request_or_response.is_a?(Net::HTTPResponse) ? "B" : "b")
50
+ end
51
+
52
+ def compile_request
53
+ compile_request_header
54
+ compile_request_body
55
+
56
+ buffer << "\n" * 2 if show_header?(request) || show_body?(request)
57
+ end
58
+
59
+ def compile_request_header
60
+ return unless show_header? request
61
+
62
+ buffer << "#{request.method} #{request.path} HTTP/1.1 \n"
63
+ request.each_capitalized.sort.each do |key, value|
64
+ buffer << "#{key}: #{value}\n"
65
+ end
66
+ buffer << "\n"
67
+ end
68
+
69
+ def compile_request_body
70
+ return unless show_body?(request) && request.body && !request.body.empty?
71
+
72
+ buffer << "#{JSON.pretty_generate(request.parsed_body) rescue request.body}\n" # rubocop:disable Style/RescueModifier
73
+ end
74
+
75
+ def compile_response
76
+ compile_response_header
77
+ compile_response_body
78
+ end
79
+
80
+ def compile_response_header
81
+ return unless show_header? response
82
+
83
+ buffer << "HTTP/#{response.http_version} #{response.code} #{response.message}\n"
84
+ response.each_header { |key, value| buffer << "#{key}: #{value}\n" }
85
+ buffer << "\n"
86
+ end
87
+
88
+ def compile_response_body
89
+ return unless response.body && !response.body.empty? && options[:print].include?("B")
90
+
91
+ buffer << "#{JSON.pretty_generate(response.parsed_body)}\n"
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob(File.expand_path("renders/*.rb", __dir__)).sort.each do |path|
4
+ require path
5
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yobi
4
+ module UI
5
+ # Simple terminal progress bar
6
+ class Progress
7
+ BAR_WIDTH = 40
8
+
9
+ STYLE = {
10
+ arrow: { complete: "▸", incomplete: "▹", unknown: "◂▸" },
11
+ asterisk: { complete: "✱", incomplete: "✳", unknown: "✳✱✳" },
12
+ blade: { complete: "▰", incomplete: "▱", unknown: "▱▰▱" },
13
+ block: { complete: "█", incomplete: "░", unknown: "░█░" },
14
+ box: { complete: "■", incomplete: "□", unknown: "□■□" },
15
+ bracket: { complete: "❭", incomplete: " ", unknown: "❬=❭" },
16
+ burger: { complete: "≡", incomplete: " ", unknown: "<≡>" },
17
+ button: { complete: "⦿", incomplete: "⦾", unknown: "⦾⦿⦾" },
18
+ chevron: { complete: "›", incomplete: " ", unknown: "‹=›" },
19
+ circle: { complete: "●", incomplete: "○", unknown: "○●○" },
20
+ classic: { complete: "=", incomplete: " ", unknown: "<=>" },
21
+ crate: { complete: "▣", incomplete: "⬚", unknown: "⬚▣⬚" },
22
+ diamond: { complete: "♦", incomplete: "♢", unknown: "♢♦♢" },
23
+ dot: { complete: "・", incomplete: " ", unknown: "・・・" },
24
+ heart: { complete: "♥", incomplete: "♡", unknown: "♡♥♡" },
25
+ star: { complete: "★", incomplete: "☆", unknown: "☆★☆" }
26
+ }.freeze
27
+
28
+ def initialize(total_bytes = nil, options = { style: :block })
29
+ @total = total_bytes
30
+ @downloaded = 0
31
+ @last_draw = Time.now
32
+ @options = options
33
+ end
34
+
35
+ def increment(bytes)
36
+ @downloaded += bytes
37
+ current_time = Time.now
38
+
39
+ return unless current_time - @last_draw >= 0.1 || finished?
40
+
41
+ draw
42
+ @last_draw = current_time
43
+ end
44
+
45
+ def finished?
46
+ @total && @downloaded >= @total
47
+ end
48
+
49
+ def draw
50
+ if @total
51
+ percent = @downloaded.to_f / @total
52
+ filled = (percent * BAR_WIDTH).round
53
+ bar = theme[:complete] * filled + theme[:incomplete] * (BAR_WIDTH - filled)
54
+ print "\r[#{bar}] #{(percent * 100).round(1)}% "\
55
+ "(#{human(@downloaded)}/#{human(@total)})"
56
+ else
57
+ print "\r#{human(@downloaded)} downloaded"
58
+ end
59
+
60
+ $stdout.flush
61
+ end
62
+
63
+ private
64
+
65
+ def theme
66
+ STYLE[@options[:style]] || STYLE[:box]
67
+ end
68
+
69
+ def human(bytes)
70
+ if bytes > 1_048_576
71
+ format("%.2f MiB", bytes / 1_048_576.0)
72
+ elsif bytes > 1024
73
+ format("%.2f KiB", bytes / 1024.0)
74
+ else
75
+ "#{bytes} B"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
data/lib/yobi/ui.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob(File.expand_path("ui/*.rb", __dir__)).sort.each do |path|
4
+ require path
5
+ end
data/lib/yobi/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Yobi
4
4
  # Yobi gem version
5
- VERSION = "0.10.0"
5
+ VERSION = "0.12.0"
6
6
  end
data/lib/yobi.rb CHANGED
@@ -5,6 +5,8 @@ require "erb"
5
5
  require "yobi/cli"
6
6
  require "yobi/extensions"
7
7
  require "yobi/http"
8
+ require "yobi/ui"
9
+ require "yobi/renders"
8
10
  require "yobi/version"
9
11
 
10
12
  # Yobi Http CLI client namespace
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yobi-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Vinciguerra
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-02-22 00:00:00.000000000 Z
10
+ date: 2026-02-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -50,6 +50,7 @@ files:
50
50
  - Agents.md
51
51
  - CHANGELOG.md
52
52
  - CODE_OF_CONDUCT.md
53
+ - Examples.md
53
54
  - README.md
54
55
  - Rakefile
55
56
  - exe/yobi
@@ -61,6 +62,11 @@ files:
61
62
  - lib/yobi/extensions/net_http.rb
62
63
  - lib/yobi/extensions/net_http/parsing.rb
63
64
  - lib/yobi/http.rb
65
+ - lib/yobi/renders.rb
66
+ - lib/yobi/renders/colored.rb
67
+ - lib/yobi/renders/raw.rb
68
+ - lib/yobi/ui.rb
69
+ - lib/yobi/ui/progress.rb
64
70
  - lib/yobi/version.rb
65
71
  - screenshot.png
66
72
  - spec/extensions/net_http/parsing_spec.rb