net-hippie 1.3.0 → 1.5.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: 998433769957af723da2f4803acd93083a124bbefb5af0bf95a5083a0afae59c
4
- data.tar.gz: 45b2708548b30020c6a74f269669545ed3355d4c567a914763313dd516f35afd
3
+ metadata.gz: f1f315d747c0e48d92a0a25bb9395fe80f472757782d8341b43f833462f5338e
4
+ data.tar.gz: d71b266d4e60f3ca16f6eed02c0ac2d42b10ab6c60a445a2c349351b75bad4f8
5
5
  SHA512:
6
- metadata.gz: bd3c51b47e5bc632c105daf5aa589cc1df70952ea2b7baa05c699b63e5a54b74054f19d4d42f2715d9d0339a7b28a55e17714e087fa6a385689b158426f6f0bf
7
- data.tar.gz: b7374b67ac0bcf7f93a0ac9715e5fe8cd517e614dcdd06cdf924bb61ca76c4f222b802e9b3cf3dd5a735c78ac5f076f3db13128ef59c510e826c1748d19c72c0
6
+ metadata.gz: 524460bd6fd5a982db5025418ebb9449cfee70876df3338230f06eb97fdf2bedae4c591d987b3c06fca7f363355d6acf96ceecaf9c3299915eacecd578a5a5d6
7
+ data.tar.gz: 7b885d2bcdb7298bad431ad2fdf51a67b7fd356caa0cc01ff8557cbf85bc7e9eb575e655e95789f53795a5f8acd16c3b98c25b2d831ec9410a5a8d5e03aa6c8d
data/.rubocop.yml CHANGED
@@ -1,3 +1,7 @@
1
+ plugins:
2
+ - rubocop-minitest
3
+ - rubocop-rake
4
+
1
5
  AllCops:
2
6
  Exclude:
3
7
  - 'coverage/**/*'
@@ -6,7 +10,7 @@ AllCops:
6
10
  - 'tmp/**/*'
7
11
  - 'vendor/**/*'
8
12
  NewCops: enable
9
- TargetRubyVersion: 3.2
13
+ TargetRubyVersion: 4.0
10
14
 
11
15
  Gemspec/DevelopmentDependencies:
12
16
  EnforcedStyle: gemspec
data/CHANGELOG.md CHANGED
@@ -1,5 +1,3 @@
1
- Version 1.3.0
2
-
3
1
  # Changelog
4
2
  All notable changes to this project will be documented in this file.
5
3
 
@@ -8,6 +6,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
6
 
9
7
  ## [Unreleased]
10
8
 
9
+ ## [1.5.0] - 2026-02-01
10
+
11
+ ### Changed
12
+
13
+ - Prefer IPv4 addresses in DNS resolution to avoid connectivity issues
14
+ - Increase default `keep_alive_timeout` to 30 seconds for better connection reuse
15
+ - Set TLS `min_version` to TLS 1.2 by default for improved security
16
+ - Improve retry logging format with structured error details
17
+
18
+ ### Added
19
+
20
+ - Add thread-safe connection pooling with LRU eviction
21
+ - Add DNS pre-resolution with configurable timeout to prevent indefinite hangs
22
+ - Add DNS caching with TTL support
23
+ - Add persistent HTTP sessions to avoid `Connection: close` overhead
24
+ - Add `head` and `options` HTTP methods
25
+ - Add `close_all` method to release all pooled connections
26
+ - Add `reset_default_client!` method to reset the default client
27
+ - Add configuration options: `keep_alive_timeout`, `max_retries`, `min_version`, `continue_timeout`, `ignore_eof`, `max_connections`, `dns_timeout`, `dns_ttl`
28
+ - Parse TLS certificates once at initialization for performance
29
+
30
+ ### Fixed
31
+
32
+ - Fix connection leak when racing threads create duplicate connections
33
+
34
+ ## [1.4.0] - 2025-10-08
35
+ ### Added
36
+ - Streaming response support via block parameter
37
+ - Backward compatible with existing block API (arity-based detection)
38
+
11
39
  ## [1.3.0] - 2025-04-30
12
40
  ### Changed
13
41
  - Ruby 2.3+ required
@@ -95,30 +123,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
95
123
  - with\_retry.
96
124
  - authorization header helpers
97
125
 
98
- [Unreleased]: https://github.com/xlgmokha/net-hippie/compare/v1.3.0...HEAD
99
- [1.3.0]: https://github.com/xlgmokha/net-hippie/compare/v1.2.0...v1.3.0
100
- [1.2.0]: https://github.com/xlgmokha/net-hippie/compare/v1.1.1...v1.2.0
101
- [1.1.1]: https://github.com/xlgmokha/net-hippie/compare/v1.1.0...v1.1.1
102
- [1.1.0]: https://github.com/xlgmokha/net-hippie/compare/v1.0.1...v1.1.0
103
- [1.0.1]: https://github.com/xlgmokha/net-hippie/compare/v1.0.0...v1.0.1
104
- [1.0.0]: https://github.com/xlgmokha/net-hippie/compare/v0.3.2...v1.0.0
105
- [0.3.2]: https://github.com/xlgmokha/net-hippie/compare/v0.3.1...v0.3.2
106
- [0.3.1]: https://github.com/xlgmokha/net-hippie/compare/v0.3.0...v0.3.1
107
- [0.3.0]: https://github.com/xlgmokha/net-hippie/compare/v0.2.7...v0.3.0
108
- [0.2.7]: https://github.com/xlgmokha/net-hippie/compare/v0.2.6...v0.2.7
109
- [0.2.6]: https://github.com/xlgmokha/net-hippie/compare/v0.2.5...v0.2.6
110
- [0.2.5]: https://github.com/xlgmokha/net-hippie/compare/v0.2.4...v0.2.5
111
- [0.2.4]: https://github.com/xlgmokha/net-hippie/compare/v0.2.3...v0.2.4
112
- [0.2.3]: https://github.com/xlgmokha/net-hippie/compare/v0.2.2...v0.2.3
113
- [0.2.2]: https://github.com/xlgmokha/net-hippie/compare/v0.2.1...v0.2.2
114
- [0.2.1]: https://github.com/xlgmokha/net-hippie/compare/v0.2.0...v0.2.1
115
- [0.2.0]: https://github.com/xlgmokha/net-hippie/compare/v0.1.9...v0.2.0
116
- [0.1.9]: https://github.com/xlgmokha/net-hippie/compare/v0.1.8...v0.1.9
117
- [0.1.8]: https://github.com/xlgmokha/net-hippie/compare/v0.1.7...v0.1.8
118
- [0.1.7]: https://github.com/xlgmokha/net-hippie/compare/v0.1.6...v0.1.7
119
- [0.1.6]: https://github.com/xlgmokha/net-hippie/compare/v0.1.5...v0.1.6
120
- [0.1.5]: https://github.com/xlgmokha/net-hippie/compare/v0.1.4...v0.1.5
121
- [0.1.4]: https://github.com/xlgmokha/net-hippie/compare/v0.1.3...v0.1.4
122
- [0.1.3]: https://github.com/xlgmokha/net-hippie/compare/v0.1.2...v0.1.3
123
- [0.1.2]: https://github.com/xlgmokha/net-hippie/compare/v0.1.1...v0.1.2
124
- [0.1.1]: https://github.com/xlgmokha/net-hippie/compare/v0.1.0...v0.1.1
126
+ [Unreleased]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v1.5.0...HEAD
127
+ [1.5.0]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v1.4.0...v1.5.0
128
+ [1.4.0]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v1.3.0...v1.4.0
129
+ [1.3.0]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v1.2.0...v1.3.0
130
+ [1.2.0]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v1.1.1...v1.2.0
131
+ [1.1.1]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v1.1.0...v1.1.1
132
+ [1.1.0]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v1.0.1...v1.1.0
133
+ [1.0.1]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v1.0.0...v1.0.1
134
+ [1.0.0]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.3.2...v1.0.0
135
+ [0.3.2]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.3.1...v0.3.2
136
+ [0.3.1]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.3.0...v0.3.1
137
+ [0.3.0]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.2.7...v0.3.0
138
+ [0.2.7]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.2.6...v0.2.7
139
+ [0.2.6]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.2.5...v0.2.6
140
+ [0.2.5]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.2.4...v0.2.5
141
+ [0.2.4]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.2.3...v0.2.4
142
+ [0.2.3]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.2.2...v0.2.3
143
+ [0.2.2]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.2.1...v0.2.2
144
+ [0.2.1]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.2.0...v0.2.1
145
+ [0.2.0]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.1.9...v0.2.0
146
+ [0.1.9]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.1.8...v0.1.9
147
+ [0.1.8]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.1.7...v0.1.8
148
+ [0.1.7]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.1.6...v0.1.7
149
+ [0.1.6]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.1.5...v0.1.6
150
+ [0.1.5]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.1.4...v0.1.5
151
+ [0.1.4]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.1.3...v0.1.4
152
+ [0.1.3]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.1.2...v0.1.3
153
+ [0.1.2]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.1.1...v0.1.2
154
+ [0.1.1]: https://src.mokhan.ca/xlgmokha/net-hippie/compare/v0.1.0...v0.1.1
data/README.md CHANGED
@@ -1,9 +1,5 @@
1
1
  # Net::Hippie
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/net-hippie.svg)](https://rubygems.org/gems/net-hippie)
4
- [![Build Status](https://github.com/xlgmokha/net-hippie/actions/workflows/ci.yml/badge.svg))](https://github.com/xlgmokha/net-hippie/actions)
5
-
6
-
7
3
  `Net::Hippie` is a light weight wrapper around `net/http` that defaults to
8
4
  sending `JSON` messages.
9
5
 
@@ -90,6 +86,40 @@ headers = { 'Authorization' => Net::Hippie.bearer_auth('token') }
90
86
  Net::Hippie.get('https://www.example.org', headers: headers)
91
87
  ```
92
88
 
89
+ ### Streaming Responses
90
+
91
+ Net::Hippie supports streaming responses by passing a block that accepts the response object:
92
+
93
+ ```ruby
94
+ Net::Hippie.post('https://api.example.com/stream', body: { prompt: 'Hello' }) do |response|
95
+ response.read_body do |chunk|
96
+ print chunk
97
+ end
98
+ end
99
+ ```
100
+
101
+ This is useful for Server-Sent Events (SSE) or other streaming APIs:
102
+
103
+ ```ruby
104
+ client = Net::Hippie::Client.new
105
+ client.post('https://api.openai.com/v1/chat/completions',
106
+ headers: {
107
+ 'Authorization' => Net::Hippie.bearer_auth(ENV['OPENAI_API_KEY']),
108
+ 'Content-Type' => 'application/json'
109
+ },
110
+ body: { model: 'gpt-4', messages: [{ role: 'user', content: 'Hi' }], stream: true }
111
+ ) do |response|
112
+ buffer = ""
113
+ response.read_body do |chunk|
114
+ buffer += chunk
115
+ while (line = buffer.slice!(/.*\n/))
116
+ next if line.strip.empty?
117
+ puts line if line.start_with?('data: ')
118
+ end
119
+ end
120
+ end
121
+ ```
122
+
93
123
  ## Development
94
124
 
95
125
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test` to run the tests.
@@ -102,7 +132,7 @@ push git commits and tags, and push the `.gem` file to [rubygems.org](https://ru
102
132
 
103
133
  ## Contributing
104
134
 
105
- Bug reports and pull requests are welcome on GitHub at https://github.com/xlgmokha/net-hippie.
135
+ Patches are welcome at https://src.mokhan.ca/xlgmokha/net-hippie.
106
136
 
107
137
  ## License
108
138
 
@@ -1,15 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'net/hippie/dns_cache'
4
+ require 'net/hippie/tls_parser'
5
+
3
6
  module Net
4
7
  module Hippie
5
- # A simple client for connecting with http resources.
8
+ # HTTP client with connection pooling, DNS caching, and retry logic.
6
9
  class Client
10
+ include TlsParser
11
+
7
12
  DEFAULT_HEADERS = {
8
13
  'Accept' => 'application/json',
9
14
  'Content-Type' => 'application/json',
10
- 'User-Agent' => "net/hippie #{Net::Hippie::VERSION}"
15
+ 'User-Agent' => "net/hippie #{VERSION}"
11
16
  }.freeze
12
17
 
18
+ JITTER = Random.new.freeze
19
+
13
20
  attr_reader :mapper, :logger, :follow_redirects
14
21
 
15
22
  def initialize(options = {})
@@ -17,92 +24,103 @@ module Net
17
24
  @mapper = options.fetch(:mapper, ContentTypeMapper.new)
18
25
  @logger = options.fetch(:logger, Net::Hippie.logger)
19
26
  @follow_redirects = options.fetch(:follow_redirects, 0)
20
- @default_headers = options.fetch(:headers, DEFAULT_HEADERS)
21
- @connections = Hash.new do |hash, key|
22
- scheme, host, port = key
23
- hash[key] = Connection.new(scheme, host, port, options)
24
- end
27
+ @default_headers = options.fetch(:headers, DEFAULT_HEADERS).freeze
28
+ configure_pool(options)
29
+ configure_tls(options)
25
30
  end
26
31
 
27
- def execute(uri, request, limit: follow_redirects, &block)
28
- connection = connection_for(uri)
29
- response = connection.run(request)
30
- if limit.positive? && response.is_a?(Net::HTTPRedirection)
31
- url = connection.build_url_for(response['location'])
32
- request = request_for(Net::HTTP::Get, url)
33
- execute(url, request, limit: limit - 1, &block)
34
- else
35
- block_given? ? yield(request, response) : response
32
+ %i[get post put patch delete].each do |method|
33
+ define_method(method) do |uri, headers: {}, body: {}, stream: false, &block|
34
+ run(uri, Net::HTTP.const_get(method.capitalize), headers, body, stream, &block)
36
35
  end
37
36
  end
38
37
 
39
- def get(uri, headers: {}, body: {}, &block)
40
- run(uri, Net::HTTP::Get, headers, body, &block)
41
- end
42
-
43
- def patch(uri, headers: {}, body: {}, &block)
44
- run(uri, Net::HTTP::Patch, headers, body, &block)
38
+ %i[head options].each do |method|
39
+ define_method(method) do |uri, headers: {}, &block|
40
+ run(uri, Net::HTTP.const_get(method.capitalize), headers, {}, false, &block)
41
+ end
45
42
  end
46
43
 
47
- def post(uri, headers: {}, body: {}, &block)
48
- run(uri, Net::HTTP::Post, headers, body, &block)
49
- end
44
+ def execute(uri, request, limit: follow_redirects, stream: false, &block)
45
+ conn = connection_for(uri)
46
+ return conn.run(request) { |res| res.read_body(&block) } if stream && block
50
47
 
51
- def put(uri, headers: {}, body: {}, &block)
52
- run(uri, Net::HTTP::Put, headers, body, &block)
53
- end
48
+ return execute_with_block(conn, request, &block) if block
54
49
 
55
- def delete(uri, headers: {}, body: {}, &block)
56
- run(uri, Net::HTTP::Delete, headers, body, &block)
50
+ response = conn.run(request)
51
+ limit.positive? && response.is_a?(Net::HTTPRedirection) ? follow_redirect(uri, response, limit) : response
57
52
  end
58
53
 
59
- # attempt 1 -> delay 0.1 second
60
- # attempt 2 -> delay 0.2 second
61
- # attempt 3 -> delay 0.4 second
62
- # attempt 4 -> delay 0.8 second
63
- # attempt 5 -> delay 1.6 second
64
- # attempt 6 -> delay 3.2 second
65
- # attempt 7 -> delay 6.4 second
66
- # attempt 8 -> delay 12.8 second
67
54
  def with_retry(retries: 3)
68
- retries = 0 if retries.nil? || retries.negative?
55
+ retries = [retries.to_i, 0].max
56
+ 0.upto(retries) do |attempt|
57
+ return yield self
58
+ rescue *CONNECTION_ERRORS => error
59
+ raise if attempt == retries
69
60
 
70
- 0.upto(retries) do |n|
71
- attempt(n, retries) do
72
- return yield self
73
- end
61
+ sleep_with_backoff(attempt, retries, error)
74
62
  end
75
63
  end
76
64
 
65
+ def close_all
66
+ @pool.close_all
67
+ @dns_cache.clear
68
+ end
69
+
77
70
  private
78
71
 
79
- attr_reader :default_headers
72
+ def configure_pool(options)
73
+ @dns_ttl = options.fetch(:dns_ttl, 300)
74
+ @dns_cache = DnsCache.new(
75
+ timeout: options.fetch(:dns_timeout, 5), ttl: @dns_ttl, logger: @logger
76
+ )
77
+ @pool = ConnectionPool.new(max_size: options.fetch(:max_connections, 100), dns_ttl: @dns_ttl)
78
+ end
80
79
 
81
- def attempt(attempt, max)
82
- yield
83
- rescue *CONNECTION_ERRORS => error
84
- raise error if attempt == max
80
+ def configure_tls(options)
81
+ @tls_cert = parse_cert(options[:certificate])
82
+ @tls_key = parse_key(options[:key], options[:passphrase])
83
+ @continue_timeout = options[:continue_timeout]
84
+ @ignore_eof = options.fetch(:ignore_eof, true)
85
+ end
85
86
 
86
- delay = ((2**attempt) * 0.1) + Random.rand(0.05) # delay + jitter
87
- logger&.warn("`#{error.message}` #{attempt + 1}/#{max} Delay: #{delay}s")
88
- sleep delay
87
+ def run(uri, method, headers, body, stream, &block)
88
+ uri = URI.parse(uri.to_s) unless uri.is_a?(URI::Generic)
89
+ execute(uri, build_request(method, uri, headers, body), stream: stream, &block)
89
90
  end
90
91
 
91
- def request_for(type, uri, headers: {}, body: {})
92
- final_headers = default_headers.merge(headers)
93
- type.new(URI.parse(uri.to_s), final_headers).tap do |x|
94
- x.body = mapper.map_from(final_headers, body) unless body.empty?
95
- end
92
+ def build_request(type, uri, headers, body)
93
+ merged = headers.empty? ? @default_headers : @default_headers.merge(headers)
94
+ path = uri.respond_to?(:request_uri) ? uri.request_uri : uri.path
95
+ type.new(path, merged).tap { |req| req.body = @mapper.map_from(merged, body) unless body.empty? }
96
96
  end
97
97
 
98
- def run(uri, http_method, headers, body, &block)
99
- request = request_for(http_method, uri, headers: headers, body: body)
100
- execute(uri, request, &block)
98
+ def execute_with_block(conn, request, &block)
99
+ block.arity == 2 ? yield(request, conn.run(request)) : conn.run(request, &block)
101
100
  end
102
101
 
103
102
  def connection_for(uri)
104
- uri = URI.parse(uri.to_s)
105
- @connections[[uri.scheme, uri.host, uri.port]]
103
+ uri = URI.parse(uri.to_s) unless uri.is_a?(URI::Generic)
104
+ key = [uri.scheme, uri.host, uri.port]
105
+ @pool.checkout(key) { Connection.new(uri.scheme, uri.host, uri.port, @dns_cache.resolve(uri.host), conn_opts) }
106
+ end
107
+
108
+ def sleep_with_backoff(attempt, max_retries, error)
109
+ delay = ((2**attempt) * 0.1) + JITTER.rand(0.05)
110
+ logger&.warn("[Hippie] #{error.class}: #{error.message} | Retry #{attempt + 1}/#{max_retries}")
111
+ sleep delay
112
+ end
113
+
114
+ def conn_opts
115
+ @options.merge(
116
+ tls_cert: @tls_cert, tls_key: @tls_key,
117
+ continue_timeout: @continue_timeout, ignore_eof: @ignore_eof
118
+ )
119
+ end
120
+
121
+ def follow_redirect(original_uri, response, limit)
122
+ redirect_uri = original_uri.merge(response['location'])
123
+ execute(redirect_uri, build_request(Net::HTTP::Get, redirect_uri, {}, {}), limit: limit - 1)
106
124
  end
107
125
  end
108
126
  end
@@ -2,21 +2,31 @@
2
2
 
3
3
  module Net
4
4
  module Hippie
5
- # A connection to a specific host
5
+ # Persistent HTTP connection with automatic reconnection.
6
6
  class Connection
7
- def initialize(scheme, host, port, options = {})
8
- http = Net::HTTP.new(host, port)
9
- http.read_timeout = options.fetch(:read_timeout, 10)
10
- http.open_timeout = options.fetch(:open_timeout, 10)
11
- http.use_ssl = scheme == 'https'
12
- http.verify_mode = options.fetch(:verify_mode, Net::Hippie.verify_mode)
13
- http.set_debug_output(options[:logger]) if options[:logger]
14
- apply_client_tls_to(http, options)
15
- @http = http
7
+ RETRYABLE_ERRORS = [EOFError, Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE, IOError].freeze
8
+
9
+ def initialize(scheme, host, port, ipaddr, options = {})
10
+ @mutex = Mutex.new
11
+ @created_at = Time.now
12
+ @http = build_http(scheme, host, port, ipaddr, options)
16
13
  end
17
14
 
18
- def run(request)
19
- @http.request(request)
15
+ def run(request, &block)
16
+ @mutex.synchronize do
17
+ ensure_started
18
+ execute(request, &block)
19
+ end
20
+ end
21
+
22
+ def stale?(ttl)
23
+ (Time.now - @created_at) > ttl
24
+ end
25
+
26
+ def close
27
+ @mutex.synchronize { @http.finish if @http.started? }
28
+ rescue IOError
29
+ nil
20
30
  end
21
31
 
22
32
  def build_url_for(path)
@@ -27,15 +37,51 @@ module Net
27
37
 
28
38
  private
29
39
 
30
- def apply_client_tls_to(http, options)
31
- return if options[:certificate].nil? || options[:key].nil?
40
+ def build_http(scheme, host, port, ipaddr, options)
41
+ Net::HTTP.new(host, port).tap do |http|
42
+ configure_timeouts(http, options)
43
+ configure_ssl(http, scheme, options)
44
+ configure_tls_client(http, options)
45
+ http.ipaddr = ipaddr if ipaddr
46
+ end
47
+ end
48
+
49
+ def configure_timeouts(http, options)
50
+ http.open_timeout = options.fetch(:open_timeout, 10)
51
+ http.read_timeout = options.fetch(:read_timeout, 10)
52
+ http.write_timeout = options.fetch(:write_timeout, 10)
53
+ http.keep_alive_timeout = options.fetch(:keep_alive_timeout, 30)
54
+ http.max_retries = options.fetch(:max_retries, 1)
55
+ http.continue_timeout = options[:continue_timeout] if options[:continue_timeout]
56
+ http.ignore_eof = options.fetch(:ignore_eof, true)
57
+ end
58
+
59
+ def configure_ssl(http, scheme, options)
60
+ http.use_ssl = scheme == 'https'
61
+ return unless http.use_ssl?
62
+
63
+ http.min_version = options.fetch(:min_version, :TLS1_2)
64
+ http.verify_mode = options.fetch(:verify_mode, Net::Hippie.verify_mode)
65
+ http.set_debug_output(options[:logger]) if options[:logger]
66
+ end
32
67
 
33
- http.cert = OpenSSL::X509::Certificate.new(options[:certificate])
34
- http.key = private_key(options[:key], options[:passphrase])
68
+ def configure_tls_client(http, options)
69
+ http.cert = options[:tls_cert] if options[:tls_cert]
70
+ http.key = options[:tls_key] if options[:tls_key]
35
71
  end
36
72
 
37
- def private_key(key, passphrase, type = OpenSSL::PKey::RSA)
38
- passphrase ? type.new(key, passphrase) : type.new(key)
73
+ def ensure_started
74
+ @http.start unless @http.started?
75
+ end
76
+
77
+ def execute(request, retried: false, &block)
78
+ block ? @http.request(request, &block) : @http.request(request)
79
+ rescue *RETRYABLE_ERRORS
80
+ raise if retried
81
+
82
+ @http.finish if @http.started?
83
+ @http.start
84
+ execute(request, retried: true, &block)
39
85
  end
40
86
  end
41
87
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Hippie
5
+ # Thread-safe connection pool with LRU eviction.
6
+ class ConnectionPool
7
+ def initialize(max_size: 100, dns_ttl: 300)
8
+ @max_size = max_size
9
+ @dns_ttl = dns_ttl
10
+ @connections = {}
11
+ @monitor = Monitor.new
12
+ end
13
+
14
+ def checkout(key, &block)
15
+ reuse(key) || create(key, &block)
16
+ end
17
+
18
+ def close_all
19
+ @monitor.synchronize do
20
+ @connections.each_value(&:close)
21
+ @connections.clear
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def reuse(key)
28
+ @monitor.synchronize do
29
+ return nil unless @connections.key?(key)
30
+
31
+ conn = @connections.delete(key)
32
+ return @connections[key] = conn unless conn.stale?(@dns_ttl)
33
+
34
+ conn.close
35
+ nil
36
+ end
37
+ end
38
+
39
+ def create(key)
40
+ conn = yield
41
+ @monitor.synchronize do
42
+ existing = reuse(key)
43
+ if existing
44
+ conn.close
45
+ return existing
46
+ end
47
+ evict_lru if @connections.size >= @max_size
48
+ @connections[key] = conn
49
+ end
50
+ end
51
+
52
+ def evict_lru
53
+ key, conn = @connections.first
54
+ conn.close
55
+ @connections.delete(key)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Hippie
5
+ # Thread-safe DNS resolution cache with TTL.
6
+ class DnsCache
7
+ def initialize(timeout: 5, ttl: 300, logger: nil)
8
+ @timeout = timeout
9
+ @ttl = ttl
10
+ @logger = logger
11
+ @cache = {}
12
+ @monitor = Monitor.new
13
+ end
14
+
15
+ def resolve(hostname)
16
+ cached = get(hostname)
17
+ return cached if cached
18
+
19
+ ip = Net::Hippie.resolve(hostname, timeout: @timeout)
20
+ set(hostname, ip)
21
+ ip
22
+ rescue Timeout::Error, Resolv::ResolvError => error
23
+ @logger&.warn("[Hippie] DNS resolution failed for #{hostname}: #{error.message}")
24
+ nil
25
+ end
26
+
27
+ def clear
28
+ @monitor.synchronize { @cache.clear }
29
+ end
30
+
31
+ private
32
+
33
+ def get(hostname)
34
+ @monitor.synchronize do
35
+ entry = @cache[hostname]
36
+ entry[:ip] if entry && (Time.now - entry[:time]) < @ttl
37
+ end
38
+ end
39
+
40
+ def set(hostname, ip_addr)
41
+ @monitor.synchronize { @cache[hostname] = { ip: ip_addr, time: Time.now } }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Hippie
5
+ # Parses TLS certificates and keys from various formats.
6
+ module TlsParser
7
+ def parse_cert(cert)
8
+ return cert if cert.is_a?(OpenSSL::X509::Certificate) || cert.nil?
9
+
10
+ OpenSSL::X509::Certificate.new(cert)
11
+ end
12
+
13
+ def parse_key(key, passphrase)
14
+ return key if key.is_a?(OpenSSL::PKey::PKey) || key.nil?
15
+
16
+ passphrase ? OpenSSL::PKey::RSA.new(key, passphrase) : OpenSSL::PKey::RSA.new(key)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Net
4
4
  module Hippie
5
- VERSION = '1.3.0'
5
+ VERSION = '1.5.0'
6
6
  end
7
7
  end
data/lib/net/hippie.rb CHANGED
@@ -3,69 +3,83 @@
3
3
  require 'base64'
4
4
  require 'json'
5
5
  require 'logger'
6
+ require 'monitor'
6
7
  require 'net/http'
7
8
  require 'openssl'
9
+ require 'resolv'
10
+ require 'timeout'
8
11
 
9
- require 'net/hippie/version'
10
12
  require 'net/hippie/client'
11
13
  require 'net/hippie/connection'
14
+ require 'net/hippie/connection_pool'
12
15
  require 'net/hippie/content_type_mapper'
16
+ require 'net/hippie/version'
13
17
 
14
18
  module Net
15
- # net/http for hippies.
19
+ # High-performance HTTP client with connection pooling and DNS caching.
16
20
  module Hippie
17
21
  CONNECTION_ERRORS = [
18
22
  EOFError,
19
23
  Errno::ECONNREFUSED,
20
24
  Errno::ECONNRESET,
21
- Errno::ECONNRESET,
22
25
  Errno::EHOSTUNREACH,
23
26
  Errno::EINVAL,
27
+ Errno::EPIPE,
28
+ Errno::ECONNABORTED,
29
+ Errno::ETIMEDOUT,
30
+ IOError,
24
31
  Net::OpenTimeout,
25
32
  Net::ProtocolError,
26
33
  Net::ReadTimeout,
34
+ Net::WriteTimeout,
27
35
  OpenSSL::OpenSSLError,
28
36
  OpenSSL::SSL::SSLError,
29
37
  SocketError,
30
38
  Timeout::Error
31
39
  ].freeze
32
40
 
33
- def self.logger
34
- @logger ||= Logger.new(nil)
35
- end
41
+ BASIC_PREFIX = 'Basic '
42
+ BEARER_PREFIX = 'Bearer '
36
43
 
37
- def self.logger=(logger)
38
- @logger = logger
39
- end
44
+ class << self
45
+ attr_writer :logger, :verify_mode
40
46
 
41
- def self.verify_mode
42
- @verify_mode ||= OpenSSL::SSL::VERIFY_PEER
43
- end
47
+ def logger
48
+ @logger ||= Logger.new(nil)
49
+ end
44
50
 
45
- def self.verify_mode=(mode)
46
- @verify_mode = mode
47
- end
51
+ def verify_mode
52
+ @verify_mode ||= OpenSSL::SSL::VERIFY_PEER
53
+ end
48
54
 
49
- def self.basic_auth(username, password)
50
- "Basic #{::Base64.strict_encode64("#{username}:#{password}")}"
51
- end
55
+ def basic_auth(username, password)
56
+ BASIC_PREFIX + ::Base64.strict_encode64("#{username}:#{password}")
57
+ end
52
58
 
53
- def self.bearer_auth(token)
54
- "Bearer #{token}"
55
- end
59
+ def bearer_auth(token)
60
+ BEARER_PREFIX + token
61
+ end
56
62
 
57
- def self.method_missing(symbol, *args)
58
- default_client.with_retry(retries: 3) do |client|
59
- client.public_send(symbol, *args)
60
- end || super
61
- end
63
+ def default_client
64
+ @default_client ||= Client.new(follow_redirects: 3, logger: logger)
65
+ end
62
66
 
63
- def self.respond_to_missing?(name, _include_private = false)
64
- Client.public_instance_methods.include?(name.to_sym) || super
65
- end
67
+ def reset_default_client!
68
+ @default_client = nil
69
+ end
70
+
71
+ %i[get post put patch delete head options].each do |method|
72
+ define_method(method) do |*args, **kwargs, &block|
73
+ default_client.with_retry(retries: 3) { |c| c.public_send(method, *args, **kwargs, &block) }
74
+ end
75
+ end
66
76
 
67
- def self.default_client
68
- @default_client ||= Client.new(follow_redirects: 3, logger: logger)
77
+ def resolve(hostname, timeout: 5)
78
+ Timeout.timeout(timeout) do
79
+ addresses = Resolv.getaddresses(hostname)
80
+ addresses.find { |a| a.match?(/^\d+\.\d+\.\d+\.\d+$/) } || addresses.first
81
+ end
82
+ end
69
83
  end
70
84
  end
71
85
  end
data/net-hippie.gemspec CHANGED
@@ -7,17 +7,18 @@ require 'net/hippie/version'
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = 'net-hippie'
9
9
  spec.version = Net::Hippie::VERSION
10
- spec.authors = ['mo']
10
+ spec.authors = ['mo khan']
11
11
  spec.email = ['mo@mokhan.ca']
12
12
 
13
13
  spec.summary = 'net/http for hippies. ☮️ '
14
14
  spec.description = 'net/http for hippies. ☮️ '
15
- spec.homepage = 'https://rubygems.org/gems/net-hippie'
15
+ spec.homepage = 'https://src.mokhan.ca/xlgmokha/net-hippie'
16
16
  spec.license = 'MIT'
17
- spec.metadata = {
18
- 'source_code_uri' => 'https://github.com/xlgmokha/net-hippie',
19
- 'rubygems_mfa_required' => 'true'
20
- }
17
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://git.mokhan.ca/xlgmokha/net-hippie.git'
20
+ spec.metadata['changelog_uri'] = 'https://src.mokhan.ca/xlgmokha/net-hippie/blob/main/CHANGELOG.md.html'
21
+ spec.metadata['rubygems_mfa_required'] = 'true'
21
22
 
22
23
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
23
24
  f.match(%r{^(test|spec|features)/})
@@ -25,16 +26,21 @@ Gem::Specification.new do |spec|
25
26
  spec.bindir = 'exe'
26
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
28
  spec.require_paths = ['lib']
28
- spec.required_ruby_version = Gem::Requirement.new('>= 3.2.0')
29
+ spec.required_ruby_version = Gem::Requirement.new('>= 4.0.0')
29
30
 
30
31
  spec.add_dependency 'base64', '~> 0.1'
31
32
  spec.add_dependency 'json', '~> 2.0'
32
33
  spec.add_dependency 'logger', '~> 1.0'
33
- spec.add_dependency 'net-http', '~> 0.6'
34
- spec.add_dependency 'openssl', '~> 3.0'
35
- spec.add_development_dependency 'minitest', '~> 5.0'
34
+ spec.add_dependency 'monitor', '~> 0.1'
35
+ spec.add_dependency 'net-http', '~> 0.1'
36
+ spec.add_dependency 'openssl', '~> 4.0'
37
+ spec.add_dependency 'resolv', '~> 0.1'
38
+ spec.add_dependency 'timeout', '~> 0.1'
39
+ spec.add_development_dependency 'minitest', '~> 6.0'
36
40
  spec.add_development_dependency 'rake', '~> 13.0'
37
41
  spec.add_development_dependency 'rubocop', '~> 1.9'
42
+ spec.add_development_dependency 'rubocop-minitest', '~> 0.1'
43
+ spec.add_development_dependency 'rubocop-rake', '~> 0.1'
38
44
  spec.add_development_dependency 'vcr', '~> 6.0'
39
45
  spec.add_development_dependency 'webmock', '~> 3.4'
40
46
  end
@@ -0,0 +1,25 @@
1
+ module Net
2
+ module Hippie
3
+ class Client
4
+ DEFAULT_HEADERS: Hash[String, String]
5
+
6
+ attr_reader mapper: ContentTypeMapper
7
+ attr_reader logger: untyped
8
+ attr_reader follow_redirects: Integer
9
+
10
+ def initialize: (?Hash[Symbol, untyped] options) -> void
11
+
12
+ def get: (untyped uri, ?headers: Hash[String, String], ?body: untyped, ?stream: bool) ?{ (untyped) -> void } -> untyped
13
+ def post: (untyped uri, ?headers: Hash[String, String], ?body: untyped, ?stream: bool) ?{ (untyped) -> void } -> untyped
14
+ def put: (untyped uri, ?headers: Hash[String, String], ?body: untyped, ?stream: bool) ?{ (untyped) -> void } -> untyped
15
+ def patch: (untyped uri, ?headers: Hash[String, String], ?body: untyped, ?stream: bool) ?{ (untyped) -> void } -> untyped
16
+ def delete: (untyped uri, ?headers: Hash[String, String], ?body: untyped, ?stream: bool) ?{ (untyped) -> void } -> untyped
17
+ def head: (untyped uri, ?headers: Hash[String, String]) ?{ (untyped) -> void } -> untyped
18
+ def options: (untyped uri, ?headers: Hash[String, String]) ?{ (untyped) -> void } -> untyped
19
+
20
+ def execute: (untyped uri, untyped request, ?limit: Integer, ?stream: bool) ?{ (untyped) -> void } -> untyped
21
+ def with_retry: (?retries: Integer) { (Client) -> untyped } -> untyped
22
+ def close_all: () -> void
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ module Net
2
+ module Hippie
3
+ class ContentTypeMapper
4
+ def map_from: (Hash[String, String] headers, (Hash[untyped, untyped] | String) body) -> (String | Hash[untyped, untyped])
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ module Net
2
+ module Hippie
3
+ VERSION: String
4
+
5
+ CONNECTION_ERRORS: Array[Class]
6
+
7
+ def self.logger: () -> untyped
8
+ def self.logger=: (untyped) -> untyped
9
+
10
+ def self.verify_mode: () -> Integer
11
+ def self.verify_mode=: (Integer) -> Integer
12
+
13
+ def self.basic_auth: (String username, String password) -> String
14
+ def self.bearer_auth: (String token) -> String
15
+
16
+ def self.default_client: () -> Client
17
+ def self.reset_default_client!: () -> nil
18
+
19
+ def self.resolve: (String hostname, ?timeout: Integer) -> String?
20
+
21
+ def self.get: (untyped uri, ?headers: Hash[String, String], ?body: untyped, ?stream: bool) ?{ (untyped) -> void } -> untyped
22
+ def self.post: (untyped uri, ?headers: Hash[String, String], ?body: untyped, ?stream: bool) ?{ (untyped) -> void } -> untyped
23
+ def self.put: (untyped uri, ?headers: Hash[String, String], ?body: untyped, ?stream: bool) ?{ (untyped) -> void } -> untyped
24
+ def self.patch: (untyped uri, ?headers: Hash[String, String], ?body: untyped, ?stream: bool) ?{ (untyped) -> void } -> untyped
25
+ def self.delete: (untyped uri, ?headers: Hash[String, String], ?body: untyped, ?stream: bool) ?{ (untyped) -> void } -> untyped
26
+ def self.head: (untyped uri, ?headers: Hash[String, String]) ?{ (untyped) -> void } -> untyped
27
+ def self.options: (untyped uri, ?headers: Hash[String, String]) ?{ (untyped) -> void } -> untyped
28
+ end
29
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-hippie
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
- - mo
7
+ - mo khan
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-30 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -51,48 +51,90 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '1.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: monitor
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.1'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.1'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: net-http
56
70
  requirement: !ruby/object:Gem::Requirement
57
71
  requirements:
58
72
  - - "~>"
59
73
  - !ruby/object:Gem::Version
60
- version: '0.6'
74
+ version: '0.1'
61
75
  type: :runtime
62
76
  prerelease: false
63
77
  version_requirements: !ruby/object:Gem::Requirement
64
78
  requirements:
65
79
  - - "~>"
66
80
  - !ruby/object:Gem::Version
67
- version: '0.6'
81
+ version: '0.1'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: openssl
70
84
  requirement: !ruby/object:Gem::Requirement
71
85
  requirements:
72
86
  - - "~>"
73
87
  - !ruby/object:Gem::Version
74
- version: '3.0'
88
+ version: '4.0'
75
89
  type: :runtime
76
90
  prerelease: false
77
91
  version_requirements: !ruby/object:Gem::Requirement
78
92
  requirements:
79
93
  - - "~>"
80
94
  - !ruby/object:Gem::Version
81
- version: '3.0'
95
+ version: '4.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: resolv
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.1'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.1'
110
+ - !ruby/object:Gem::Dependency
111
+ name: timeout
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.1'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.1'
82
124
  - !ruby/object:Gem::Dependency
83
125
  name: minitest
84
126
  requirement: !ruby/object:Gem::Requirement
85
127
  requirements:
86
128
  - - "~>"
87
129
  - !ruby/object:Gem::Version
88
- version: '5.0'
130
+ version: '6.0'
89
131
  type: :development
90
132
  prerelease: false
91
133
  version_requirements: !ruby/object:Gem::Requirement
92
134
  requirements:
93
135
  - - "~>"
94
136
  - !ruby/object:Gem::Version
95
- version: '5.0'
137
+ version: '6.0'
96
138
  - !ruby/object:Gem::Dependency
97
139
  name: rake
98
140
  requirement: !ruby/object:Gem::Requirement
@@ -121,6 +163,34 @@ dependencies:
121
163
  - - "~>"
122
164
  - !ruby/object:Gem::Version
123
165
  version: '1.9'
166
+ - !ruby/object:Gem::Dependency
167
+ name: rubocop-minitest
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '0.1'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '0.1'
180
+ - !ruby/object:Gem::Dependency
181
+ name: rubocop-rake
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - "~>"
185
+ - !ruby/object:Gem::Version
186
+ version: '0.1'
187
+ type: :development
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: '0.1'
124
194
  - !ruby/object:Gem::Dependency
125
195
  name: vcr
126
196
  requirement: !ruby/object:Gem::Requirement
@@ -157,8 +227,6 @@ executables:
157
227
  extensions: []
158
228
  extra_rdoc_files: []
159
229
  files:
160
- - ".github/dependabot.yml"
161
- - ".github/workflows/ci.yml"
162
230
  - ".gitignore"
163
231
  - ".rubocop.yml"
164
232
  - CHANGELOG.md
@@ -175,14 +243,23 @@ files:
175
243
  - lib/net/hippie.rb
176
244
  - lib/net/hippie/client.rb
177
245
  - lib/net/hippie/connection.rb
246
+ - lib/net/hippie/connection_pool.rb
178
247
  - lib/net/hippie/content_type_mapper.rb
248
+ - lib/net/hippie/dns_cache.rb
249
+ - lib/net/hippie/tls_parser.rb
179
250
  - lib/net/hippie/version.rb
180
251
  - net-hippie.gemspec
181
- homepage: https://rubygems.org/gems/net-hippie
252
+ - sig/net/hippie.rbs
253
+ - sig/net/hippie/client.rbs
254
+ - sig/net/hippie/content_type_mapper.rbs
255
+ homepage: https://src.mokhan.ca/xlgmokha/net-hippie
182
256
  licenses:
183
257
  - MIT
184
258
  metadata:
185
- source_code_uri: https://github.com/xlgmokha/net-hippie
259
+ allowed_push_host: https://rubygems.org
260
+ homepage_uri: https://src.mokhan.ca/xlgmokha/net-hippie
261
+ source_code_uri: https://git.mokhan.ca/xlgmokha/net-hippie.git
262
+ changelog_uri: https://src.mokhan.ca/xlgmokha/net-hippie/blob/main/CHANGELOG.md.html
186
263
  rubygems_mfa_required: 'true'
187
264
  rdoc_options: []
188
265
  require_paths:
@@ -191,14 +268,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
191
268
  requirements:
192
269
  - - ">="
193
270
  - !ruby/object:Gem::Version
194
- version: 3.2.0
271
+ version: 4.0.0
195
272
  required_rubygems_version: !ruby/object:Gem::Requirement
196
273
  requirements:
197
274
  - - ">="
198
275
  - !ruby/object:Gem::Version
199
276
  version: '0'
200
277
  requirements: []
201
- rubygems_version: 3.6.6
278
+ rubygems_version: 4.0.5
202
279
  specification_version: 4
203
280
  summary: net/http for hippies. ☮️
204
281
  test_files: []
@@ -1,9 +0,0 @@
1
- ---
2
- version: 2
3
- updates:
4
- - package-ecosystem: "bundler"
5
- directory: "/"
6
- schedule:
7
- interval: "daily"
8
- assignees:
9
- - "xlgmokha"
@@ -1,31 +0,0 @@
1
- name: ci
2
- on:
3
- push:
4
- branches: [main]
5
- pull_request:
6
- branches: [main]
7
- jobs:
8
- test:
9
- runs-on: ubuntu-latest
10
- strategy:
11
- matrix:
12
- ruby-version: ['3.2', '3.3', '3.4']
13
- steps:
14
- - uses: actions/checkout@v2
15
- - name: Set up Ruby
16
- uses: ruby/setup-ruby@v1
17
- with:
18
- ruby-version: ${{ matrix.ruby-version }}
19
- bundler-cache: true
20
- - name: Running tests…
21
- run: sh bin/test
22
- style:
23
- runs-on: ubuntu-latest
24
- steps:
25
- - uses: actions/checkout@v2
26
- - uses: ruby/setup-ruby@v1
27
- with:
28
- ruby-version: '3.4'
29
- bundler-cache: true
30
- - name: Running style checks…
31
- run: sh bin/style