mint_http 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []