ueki 1.0.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +51 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +162 -0
- data/Rakefile +12 -0
- data/lib/ueki/http_client/default_requester.rb +137 -0
- data/lib/ueki/http_client/exception_class_builder.rb +69 -0
- data/lib/ueki/http_client/json_response_body_parser.rb +21 -0
- data/lib/ueki/http_client/request_body_converter.rb +25 -0
- data/lib/ueki/http_client/requester_shorthand.rb +28 -0
- data/lib/ueki/http_client/unsuccessful_response_exception_class_picker.rb +31 -0
- data/lib/ueki/http_client.rb +37 -0
- data/lib/ueki/version.rb +5 -0
- data/lib/ueki.rb +7 -0
- metadata +64 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a26fe40e5550c573ba85ceaf58122fa63cf27c358d579b14b9c72fbe985eb637
|
4
|
+
data.tar.gz: e3391e1a4302b29ed51f0379f268bd4dfff89e05a1d406a04d4b87e8fcf8424d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: de1e9a7eb8b7a79609ff6e78f6b0a557e85a4b907f43348aba985a3dbee989061dbf0d10a6e35060e2ea02cd2b15a5161915ee70885c12e1928bee671622b882
|
7
|
+
data.tar.gz: 1c6f4e75b67b4b7b9e5f77763bab3fcdd7411884fb9b0629018861831a0573602e13b6c074aa301e0ce835335663b16865cdb0e7e287eccb735f14ec96667052
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-performance
|
3
|
+
- rubocop-rake
|
4
|
+
- rubocop-rspec
|
5
|
+
|
6
|
+
AllCops:
|
7
|
+
TargetRubyVersion: 3.1
|
8
|
+
NewCops: enable
|
9
|
+
|
10
|
+
Security/IoMethods:
|
11
|
+
Exclude:
|
12
|
+
- "spec/ueki/http_client/fake_requester.rb"
|
13
|
+
|
14
|
+
Style/Documentation:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Style/StringLiterals:
|
18
|
+
EnforcedStyle: double_quotes
|
19
|
+
|
20
|
+
Style/StringLiteralsInInterpolation:
|
21
|
+
EnforcedStyle: double_quotes
|
22
|
+
|
23
|
+
Layout/LineLength:
|
24
|
+
Enabled: false
|
25
|
+
|
26
|
+
Metrics/AbcSize:
|
27
|
+
Enabled: false
|
28
|
+
|
29
|
+
Metrics/CyclomaticComplexity:
|
30
|
+
Enabled: false
|
31
|
+
|
32
|
+
Metrics/MethodLength:
|
33
|
+
Enabled: false
|
34
|
+
|
35
|
+
Metrics/ParameterLists:
|
36
|
+
Enabled: false
|
37
|
+
|
38
|
+
Naming/MemoizedInstanceVariableName:
|
39
|
+
EnforcedStyleForLeadingUnderscores: required
|
40
|
+
|
41
|
+
RSpec/DescribeClass:
|
42
|
+
Enabled: false
|
43
|
+
|
44
|
+
RSpec/ExampleLength:
|
45
|
+
Enabled: false
|
46
|
+
|
47
|
+
RSpec/MultipleExpectations:
|
48
|
+
Enabled: false
|
49
|
+
|
50
|
+
RSpec/NestedGroups:
|
51
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Tomohiko Mimura
|
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,162 @@
|
|
1
|
+
# Ueki
|
2
|
+
|
3
|
+
Ueki provides "definition of simple request methods such as `get`, `post`, etc." and "definition and handling of error exception classes for timeout error and each status code" required for HTTP Client Library.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Install the gem and add to the application's Gemfile by executing:
|
8
|
+
|
9
|
+
$ bundle add ueki
|
10
|
+
|
11
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
12
|
+
|
13
|
+
$ gem install ueki
|
14
|
+
|
15
|
+
> [!CAUTION]
|
16
|
+
> `Ueki` uses faraday by default.
|
17
|
+
> However, it is possible to choose not to use faraday, so there is no dependency.
|
18
|
+
> If you want to use faraday, please install [faraday](https://github.com/lostisland/faraday).
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
Simply include the Module created by Ueki in your own HTTP Client Library.
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
class BookStoreClient
|
26
|
+
include Ueki::HttpClient.new('http://example.com')
|
27
|
+
|
28
|
+
# Class Method
|
29
|
+
def self.delete_book(id)
|
30
|
+
delete("/books/#{id}")
|
31
|
+
end
|
32
|
+
|
33
|
+
# Instance Method
|
34
|
+
def books(limit:, offset: 0)
|
35
|
+
get('/books', params: { limit: limit, offset: offset })
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def default_headers
|
41
|
+
h = super
|
42
|
+
h['X-Request-Id'] = Current.request_id if Current.request_id.present?
|
43
|
+
h
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
BookStoreClient.new.post('/books', params: { title: 'Programming Ruby' })
|
48
|
+
#=> { id: 1, title: 'Programming Ruby' }
|
49
|
+
|
50
|
+
BookStoreClient.new.books(limit: 5)
|
51
|
+
#=> { books: [{ id: 1, title: 'Programming Ruby' }] }
|
52
|
+
|
53
|
+
BookStoreClient.delete_book(1)
|
54
|
+
#=> nil
|
55
|
+
|
56
|
+
BookStoreClient.get('/books/1')
|
57
|
+
#=> BookStoreClient::NotFoundError (status: 404, body: { message: 'Not Found' })
|
58
|
+
```
|
59
|
+
|
60
|
+
Exception classes are defined according to the following tree structure.
|
61
|
+
```
|
62
|
+
BookStoreClient
|
63
|
+
└── Error
|
64
|
+
├── RequestError
|
65
|
+
│ ├── TimeoutError
|
66
|
+
│ └── UnexpectedError
|
67
|
+
└── UnsuccessfulResponseError
|
68
|
+
├── BadRequestError
|
69
|
+
│ ├── UnauthorizedError
|
70
|
+
│ ├── ForbiddenError
|
71
|
+
│ ├── NotFoundError
|
72
|
+
│ ├── RequestTimeoutError
|
73
|
+
│ ├── ConflictError
|
74
|
+
│ ├── UnprocessableEntityError
|
75
|
+
│ └── TooManyRequestsError
|
76
|
+
└── ServerError
|
77
|
+
```
|
78
|
+
|
79
|
+
## Why not faraday?
|
80
|
+
|
81
|
+
faraday has enough features.
|
82
|
+
However, all request error exceptions are subclasses of `Faraday::Error`.
|
83
|
+
|
84
|
+
That behavior does not allow for proper exception handling, so we repeatedly implement a process that replaces the exception.
|
85
|
+
```ruby
|
86
|
+
class BookStoreClient
|
87
|
+
class Error < StandardError; end
|
88
|
+
class ClientError < Error; end
|
89
|
+
class ServerError < Error; end
|
90
|
+
|
91
|
+
def books(limit:, offset: 0)
|
92
|
+
connection = Faraday.new("https://example.com") do |builder|
|
93
|
+
builder.response :raise_error
|
94
|
+
end
|
95
|
+
|
96
|
+
connection.get("/books")
|
97
|
+
rescue Faraday::Error::ClientError
|
98
|
+
raise ClientError
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
Ueki frees us from this tedious and redundant implementation!
|
104
|
+
|
105
|
+
## How to customize
|
106
|
+
|
107
|
+
"Request Processing" is not important for Ueki.
|
108
|
+
Therefore, this part can be freely customized.
|
109
|
+
|
110
|
+
There are two ways to customize.
|
111
|
+
|
112
|
+
### Q. I enable keep alive and don't want to output logs?
|
113
|
+
|
114
|
+
A. You can use [net_http_persistent](https://github.com/lostisland/faraday-net_http_persistent) adapter by overriding `_initialize_faraday_connection`. Set up your favorite faraday middleware to match.
|
115
|
+
|
116
|
+
See [lib/ueki/http_client/default_requester.rb](https://github.com/tmimura39/ueki/blob/main/lib/ueki/http_client/default_requester.rb) for details.
|
117
|
+
```ruby
|
118
|
+
class BookStoreClient
|
119
|
+
include Ueki::HttpClient.new('http://example.com')
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def _initialize_faraday_connection(request_options)
|
124
|
+
Faraday.new(url: self.class::ENDPOINT, headers: _default_headers, request: request_options) do |builder|
|
125
|
+
builder.adapter :net_http_persistent, pool_size: 5 do |http|
|
126
|
+
http.idle_timeout = 100
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
### Q. How can I use faraday independent request processing across multiple Client Libraries?
|
134
|
+
|
135
|
+
A. It is recommended to create a Requester module.
|
136
|
+
|
137
|
+
A module like [lib/ueki/http_client/default_requester.rb](https://github.com/tmimura39/ueki/blob/main/lib/ueki/http_client/default_requester.rb) could be created and applied to Ueki.
|
138
|
+
In this case, you can use "automatic exception class definition" and "exception class acquisition processing according to status code ( `#pickup_unsuccessful_response_exception_class` )"
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
class BookStoreClient
|
142
|
+
include Ueki::HttpClient.new('http://example.com', requester: CustomRequester)
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
I am happy with my current DefaultRequester.
|
147
|
+
So this customization method is not sophisticated.
|
148
|
+
I will gladly accept any better suggestions.
|
149
|
+
|
150
|
+
## Development
|
151
|
+
|
152
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
153
|
+
|
154
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
155
|
+
|
156
|
+
## Contributing
|
157
|
+
|
158
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/tmimura39/ueki.
|
159
|
+
|
160
|
+
## License
|
161
|
+
|
162
|
+
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,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faraday"
|
4
|
+
|
5
|
+
module Ueki
|
6
|
+
class HttpClient
|
7
|
+
# HTTP Request processing is defined.
|
8
|
+
# You can also use your own Requester class with the same I/F.
|
9
|
+
module DefaultRequester
|
10
|
+
UNSPECIFIED = Object.new.freeze
|
11
|
+
private_constant :UNSPECIFIED
|
12
|
+
|
13
|
+
def get(path, params: nil, headers: {}, request_options: {}, response_body_parser: UNSPECIFIED)
|
14
|
+
response_body_parser = default_response_body_parser_if_unspecifiied(response_body_parser)
|
15
|
+
request(:get, path:, params:, headers:, request_options:, response_body_parser:)
|
16
|
+
end
|
17
|
+
|
18
|
+
def post(path, params: nil, headers: {}, request_options: {}, request_body_converter: UNSPECIFIED, response_body_parser: UNSPECIFIED)
|
19
|
+
response_body_parser = default_response_body_parser_if_unspecifiied(response_body_parser)
|
20
|
+
request_body_converter = default_request_body_converter_if_unspecified(request_body_converter)
|
21
|
+
|
22
|
+
headers[:"Content-Type"] ||= "application/json" unless params.nil?
|
23
|
+
params = request_body_converter.call(content_type: headers[:"Content-Type"], params:)
|
24
|
+
request(:post, path:, params:, headers:, request_options:, response_body_parser:)
|
25
|
+
end
|
26
|
+
|
27
|
+
def put(path, params: nil, headers: {}, request_options: {}, request_body_converter: UNSPECIFIED, response_body_parser: UNSPECIFIED)
|
28
|
+
response_body_parser = default_response_body_parser_if_unspecifiied(response_body_parser)
|
29
|
+
request_body_converter = default_request_body_converter_if_unspecified(request_body_converter)
|
30
|
+
|
31
|
+
headers[:"Content-Type"] ||= "application/json" unless params.nil?
|
32
|
+
params = request_body_converter.call(content_type: headers[:"Content-Type"], params:)
|
33
|
+
request(:put, path:, params:, headers:, request_options:, response_body_parser:)
|
34
|
+
end
|
35
|
+
|
36
|
+
def patch(path, params: nil, headers: {}, request_options: {}, request_body_converter: UNSPECIFIED, response_body_parser: UNSPECIFIED)
|
37
|
+
response_body_parser = default_response_body_parser_if_unspecifiied(response_body_parser)
|
38
|
+
request_body_converter = default_request_body_converter_if_unspecified(request_body_converter)
|
39
|
+
|
40
|
+
headers[:"Content-Type"] ||= "application/json" unless params.nil?
|
41
|
+
params = request_body_converter.call(content_type: headers[:"Content-Type"], params:)
|
42
|
+
request(:patch, path:, params:, headers:, request_options:, response_body_parser:)
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete(path, params: nil, headers: {}, request_options: {}, response_body_parser: UNSPECIFIED)
|
46
|
+
response_body_parser = default_response_body_parser_if_unspecifiied(response_body_parser)
|
47
|
+
request(:delete, path:, params:, headers:, request_options:, response_body_parser:)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def default_response_body_parser_if_unspecifiied(response_body_parser)
|
53
|
+
if response_body_parser == UNSPECIFIED
|
54
|
+
require_relative "json_response_body_parser"
|
55
|
+
JsonResponseBodyParser
|
56
|
+
else
|
57
|
+
response_body_parser
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def default_request_body_converter_if_unspecified(request_body_converter)
|
62
|
+
if request_body_converter == UNSPECIFIED
|
63
|
+
require_relative "request_body_converter"
|
64
|
+
RequestBodyConverter
|
65
|
+
else
|
66
|
+
request_body_converter
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def request(method, path:, params:, headers:, request_options:, response_body_parser:)
|
71
|
+
response = _with_request_error_handling do
|
72
|
+
_request(method, path:, params:, headers:, request_options:)
|
73
|
+
end
|
74
|
+
assert_successful_response(response:, response_body_parser:)
|
75
|
+
|
76
|
+
response_body_parser.nil? ? response.body : response_body_parser.call(response.body)
|
77
|
+
end
|
78
|
+
|
79
|
+
def assert_successful_response(response:, response_body_parser:)
|
80
|
+
status = _pickup_status(response)
|
81
|
+
exception_class = pickup_unsuccessful_response_exception_class(status)
|
82
|
+
return if exception_class.nil?
|
83
|
+
|
84
|
+
message, status, body, headers = _extract_from_raw_response(response:, response_body_parser:)
|
85
|
+
raise exception_class.new(message, status:, body:, headers:, response:)
|
86
|
+
end
|
87
|
+
|
88
|
+
# ===========================================================================================
|
89
|
+
# The methods defined below depend on faraday.
|
90
|
+
# If you want to customize faraday or use another HTTP library, override the methods.
|
91
|
+
# If no customization is required, use the default.
|
92
|
+
|
93
|
+
def _request(method, path:, params:, headers:, request_options:)
|
94
|
+
_faraday_connection(request_options).public_send(method, path, params, headers)
|
95
|
+
end
|
96
|
+
|
97
|
+
def _faraday_connection(request_options)
|
98
|
+
@_faraday_connection ||= {}
|
99
|
+
@_faraday_connection[request_options] ||= _initialize_faraday_connection(request_options)
|
100
|
+
end
|
101
|
+
|
102
|
+
def _initialize_faraday_connection(request_options)
|
103
|
+
Faraday.new(url: self.class::ENDPOINT, headers: _default_headers, request: request_options) do |builder|
|
104
|
+
builder.response :logger, (defined?(Rails) ? Rails.logger : nil) do |logger|
|
105
|
+
logger.filter(/Authorization:\ (.*)/, "Authorization: [token]")
|
106
|
+
end
|
107
|
+
|
108
|
+
builder.adapter :net_http
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def _default_headers
|
113
|
+
{ user_agent: self.class.name }
|
114
|
+
end
|
115
|
+
|
116
|
+
def _pickup_status(response)
|
117
|
+
response.status
|
118
|
+
end
|
119
|
+
|
120
|
+
def _extract_from_raw_response(response:, response_body_parser:)
|
121
|
+
message = response.to_hash.except(:request_headers, :response)
|
122
|
+
status = response.status
|
123
|
+
body = response_body_parser.call(response.body)
|
124
|
+
headers = response.headers
|
125
|
+
[message, status, body, headers]
|
126
|
+
end
|
127
|
+
|
128
|
+
def _with_request_error_handling
|
129
|
+
yield
|
130
|
+
rescue Faraday::TimeoutError => e
|
131
|
+
raise self.class::TimeoutError, e
|
132
|
+
rescue StandardError => e
|
133
|
+
raise self.class::UnexpectedError, e
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ueki
|
4
|
+
class HttpClient
|
5
|
+
class ExceptionClassBuilder
|
6
|
+
def initialize
|
7
|
+
@error = Class.new(StandardError)
|
8
|
+
@request_error = build_request_error(@error)
|
9
|
+
@timeout_error = Class.new(@request_error)
|
10
|
+
@unexpected_error = Class.new(@request_error)
|
11
|
+
@unsuccessful_response_error = build_unsuccessful_response_error(@error)
|
12
|
+
@bad_request_error = Class.new(@unsuccessful_response_error)
|
13
|
+
@unauthorized_error = Class.new(@bad_request_error)
|
14
|
+
@forbidden_error = Class.new(@bad_request_error)
|
15
|
+
@not_found_error = Class.new(@bad_request_error)
|
16
|
+
@request_timeout_error = Class.new(@bad_request_error)
|
17
|
+
@conflict_error = Class.new(@bad_request_error)
|
18
|
+
@unprocessable_entity_error = Class.new(@bad_request_error)
|
19
|
+
@too_many_requests_error = Class.new(@bad_request_error)
|
20
|
+
@server_error = Class.new(@unsuccessful_response_error)
|
21
|
+
end
|
22
|
+
|
23
|
+
def exception_classes
|
24
|
+
{
|
25
|
+
Error: @error,
|
26
|
+
RequestError: @request_error,
|
27
|
+
TimeoutError: @timeout_error,
|
28
|
+
UnexpectedError: @unexpected_error,
|
29
|
+
UnsuccessfulResponseError: @unsuccessful_response_error,
|
30
|
+
BadRequestError: @bad_request_error,
|
31
|
+
UnauthorizedError: @unauthorized_error,
|
32
|
+
ForbiddenError: @forbidden_error,
|
33
|
+
NotFoundError: @not_found_error,
|
34
|
+
RequestTimeoutError: @request_timeout_error,
|
35
|
+
ConflictError: @conflict_error,
|
36
|
+
UnprocessableEntityError: @unprocessable_entity_error,
|
37
|
+
TooManyRequestsError: @too_many_requests_error,
|
38
|
+
ServerError: @server_error
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def build_request_error(error)
|
45
|
+
Class.new(error) do
|
46
|
+
def initialize(exception)
|
47
|
+
super("#{exception.class.name}: #{exception.message}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_unsuccessful_response_error(error)
|
53
|
+
Class.new(error) do
|
54
|
+
attr_reader :status, :body, :headers, :response
|
55
|
+
|
56
|
+
def initialize(message, status:, body:, headers:, response:)
|
57
|
+
@status = status
|
58
|
+
@body = body
|
59
|
+
@headers = headers
|
60
|
+
@response = response
|
61
|
+
|
62
|
+
super(message)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
private_constant :ExceptionClassBuilder
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Ueki
|
6
|
+
class HttpClient
|
7
|
+
# Simple JSON Parser (default)
|
8
|
+
# If it cannot be parsed as JSON, return the value before parsing
|
9
|
+
module JsonResponseBodyParser
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def call(body)
|
13
|
+
return if body.nil? || (body.is_a?(String) && body.empty?)
|
14
|
+
|
15
|
+
JSON.parse(body, symbolize_names: true)
|
16
|
+
rescue JSON::ParserError
|
17
|
+
body
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module Ueki
|
7
|
+
class HttpClient
|
8
|
+
module RequestBodyConverter
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def call(content_type:, params:)
|
12
|
+
return if params.nil?
|
13
|
+
|
14
|
+
case content_type
|
15
|
+
when "application/json"
|
16
|
+
params&.to_json
|
17
|
+
when "application/x-www-form-urlencoded"
|
18
|
+
URI.encode_www_form(params)
|
19
|
+
else
|
20
|
+
params
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ueki
|
4
|
+
class HttpClient
|
5
|
+
module RequesterShorthand
|
6
|
+
def get(path, **args)
|
7
|
+
new.get(path, **args)
|
8
|
+
end
|
9
|
+
|
10
|
+
def post(path, **args)
|
11
|
+
new.post(path, **args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def put(path, **args)
|
15
|
+
new.put(path, **args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def patch(path, **args)
|
19
|
+
new.patch(path, **args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete(path, **args)
|
23
|
+
new.delete(path, **args)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
private_constant :RequesterShorthand
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ueki
|
4
|
+
class HttpClient
|
5
|
+
module UnsuccessfulResponseExceptionClassPicker
|
6
|
+
def pickup_unsuccessful_response_exception_class(status)
|
7
|
+
case status
|
8
|
+
when 401
|
9
|
+
self.class::UnauthorizedError
|
10
|
+
when 403
|
11
|
+
self.class::ForbiddenError
|
12
|
+
when 404
|
13
|
+
self.class::NotFoundError
|
14
|
+
when 408
|
15
|
+
self.class::RequestTimeoutError
|
16
|
+
when 409
|
17
|
+
self.class::ConflictError
|
18
|
+
when 422
|
19
|
+
self.class::UnprocessableEntityError
|
20
|
+
when 429
|
21
|
+
self.class::TooManyRequestsError
|
22
|
+
when 400..499
|
23
|
+
self.class::BadRequestError
|
24
|
+
when 500..599
|
25
|
+
self.class::ServerError
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
private_constant :UnsuccessfulResponseExceptionClassPicker
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ueki
|
4
|
+
# Provides a module that automatically defines exception classes and methods required for the HTTP Client Library.
|
5
|
+
class HttpClient < Module
|
6
|
+
def initialize(endpoint, requester: nil)
|
7
|
+
super()
|
8
|
+
@endpoint = endpoint
|
9
|
+
|
10
|
+
@requester =
|
11
|
+
if requester.nil?
|
12
|
+
require_relative "http_client/default_requester"
|
13
|
+
DefaultRequester
|
14
|
+
else
|
15
|
+
requester
|
16
|
+
end
|
17
|
+
|
18
|
+
@exception_classes = ExceptionClassBuilder.new.exception_classes
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def included(descendant)
|
24
|
+
descendant.const_set(:ENDPOINT, @endpoint)
|
25
|
+
@exception_classes.each_pair do |name, object|
|
26
|
+
descendant.const_set(name, object)
|
27
|
+
end
|
28
|
+
descendant.include @requester
|
29
|
+
descendant.include UnsuccessfulResponseExceptionClassPicker
|
30
|
+
descendant.extend RequesterShorthand
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
require_relative "http_client/exception_class_builder"
|
36
|
+
require_relative "http_client/requester_shorthand"
|
37
|
+
require_relative "http_client/unsuccessful_response_exception_class_picker"
|
data/lib/ueki/version.rb
ADDED
data/lib/ueki.rb
ADDED
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ueki
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tomohiko Mimura
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-08-05 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Ueki provides "simple request method" and "error exception class definition
|
14
|
+
and handling"
|
15
|
+
email:
|
16
|
+
- mito.5525@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- ".rspec"
|
22
|
+
- ".rubocop.yml"
|
23
|
+
- CHANGELOG.md
|
24
|
+
- LICENSE.txt
|
25
|
+
- README.md
|
26
|
+
- Rakefile
|
27
|
+
- lib/ueki.rb
|
28
|
+
- lib/ueki/http_client.rb
|
29
|
+
- lib/ueki/http_client/default_requester.rb
|
30
|
+
- lib/ueki/http_client/exception_class_builder.rb
|
31
|
+
- lib/ueki/http_client/json_response_body_parser.rb
|
32
|
+
- lib/ueki/http_client/request_body_converter.rb
|
33
|
+
- lib/ueki/http_client/requester_shorthand.rb
|
34
|
+
- lib/ueki/http_client/unsuccessful_response_exception_class_picker.rb
|
35
|
+
- lib/ueki/version.rb
|
36
|
+
homepage: https://github.com/tmimura39/ueki
|
37
|
+
licenses:
|
38
|
+
- MIT
|
39
|
+
metadata:
|
40
|
+
allowed_push_host: https://rubygems.org
|
41
|
+
homepage_uri: https://github.com/tmimura39/ueki
|
42
|
+
source_code_uri: https://github.com/tmimura39/ueki
|
43
|
+
changelog_uri: https://github.com/tmimura39/ueki/tree/main/CHANGELOG.md
|
44
|
+
rubygems_mfa_required: 'true'
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 3.1.0
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubygems_version: 3.5.17
|
61
|
+
signing_key:
|
62
|
+
specification_version: 4
|
63
|
+
summary: Module to assist in creating your Own HTTP client library
|
64
|
+
test_files: []
|