mint_http 0.1.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 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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-07-03
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in mint_http.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MintHttp::AuthenticationError < MintHttp::ClientError
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MintHttp::AuthorizationError < MintHttp::ClientError
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MintHttp::ClientError < MintHttp::ResponseError
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MintHttp::NotFoundError < MintHttp::ClientError
4
+ end
@@ -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,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MintHttp::ServerError < MintHttp::ResponseError
4
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MintHttp::Headers < Hash
4
+ def [](key)
5
+ super(key.downcase)
6
+ end
7
+
8
+ def []=(key, value)
9
+ super(key.downcase, value)
10
+ end
11
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MintHttp
4
+ VERSION = "0.1.0"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module MintHttp
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []