mint_http 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +34 -0
- data/LICENSE.txt +21 -0
- data/README.md +218 -0
- data/Rakefile +12 -0
- data/lib/mint_http/errors/authentication_error.rb +4 -0
- data/lib/mint_http/errors/authorization_error.rb +4 -0
- data/lib/mint_http/errors/client_error.rb +4 -0
- data/lib/mint_http/errors/not_found_error.rb +4 -0
- data/lib/mint_http/errors/response_error.rb +15 -0
- data/lib/mint_http/errors/server_error.rb +4 -0
- data/lib/mint_http/headers.rb +11 -0
- data/lib/mint_http/net_http_factory.rb +77 -0
- data/lib/mint_http/pool.rb +109 -0
- data/lib/mint_http/pool_entry.rb +72 -0
- data/lib/mint_http/request.rb +322 -0
- data/lib/mint_http/response.rb +76 -0
- data/lib/mint_http/version.rb +5 -0
- data/lib/mint_http.rb +25 -0
- data/sig/mint_http.rbs +4 -0
- metadata +137 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '091443e652f456b8bd3d867b48379610f5a0405272158921c8e92f7885483aa4'
|
4
|
+
data.tar.gz: 42b042095a6a3bad22c22d5cc4da0b004463b9e5a30918701a837ee480e46a80
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fe2c2aa2bb1984f361d8cc695a5e2e26cfa3d4b1479d746ec4ccd2ae2f8c1df765a11280796cf81b8812d1b6fa68cccd789b432c0c0d26226c88b8877d4cff97
|
7
|
+
data.tar.gz: 9e58beac30df8cb862ce02440c2132fc88f24da196e703219443959d668e5715a24a29e5dfd9b311ebcf7c4ec3dd3f02664473f18042942c8edd814c800a845e
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
mint_http (0.1.0)
|
5
|
+
base64
|
6
|
+
json
|
7
|
+
net-http
|
8
|
+
openssl
|
9
|
+
uri
|
10
|
+
|
11
|
+
GEM
|
12
|
+
remote: https://rubygems.org/
|
13
|
+
specs:
|
14
|
+
base64 (0.1.0)
|
15
|
+
ipaddr (1.2.4)
|
16
|
+
json (2.6.2)
|
17
|
+
minitest (5.18.0)
|
18
|
+
net-http (0.3.2)
|
19
|
+
uri
|
20
|
+
openssl (2.2.2)
|
21
|
+
ipaddr
|
22
|
+
rake (13.0.6)
|
23
|
+
uri (0.12.1)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
arm64-darwin-22
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
minitest (~> 5.0)
|
30
|
+
mint_http!
|
31
|
+
rake (~> 13.0)
|
32
|
+
|
33
|
+
BUNDLED WITH
|
34
|
+
2.2.33
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Ali Alhoshaiyan
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,218 @@
|
|
1
|
+
# MintHttp
|
2
|
+
|
3
|
+
A simple and fluent HTTP client with connection pooling capability.
|
4
|
+
|
5
|
+
MintHttp is built on top of Ruby's Net::HTTP library to provide you with the following features:
|
6
|
+
|
7
|
+
- Fluent API to build requests
|
8
|
+
- HTTP proxies support
|
9
|
+
- Client certificate support
|
10
|
+
- File uploads
|
11
|
+
- Connection pooling
|
12
|
+
- No DSLs or any other shenanigans
|
13
|
+
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Install the gem and add to the application's Gemfile by executing:
|
18
|
+
|
19
|
+
$ bundle add mint_http
|
20
|
+
|
21
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
22
|
+
|
23
|
+
$ gem install mint_http
|
24
|
+
|
25
|
+
|
26
|
+
## Basic Usage
|
27
|
+
|
28
|
+
Once you have installed the gem, require it into your Ruby code with:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
require 'mint_http'
|
32
|
+
```
|
33
|
+
|
34
|
+
Now perform a basic HTTP request:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
puts MintHttp.query(foo: 'bar').get('https://icanhazip.com').body
|
38
|
+
```
|
39
|
+
|
40
|
+
Note: when performing a request with MintHttp, you will get a [MintHttp::Response](/lib/mint_http/response.rb).
|
41
|
+
|
42
|
+
|
43
|
+
## Post Requests
|
44
|
+
|
45
|
+
MintHttp makes it easy to consume and interact with JSON APIs. You can simply make a post request as follows:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
MintHttp
|
49
|
+
.as_json
|
50
|
+
.accept_json
|
51
|
+
.post('https://dummyjson.com/products/add', {
|
52
|
+
title: 'Porsche Wallet'
|
53
|
+
})
|
54
|
+
```
|
55
|
+
|
56
|
+
The `as_json` and `accept_json` helper methods sets `Content-Type` and `Accept` headers to `application/json`. The above
|
57
|
+
call can be re-written as:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
MintHttp
|
61
|
+
.header('Content-Type' => 'application/json')
|
62
|
+
.header('Accept' => 'application/json')
|
63
|
+
.post('https://dummyjson.com/products/add', {
|
64
|
+
title: 'Porsche Wallet'
|
65
|
+
})
|
66
|
+
```
|
67
|
+
|
68
|
+
Note: When no content type is set, `application/json` is set by default.
|
69
|
+
|
70
|
+
|
71
|
+
## Put Requests
|
72
|
+
|
73
|
+
Like a post request, you can easily make a `PUT` request as follows:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
MintHttp.put('https://dummyjson.com/products/1', {
|
77
|
+
title: 'Porsche Wallet'
|
78
|
+
})
|
79
|
+
```
|
80
|
+
|
81
|
+
or make a `PATCH` request
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
MintHttp.patch('https://dummyjson.com/products/1', {
|
85
|
+
title: 'Porsche Wallet'
|
86
|
+
})
|
87
|
+
```
|
88
|
+
|
89
|
+
|
90
|
+
## Delete Requests
|
91
|
+
|
92
|
+
To delete some resource, you can easily call:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
MintHttp.delete('https://dummyjson.com/products/1')
|
96
|
+
```
|
97
|
+
|
98
|
+
|
99
|
+
## Uploading Files
|
100
|
+
|
101
|
+
MintHttp makes good use of the powerful Net::HTTP library to allow you to upload files:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
MintHttp
|
105
|
+
.as_multipart
|
106
|
+
.with_file('upload_field', File.open('/tmp/file.txt'), 'grocery-list.txt', 'text/plain')
|
107
|
+
.post('https://example.com/upload')
|
108
|
+
```
|
109
|
+
|
110
|
+
Note: `as_multipart` is required in order to upload files, this will set the content type to `multipart/form-data`.
|
111
|
+
|
112
|
+
|
113
|
+
## Authentication
|
114
|
+
|
115
|
+
MintHttp provides you with little helpers for common authentication schemes
|
116
|
+
|
117
|
+
### Basic Auth
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
MintHttp
|
121
|
+
.basic_auth('username', 'password')
|
122
|
+
.get('https://example.com/secret-door')
|
123
|
+
```
|
124
|
+
|
125
|
+
|
126
|
+
### Bearer Auth
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
MintHttp
|
130
|
+
.bearer('super-string-token')
|
131
|
+
.get('https://example.com/secret-door')
|
132
|
+
```
|
133
|
+
|
134
|
+
|
135
|
+
## Using a Proxy
|
136
|
+
|
137
|
+
Connecting through an HTTP proxy is as simple as chaining the following call:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
MintHttp
|
141
|
+
.via_proxy('proxy.example.com', 3128, 'optional-username', 'optional-password')
|
142
|
+
.get('https://icanhazip.com')
|
143
|
+
```
|
144
|
+
|
145
|
+
|
146
|
+
## Pooling Connections
|
147
|
+
|
148
|
+
When your application is communicating with external services so often, it is a good idea to keep a couple of connections
|
149
|
+
open if the target server supports that, this can save you time that is usually gone by TCP and TLS handshakes. This
|
150
|
+
is especially true when the target server is in a faraway geographical region.
|
151
|
+
|
152
|
+
MintHttp uses a pool to manage connections and make sure each connection is used by a single thread at a time. To create
|
153
|
+
a pool, simply do the following:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
pool = MintHttp::Pool.new({
|
157
|
+
ttl: 30_000,
|
158
|
+
idle_ttl: 10_000,
|
159
|
+
size: 10,
|
160
|
+
usage_limit: 500,
|
161
|
+
timeout: 500,
|
162
|
+
})
|
163
|
+
```
|
164
|
+
|
165
|
+
This will create a new pool with the following properties:
|
166
|
+
|
167
|
+
- Allow a connection to be open for a maximum of 30 seconds defined by `ttl`.
|
168
|
+
- If a connection is not used within 10 seconds of the last use then it is considered expired, defined by `idle_ttl`.
|
169
|
+
- Only hold a maximum of 10 connections, if an 11th thread tries to acquire a connection, it will block until there is one available or timeout is reached.
|
170
|
+
- A connection may be only used for 500 requests, then it should be closed. This is defined by `usage_limit`.
|
171
|
+
|
172
|
+
Once you have created a pool, you can use it in your requests as following:
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
MintHttp
|
176
|
+
.use_pool(pool)
|
177
|
+
.get('https://example.com')
|
178
|
+
```
|
179
|
+
|
180
|
+
A single pool can be used for multiple endpoints, as MintHttp will logically separate connections based on hostname, port,
|
181
|
+
scheme, client certificate, proxy, and other variables.
|
182
|
+
|
183
|
+
> Note: it is possible to use only a single pool for the entire application.
|
184
|
+
|
185
|
+
> Note: The Pool object is thread-safe
|
186
|
+
|
187
|
+
|
188
|
+
## The Response Object
|
189
|
+
|
190
|
+
Each HTTP request will return a [response](/lib/mint_http/response.rb) object when a response is received from the server regardless of the status code.
|
191
|
+
|
192
|
+
You can chain the `raise!` call after the request to make MintHttp throw an exception when a `4xx` ot `5xx` error is returned.
|
193
|
+
Otherwise the same response object is returned.
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
MintHttp.get('https://example.com').raise!
|
197
|
+
```
|
198
|
+
|
199
|
+
|
200
|
+
## Missing Features
|
201
|
+
|
202
|
+
There are a couple of features that are coming soon, these include:
|
203
|
+
|
204
|
+
- Retrying Requests
|
205
|
+
- Middlewares
|
206
|
+
|
207
|
+
|
208
|
+
## Credit
|
209
|
+
|
210
|
+
This library was inspired by Laravel's `Http` wrapper over `GuzzleHttp`.
|
211
|
+
|
212
|
+
## Contributing
|
213
|
+
|
214
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ahoshaiyan/mint_http.
|
215
|
+
|
216
|
+
## License
|
217
|
+
|
218
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MintHttp::ResponseError < StandardError
|
4
|
+
# @return [HTTP::Response]
|
5
|
+
attr_reader :response
|
6
|
+
|
7
|
+
def initialize(msg, response)
|
8
|
+
super(msg)
|
9
|
+
@response = response
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
response.inspect
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MintHttp::NetHttpFactory
|
4
|
+
def defaults(options = {})
|
5
|
+
options[:open_timeout] = options[:open_timeout] || 5
|
6
|
+
options[:write_timeout] = options[:write_timeout] || 5
|
7
|
+
options[:read_timeout] = options[:read_timeout] || 20
|
8
|
+
options[:ssl_timeout] = options[:ssl_timeout] || 5
|
9
|
+
options[:verify_mode] = options[:verify_mode] || OpenSSL::SSL::VERIFY_PEER
|
10
|
+
options[:verify_hostname] = options[:verify_hostname] || true
|
11
|
+
options
|
12
|
+
end
|
13
|
+
|
14
|
+
def client_namespace(hostname, port, options = {})
|
15
|
+
options = defaults(options)
|
16
|
+
|
17
|
+
host_group = "#{hostname.downcase}_#{port.to_s.downcase}"
|
18
|
+
proxy_group = "#{options[:proxy_address]}_#{options[:proxy_port]}_#{options[:proxy_user]}"
|
19
|
+
timeout_group = "#{options[:open_timeout]}_#{options[:write_timeout]}_#{options[:read_timeout]}"
|
20
|
+
|
21
|
+
cert_signature = options[:cert]&.serial&.to_s
|
22
|
+
key_signature = OpenSSL::Digest::SHA1.new(options[:key]&.to_der || '').to_s
|
23
|
+
ssl_group = "#{options[:use_ssl]}_#{options[:ssl_timeout]}_#{options[:ca_file]}_#{cert_signature}_#{key_signature}_#{options[:verify_mode]}_#{options[:verify_hostname]}"
|
24
|
+
|
25
|
+
"#{host_group}_#{proxy_group}_#{timeout_group}_#{ssl_group}"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Available options:
|
29
|
+
# proxy_address
|
30
|
+
# proxy_port
|
31
|
+
# proxy_user
|
32
|
+
# proxy_pass
|
33
|
+
# open_timeout
|
34
|
+
# write_timeout
|
35
|
+
# read_timeout
|
36
|
+
# ssl_timeout
|
37
|
+
# ca_file
|
38
|
+
# cert
|
39
|
+
# key
|
40
|
+
# verify_mode
|
41
|
+
# verify_hostname
|
42
|
+
def make_client(hostname, port, options = {})
|
43
|
+
options = defaults(options)
|
44
|
+
|
45
|
+
net_http = Net::HTTP.new(hostname, port, nil)
|
46
|
+
|
47
|
+
# Disable retries
|
48
|
+
net_http.max_retries = 0
|
49
|
+
|
50
|
+
# Set proxy options
|
51
|
+
net_http.proxy_address = options[:proxy_address]
|
52
|
+
net_http.proxy_port = options[:proxy_port]
|
53
|
+
net_http.proxy_user = options[:proxy_user]
|
54
|
+
net_http.proxy_pass = options[:proxy_pass]
|
55
|
+
|
56
|
+
# Timeout
|
57
|
+
net_http.open_timeout = options[:open_timeout]
|
58
|
+
net_http.write_timeout = options[:write_timeout]
|
59
|
+
net_http.read_timeout = options[:read_timeout]
|
60
|
+
|
61
|
+
# SSL options
|
62
|
+
net_http.use_ssl = options[:use_ssl]
|
63
|
+
net_http.ssl_timeout = options[:ssl_timeout]
|
64
|
+
net_http.cert = options[:cert]
|
65
|
+
net_http.key = options[:key]
|
66
|
+
net_http.verify_mode = options[:verify_mode]
|
67
|
+
net_http.verify_hostname = options[:verify_hostname]
|
68
|
+
|
69
|
+
if OpenSSL::X509::Store === options[:ca]
|
70
|
+
net_http.cert_store = options[:ca]
|
71
|
+
else
|
72
|
+
net_http.ca_file = options[:ca]
|
73
|
+
end
|
74
|
+
|
75
|
+
net_http
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MintHttp::Pool
|
4
|
+
attr_reader :ttl
|
5
|
+
attr_reader :idle_ttl
|
6
|
+
attr_reader :timeout
|
7
|
+
attr_reader :size
|
8
|
+
attr_reader :usage_limit
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@mutex = Mutex.new
|
12
|
+
|
13
|
+
@ttl = options[:ttl] || 10000
|
14
|
+
@idle_ttl = options[:idle_ttl] || 5000
|
15
|
+
@timeout = options[:timeout] || 5000
|
16
|
+
|
17
|
+
@size = options[:size] || 10
|
18
|
+
@usage_limit = options[:usage_limit] || 100
|
19
|
+
|
20
|
+
@pool = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def net_factory
|
24
|
+
@net_factory ||= MintHttp::NetHttpFactory.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def acquire(hostname, port, options = {})
|
28
|
+
namespace = net_factory.client_namespace(hostname, port, options)
|
29
|
+
deadline = time_ms + @timeout
|
30
|
+
|
31
|
+
while time_ms < deadline
|
32
|
+
@mutex.synchronize do
|
33
|
+
if (entry = @pool.find { |e| e.namespace == namespace && e.available? })
|
34
|
+
entry.acquire!
|
35
|
+
return entry.client
|
36
|
+
end
|
37
|
+
|
38
|
+
if @pool.length > 0 && @pool.all? { |e| e.to_clean? }
|
39
|
+
clean_pool_unsafe!
|
40
|
+
end
|
41
|
+
|
42
|
+
if @pool.length < @size
|
43
|
+
client = net_factory.make_client(hostname, port, options)
|
44
|
+
entry = append(client, namespace)
|
45
|
+
entry.acquire!
|
46
|
+
return entry.client
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
sleep_time = (deadline - time_ms) / 2
|
51
|
+
sleep_time = [sleep_time, 100].max
|
52
|
+
sleep_time = sleep_time / 1000.0
|
53
|
+
|
54
|
+
sleep(sleep_time)
|
55
|
+
end
|
56
|
+
|
57
|
+
raise RuntimeError, "Cannot acquire lock after #{@timeout}ms."
|
58
|
+
end
|
59
|
+
|
60
|
+
def release(client)
|
61
|
+
raise ArgumentError, 'An client is required to be released.' unless client
|
62
|
+
|
63
|
+
@mutex.synchronize do
|
64
|
+
if (entry = @pool.find { |e| e.matches?(client) })
|
65
|
+
entry.release!
|
66
|
+
end
|
67
|
+
|
68
|
+
clean_pool_unsafe!
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def append(client, namespace)
|
75
|
+
if @pool.any? { |e| e.matches?(client) }
|
76
|
+
raise RuntimeError, "client with id ##{client.object_id} already exists in the pool."
|
77
|
+
end
|
78
|
+
|
79
|
+
@pool << (entry = MintHttp::PoolEntry.new(self, client, namespace))
|
80
|
+
entry
|
81
|
+
end
|
82
|
+
|
83
|
+
def time_ms
|
84
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
85
|
+
end
|
86
|
+
|
87
|
+
def elapsed(beginning)
|
88
|
+
time_ms - beginning
|
89
|
+
end
|
90
|
+
|
91
|
+
def clean_pool_unsafe!
|
92
|
+
to_clean = []
|
93
|
+
|
94
|
+
@pool.delete_if do |e|
|
95
|
+
to_clean << e if (should_clean = e.to_clean?)
|
96
|
+
should_clean
|
97
|
+
end
|
98
|
+
|
99
|
+
to_clean.each do |e|
|
100
|
+
e.client.finish rescue nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def clean_pool!
|
105
|
+
@mutex.synchronize do
|
106
|
+
clean_pool_unsafe!
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MintHttp::PoolEntry
|
4
|
+
attr_reader :client
|
5
|
+
attr_reader :namespace
|
6
|
+
attr_reader :acquired
|
7
|
+
attr_reader :last_used
|
8
|
+
attr_reader :usage
|
9
|
+
attr_reader :unhealthy
|
10
|
+
|
11
|
+
def initialize(pool, client, namespace)
|
12
|
+
@pool = pool
|
13
|
+
@client = client
|
14
|
+
@namespace = namespace
|
15
|
+
@acquired = false
|
16
|
+
@birth_time = time_ms
|
17
|
+
@last_used = time_ms
|
18
|
+
@usage = 0
|
19
|
+
@unhealthy = false
|
20
|
+
end
|
21
|
+
|
22
|
+
def matches?(other)
|
23
|
+
@client.object_id == other.object_id
|
24
|
+
end
|
25
|
+
|
26
|
+
def ttl_reached?
|
27
|
+
(time_ms - @birth_time) > @pool.ttl
|
28
|
+
end
|
29
|
+
|
30
|
+
def idle_ttl_reached?
|
31
|
+
(time_ms - @last_used) > @pool.idle_ttl
|
32
|
+
end
|
33
|
+
|
34
|
+
def usage_reached?
|
35
|
+
@usage >= @pool.usage_limit
|
36
|
+
end
|
37
|
+
|
38
|
+
def expired?
|
39
|
+
idle_ttl_reached? || ttl_reached? || usage_reached?
|
40
|
+
end
|
41
|
+
|
42
|
+
def acquire!
|
43
|
+
@acquired = true
|
44
|
+
@last_used = time_ms
|
45
|
+
@usage += 1
|
46
|
+
end
|
47
|
+
|
48
|
+
def release!
|
49
|
+
@acquired = false
|
50
|
+
end
|
51
|
+
|
52
|
+
def available?
|
53
|
+
!@acquired && !expired? && !@unhealthy
|
54
|
+
end
|
55
|
+
|
56
|
+
def healthy?
|
57
|
+
# TODO: add health check
|
58
|
+
healthy = true
|
59
|
+
@unhealthy = !healthy
|
60
|
+
healthy
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_clean?
|
64
|
+
(expired? || @unhealthy) && !@acquired
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def time_ms
|
70
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,322 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'net/http'
|
5
|
+
require 'uri'
|
6
|
+
require 'openssl'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
module MintHttp
|
10
|
+
class Request
|
11
|
+
attr_reader :base_url
|
12
|
+
attr_reader :headers
|
13
|
+
attr_reader :body_type
|
14
|
+
attr_reader :body
|
15
|
+
attr_reader :query
|
16
|
+
attr_reader :open_timeout
|
17
|
+
attr_reader :write_timeout
|
18
|
+
attr_reader :read_timeout
|
19
|
+
attr_reader :ca
|
20
|
+
attr_reader :cert
|
21
|
+
attr_reader :proxy_address
|
22
|
+
attr_reader :proxy_port
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@pool = nil
|
26
|
+
@base_url = nil
|
27
|
+
@headers = {}
|
28
|
+
@body_type = nil
|
29
|
+
@body = nil
|
30
|
+
@query = {}
|
31
|
+
@files = []
|
32
|
+
@open_timeout = 5
|
33
|
+
@write_timeout = 5
|
34
|
+
@read_timeout = 20
|
35
|
+
@ca = nil
|
36
|
+
@cert = nil
|
37
|
+
@key = nil
|
38
|
+
@proxy_address = nil
|
39
|
+
@proxy_port = nil
|
40
|
+
@proxy_user = nil
|
41
|
+
@proxy_pass = nil
|
42
|
+
|
43
|
+
header('User-Agent' => 'Mint Http')
|
44
|
+
as_json
|
45
|
+
end
|
46
|
+
|
47
|
+
def use_pool(pool)
|
48
|
+
raise ArgumentError, 'Expected a MintHttp::Pool' unless Pool === pool
|
49
|
+
|
50
|
+
@pool = pool
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def timeout(open, write, read)
|
55
|
+
@open_timeout = open
|
56
|
+
@write_timeout = write
|
57
|
+
@read_timeout = read
|
58
|
+
end
|
59
|
+
|
60
|
+
def base_url(url)
|
61
|
+
@base_url = URI.parse(url)
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def use_ca(ca)
|
66
|
+
@ca = ca
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def use_cert(cert, key)
|
71
|
+
unless OpenSSL::X509::Certificate === cert
|
72
|
+
raise ArgumentError, 'Expected an OpenSSL::X509::Certificate'
|
73
|
+
end
|
74
|
+
|
75
|
+
unless OpenSSL::PKey::PKey === key
|
76
|
+
raise ArgumentError, 'Expected an OpenSSL::PKey::PKey'
|
77
|
+
end
|
78
|
+
|
79
|
+
@cert = cert
|
80
|
+
@key = key
|
81
|
+
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def via_proxy(proxy_address, proxy_port = 3128, proxy_user = nil, proxy_pass = nil)
|
86
|
+
@proxy_address = proxy_address
|
87
|
+
@proxy_port = proxy_port
|
88
|
+
@proxy_user = proxy_user
|
89
|
+
@proxy_pass = proxy_pass
|
90
|
+
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def query(queries = {})
|
95
|
+
queries.each do |k, v|
|
96
|
+
k = k.to_s
|
97
|
+
|
98
|
+
if v.nil?
|
99
|
+
@query.delete(k)
|
100
|
+
next
|
101
|
+
end
|
102
|
+
|
103
|
+
@query[k] = v.to_s
|
104
|
+
end
|
105
|
+
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
def header(headers = {})
|
110
|
+
headers.each do |k, v|
|
111
|
+
k = k.downcase.to_s
|
112
|
+
|
113
|
+
if v.nil?
|
114
|
+
@headers.delete(k)
|
115
|
+
next
|
116
|
+
end
|
117
|
+
|
118
|
+
v = v.join(' ;') if Array === v
|
119
|
+
@headers[k] = v
|
120
|
+
end
|
121
|
+
|
122
|
+
self
|
123
|
+
end
|
124
|
+
|
125
|
+
def basic_auth(username, password = '')
|
126
|
+
header('Authorization' => 'Basic ' + Base64.strict_encode64("#{username}:#{password}"))
|
127
|
+
end
|
128
|
+
|
129
|
+
def token_auth(type, token)
|
130
|
+
header('Authorization' => "#{type} #{token}")
|
131
|
+
end
|
132
|
+
|
133
|
+
def bearer(token)
|
134
|
+
token_auth('Bearer', token)
|
135
|
+
end
|
136
|
+
|
137
|
+
def accept(type)
|
138
|
+
header('Accept' => type)
|
139
|
+
end
|
140
|
+
|
141
|
+
def accept_json
|
142
|
+
accept('application/json')
|
143
|
+
end
|
144
|
+
|
145
|
+
def content_type(type)
|
146
|
+
header('Content-Type' => type)
|
147
|
+
end
|
148
|
+
|
149
|
+
def with_body(raw)
|
150
|
+
@body_type = :raw
|
151
|
+
@body = raw
|
152
|
+
content_type(nil)
|
153
|
+
self
|
154
|
+
end
|
155
|
+
|
156
|
+
def as_json
|
157
|
+
@body_type = :json
|
158
|
+
content_type('application/json')
|
159
|
+
end
|
160
|
+
|
161
|
+
def as_form
|
162
|
+
@body_type = :form
|
163
|
+
content_type('application/x-www-form-urlencoded')
|
164
|
+
end
|
165
|
+
|
166
|
+
def as_multipart
|
167
|
+
@body_type = :multipart
|
168
|
+
content_type('multipart/form-data')
|
169
|
+
end
|
170
|
+
|
171
|
+
def with_file(name, file, filename = nil, content_type = nil)
|
172
|
+
unless file.respond_to?(:read)
|
173
|
+
raise ArgumentError, "File must be an IO or IO like"
|
174
|
+
end
|
175
|
+
|
176
|
+
@files << [
|
177
|
+
name,
|
178
|
+
file,
|
179
|
+
{ filename: filename, content_type: content_type }.compact
|
180
|
+
]
|
181
|
+
|
182
|
+
self
|
183
|
+
end
|
184
|
+
|
185
|
+
def get(url, params = {})
|
186
|
+
query(params).send_request('get', url)
|
187
|
+
end
|
188
|
+
|
189
|
+
def head(url, params = {})
|
190
|
+
query(params).send_request('head', url)
|
191
|
+
end
|
192
|
+
|
193
|
+
def post(url, data = nil)
|
194
|
+
@body = data if data
|
195
|
+
send_request('post', url)
|
196
|
+
end
|
197
|
+
|
198
|
+
def put(url, data = nil)
|
199
|
+
@body = data if data
|
200
|
+
send_request('put', url)
|
201
|
+
end
|
202
|
+
|
203
|
+
def patch(url, data = nil)
|
204
|
+
@body = data if data
|
205
|
+
send_request('patch', url)
|
206
|
+
end
|
207
|
+
|
208
|
+
def delete(url, data = nil)
|
209
|
+
@body = data if data
|
210
|
+
send_request('delete', url)
|
211
|
+
end
|
212
|
+
|
213
|
+
def send_request(method, url)
|
214
|
+
url, net_request, options = build_request(method, url)
|
215
|
+
|
216
|
+
res = with_client(url.hostname, url.port, options) do |http|
|
217
|
+
http.request(net_request)
|
218
|
+
end
|
219
|
+
|
220
|
+
Response.new(res)
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
|
225
|
+
def build_request(method, url)
|
226
|
+
url = URI.parse(url)
|
227
|
+
url = @base_url + url if @base_url
|
228
|
+
|
229
|
+
unless %w[http https].include?(url.scheme)
|
230
|
+
raise ArgumentError, "Only HTTP and HTTPS URLs are allowed"
|
231
|
+
end
|
232
|
+
|
233
|
+
url.query = URI.encode_www_form(@query)
|
234
|
+
|
235
|
+
net_request = case method.to_s
|
236
|
+
when 'get'
|
237
|
+
@body_type = nil
|
238
|
+
Net::HTTP::Get.new(url)
|
239
|
+
when 'head'
|
240
|
+
@body_type = nil
|
241
|
+
Net::HTTP::Head.new(url)
|
242
|
+
when 'post'
|
243
|
+
Net::HTTP::Post.new(url)
|
244
|
+
when 'put'
|
245
|
+
Net::HTTP::Put.new(url)
|
246
|
+
when 'patch'
|
247
|
+
Net::HTTP::Patch.new(url)
|
248
|
+
when 'delete'
|
249
|
+
Net::HTTP::Delete.new(url)
|
250
|
+
else
|
251
|
+
raise ArgumentError, "Unsupported HTTP method #{method}"
|
252
|
+
end
|
253
|
+
|
254
|
+
# add body
|
255
|
+
case @body_type
|
256
|
+
when nil
|
257
|
+
# Ignore body
|
258
|
+
when :raw
|
259
|
+
net_request.body = @body
|
260
|
+
when :json
|
261
|
+
net_request.body = @body.to_json if @body
|
262
|
+
when :form
|
263
|
+
net_request.body = URI.encode_www_form(@body) if @body
|
264
|
+
when :multipart
|
265
|
+
params = []
|
266
|
+
params.concat(@body.map { |k, v| [k.to_s, v.to_s] }) if Hash === @body
|
267
|
+
params.concat(@files)
|
268
|
+
net_request.set_form(params, 'multipart/form-data')
|
269
|
+
else
|
270
|
+
raise ArgumentError, "Invalid body type #{@body_type}"
|
271
|
+
end
|
272
|
+
|
273
|
+
# add headers
|
274
|
+
@headers.each do |k, v|
|
275
|
+
net_request.send(:set_field, k, v)
|
276
|
+
end
|
277
|
+
|
278
|
+
options = {
|
279
|
+
use_ssl: url.scheme == 'https',
|
280
|
+
open_timeout: @open_timeout,
|
281
|
+
write_timeout: @write_timeout,
|
282
|
+
read_timeout: @read_timeout,
|
283
|
+
ca: @ca,
|
284
|
+
cert: @cert,
|
285
|
+
key: @key,
|
286
|
+
proxy_address: @proxy_address,
|
287
|
+
proxy_port: @proxy_port,
|
288
|
+
proxy_user: @proxy_user,
|
289
|
+
proxy_pass: @proxy_pass,
|
290
|
+
}
|
291
|
+
|
292
|
+
[url, net_request, options]
|
293
|
+
end
|
294
|
+
|
295
|
+
def with_client(hostname, port, options = {})
|
296
|
+
raise ArgumentError, 'Block is required' unless block_given?
|
297
|
+
|
298
|
+
# Get client from pool
|
299
|
+
net_http = @pool&.acquire(hostname, port, options)
|
300
|
+
|
301
|
+
# make a new client if there is no pool
|
302
|
+
unless net_http
|
303
|
+
net_http = net_factory.make_client(hostname, port, options)
|
304
|
+
end
|
305
|
+
|
306
|
+
begin
|
307
|
+
net_http.start unless net_http.started?
|
308
|
+
yield(net_http)
|
309
|
+
ensure
|
310
|
+
if @pool
|
311
|
+
@pool.release(net_http)
|
312
|
+
else
|
313
|
+
net_http.finish
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def net_factory
|
319
|
+
@net_factory ||= NetHttpFactory.new
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MintHttp
|
4
|
+
class Response
|
5
|
+
attr_reader :net_response
|
6
|
+
attr_reader :version
|
7
|
+
attr_reader :status_code
|
8
|
+
attr_reader :status_text
|
9
|
+
attr_reader :headers
|
10
|
+
|
11
|
+
def initialize(net_response)
|
12
|
+
@net_response = net_response
|
13
|
+
@version = net_response.http_version
|
14
|
+
@status_code = net_response.code.to_i
|
15
|
+
@status_text = net_response.message
|
16
|
+
@headers = Headers.new.merge(net_response.each_header.to_h)
|
17
|
+
end
|
18
|
+
|
19
|
+
def success?
|
20
|
+
(200..299).include?(@status_code)
|
21
|
+
end
|
22
|
+
|
23
|
+
def redirect?
|
24
|
+
(300..399).include?(@status_code)
|
25
|
+
end
|
26
|
+
|
27
|
+
def client_error?
|
28
|
+
(400..499).include?(@status_code)
|
29
|
+
end
|
30
|
+
|
31
|
+
def unauthenticated?
|
32
|
+
@status_code == 401
|
33
|
+
end
|
34
|
+
|
35
|
+
def unauthorized?
|
36
|
+
@status_code == 403
|
37
|
+
end
|
38
|
+
|
39
|
+
def not_found?
|
40
|
+
@status_code == 404
|
41
|
+
end
|
42
|
+
|
43
|
+
def server_error?
|
44
|
+
(500..599).include?(@status_code)
|
45
|
+
end
|
46
|
+
|
47
|
+
def raise!
|
48
|
+
case @status_code
|
49
|
+
when 401
|
50
|
+
raise Errors::AuthenticationError.new('Unauthenticated', self)
|
51
|
+
when 403
|
52
|
+
raise Errors::AuthorizationError.new('Forbidden', self)
|
53
|
+
when 404
|
54
|
+
raise Errors::NotFoundError.new('Not Found', self)
|
55
|
+
when 400..499
|
56
|
+
raise Errors::ClientError.new('Client Error', self)
|
57
|
+
when 500..599
|
58
|
+
raise Errors::ClientError.new('Server Error', self)
|
59
|
+
else
|
60
|
+
self
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def body
|
65
|
+
net_response.body
|
66
|
+
end
|
67
|
+
|
68
|
+
def json
|
69
|
+
@json ||= JSON.parse(body)
|
70
|
+
end
|
71
|
+
|
72
|
+
def inspect
|
73
|
+
"#<#{self.class}/#{@version} #{@status_code} #{@status_text} #{@headers.inspect}>"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/mint_http.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'mint_http/version'
|
4
|
+
require_relative 'mint_http/pool_entry'
|
5
|
+
require_relative 'mint_http/pool'
|
6
|
+
require_relative 'mint_http/headers'
|
7
|
+
require_relative 'mint_http/errors/response_error'
|
8
|
+
require_relative 'mint_http/errors/server_error'
|
9
|
+
require_relative 'mint_http/errors/client_error'
|
10
|
+
require_relative 'mint_http/errors/authorization_error'
|
11
|
+
require_relative 'mint_http/errors/authentication_error'
|
12
|
+
require_relative 'mint_http/errors/not_found_error'
|
13
|
+
require_relative 'mint_http/net_http_factory'
|
14
|
+
require_relative 'mint_http/response'
|
15
|
+
require_relative 'mint_http/request'
|
16
|
+
|
17
|
+
module MintHttp
|
18
|
+
class << self
|
19
|
+
# @return [::MintHttp::Request]
|
20
|
+
def method_missing(method, *args)
|
21
|
+
request = Request.new
|
22
|
+
request.send(method, *args)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/sig/mint_http.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mint_http
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ali Alhoshaiyan
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-07-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: net-http
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: openssl
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: json
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: uri
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: base64
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Like a mint breeze, MintHttp allows you to write simple HTTP requests
|
84
|
+
while giving you the full power of Net::HTTP.
|
85
|
+
email:
|
86
|
+
- ahoshaiyan@fastmail.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- CHANGELOG.md
|
92
|
+
- Gemfile
|
93
|
+
- Gemfile.lock
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- lib/mint_http.rb
|
98
|
+
- lib/mint_http/errors/authentication_error.rb
|
99
|
+
- lib/mint_http/errors/authorization_error.rb
|
100
|
+
- lib/mint_http/errors/client_error.rb
|
101
|
+
- lib/mint_http/errors/not_found_error.rb
|
102
|
+
- lib/mint_http/errors/response_error.rb
|
103
|
+
- lib/mint_http/errors/server_error.rb
|
104
|
+
- lib/mint_http/headers.rb
|
105
|
+
- lib/mint_http/net_http_factory.rb
|
106
|
+
- lib/mint_http/pool.rb
|
107
|
+
- lib/mint_http/pool_entry.rb
|
108
|
+
- lib/mint_http/request.rb
|
109
|
+
- lib/mint_http/response.rb
|
110
|
+
- lib/mint_http/version.rb
|
111
|
+
- sig/mint_http.rbs
|
112
|
+
homepage: https://github.com/ahoshaiyan/mint_http
|
113
|
+
licenses:
|
114
|
+
- MIT
|
115
|
+
metadata:
|
116
|
+
homepage_uri: https://github.com/ahoshaiyan/mint_http
|
117
|
+
source_code_uri: https://github.com/ahoshaiyan/mint_http
|
118
|
+
post_install_message:
|
119
|
+
rdoc_options: []
|
120
|
+
require_paths:
|
121
|
+
- lib
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: 3.0.6
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubygems_version: 3.2.33
|
134
|
+
signing_key:
|
135
|
+
specification_version: 4
|
136
|
+
summary: A small fluent HTTP client.
|
137
|
+
test_files: []
|