api-response-presenter 0.0.1
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/CHANGELOG.md +3 -0
- data/LICENSE +21 -0
- data/README.md +324 -0
- data/api_response_presenter.gemspec +33 -0
- data/lib/api_response/parser.rb +16 -0
- data/lib/api_response/presenter.rb +23 -0
- data/lib/api_response/processor/failure.rb +63 -0
- data/lib/api_response/processor/success.rb +51 -0
- data/lib/api_response/processor.rb +36 -0
- data/lib/api_response/types.rb +75 -0
- data/lib/api_response/version.rb +5 -0
- data/lib/api_response.rb +29 -0
- metadata +167 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 34e509264188009fe9c15dd6c38c58286b338a688e33372f1e15ab690e4f7416
|
4
|
+
data.tar.gz: a3365ff34ea26895b3393dccc4c450ccb6645dd7d7410931c2ba4c47e1c16690
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f205f9e01e0853e9afed337b6460b3672984bb632eb2e6d7ed6ea9a41d31d9bf3e3ca2cd265f9929a856b304b4f8b2b909a470ab7e5d9ee319d6ac0725e02b68
|
7
|
+
data.tar.gz: 3fb35b7a9b8def679a29868f4b9cab6fd7cd9a48a653e3d5a5329b8fcbf9a72ec9bf132cee5bcc67ee3c64e54debc3c045abdcd6ce3d4c6e991e2c2c08cacc6f
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 David Rybolovlev
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,324 @@
|
|
1
|
+
# api-response-presenter
|
2
|
+
[](https://badge.fury.io/rb/api-response-presenter) [](https://app.travis-ci.com/golifox/api-response-presenter)
|
3
|
+
[](https://coveralls.io/github/golifox/api-response-presenter)
|
4
|
+
[](https://inch-ci.org/github/golifox/api-response-presenter)
|
5
|
+
|
6
|
+
The `api-response-presenter` gem provides a flexible and easy-to-use interface for processing API responses using Faraday or
|
7
|
+
RestClient with the possibility to configure global settings or per-instance settings. It leverages
|
8
|
+
the `Dry::Configurable` for configurations, ensuring high performance and full test coverage.
|
9
|
+
|
10
|
+
## Supported Ruby Versions
|
11
|
+
This library oficially supports the following Ruby versions:
|
12
|
+
- MRI `>=2.7.4`
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'api-response-presenter'
|
20
|
+
```
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
```bash
|
25
|
+
bundle install
|
26
|
+
```
|
27
|
+
|
28
|
+
Or install it yourself as:
|
29
|
+
|
30
|
+
```bash
|
31
|
+
gem install api-response-presenter
|
32
|
+
```
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
### Configuration
|
37
|
+
|
38
|
+
You can configure api_response globally in an initializer or setup block:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
# config/initializers/api_response.rb
|
42
|
+
|
43
|
+
ApiResponse.config.some_option = 'some_value'
|
44
|
+
|
45
|
+
# or
|
46
|
+
|
47
|
+
ApiResponse.configure do |config|
|
48
|
+
config.adapter = :faraday # or :rest_client, :excon, :http
|
49
|
+
config.monad = false
|
50
|
+
config.extract_from_body = ->(body) { body }
|
51
|
+
config.struct = nil
|
52
|
+
config.raw_response = false
|
53
|
+
config.error_json = false
|
54
|
+
config.default_return_value = nil
|
55
|
+
config.default_status = :conflict
|
56
|
+
config.default_error_key = :external_api_error
|
57
|
+
config.default_error = 'External Api error'
|
58
|
+
|
59
|
+
# dependency injection
|
60
|
+
config.success_processor = ApiResponse::Processor::Success
|
61
|
+
config.failure_processor = ApiResponse::Processor::Failure
|
62
|
+
config.parser = ApiResponse::Parser
|
63
|
+
config.options = {}
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
or on instance config, provide block (see: BasicUsage).
|
68
|
+
|
69
|
+
### Basic Example
|
70
|
+
|
71
|
+
Here is a basic example of using api_response to process an API response:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
response = Faraday.get('https://api.example.com/data')
|
75
|
+
result = ApiResponse::Presenter.call(response) do |config|
|
76
|
+
config.monad = true
|
77
|
+
end
|
78
|
+
|
79
|
+
# or
|
80
|
+
# Usefull for using in another libraries
|
81
|
+
result ||= ApiResponse::Presenter.call(response, monad: true)
|
82
|
+
|
83
|
+
if result.success?
|
84
|
+
puts "Success: #{result.success}"
|
85
|
+
else
|
86
|
+
puts "Error: #{result.failure}"
|
87
|
+
end
|
88
|
+
|
89
|
+
```
|
90
|
+
|
91
|
+
### Config options
|
92
|
+
|
93
|
+
- `ApiResponse.config.adapter`: response adapter that you are using.
|
94
|
+
- Default: `:faraday`.
|
95
|
+
- Available values: `:rest_client`, `:excon`, `:http` and others. Checks that response respond to `#status` (only Faraday and Excon)
|
96
|
+
or `#code` (others)
|
97
|
+
- `ApiResponse.config.monad` wrap result into [dry-monads](https://github.com/dry-rb/dry-monads)
|
98
|
+
- Default: `false`
|
99
|
+
- Example: `ApiResponse::Presenter.call(response, monad: true) # => Success({})` or `Failure({error:, status:, error_key:})`
|
100
|
+
- Note: if you use `ApiResponse::Presenter.call` with monad: true, you should use `#success?` and `#failure?` methods to check result
|
101
|
+
- Options only for `ApiResponse.config.monad = true`:
|
102
|
+
- `ApiResponse.config.default_status` default status for `ApiResponse::Presenter.call` if response is not success. You can provide symbol or integer.
|
103
|
+
- Default: `:conflict`
|
104
|
+
- `ApiResponse.config.symbol_status` option for symbolize status from response (or default status if it an Integer).
|
105
|
+
- Default: `true`
|
106
|
+
- Example: `ApiResponse::Presenter.call(response, monad: true, default_status: 500, symbol_status: false) # => Failure({error:, status: 500, error_key:})`
|
107
|
+
- `ApiResponse.config.default_error_key` default error key for `ApiResponse::Presenter.call` if response is not success
|
108
|
+
- Default: `:external_api_error`
|
109
|
+
- `ApiResponse.config.default_error` default error message for `ApiResponse::Presenter.call` if response is not success
|
110
|
+
- Default: `'External Api error'`
|
111
|
+
- `ApiResponse.config.extract_from_body` procedure that is applied to the `response.body` after it has been parsed from JSON string to Ruby hash with symbolize keys.
|
112
|
+
- Default: `->(body) { body }`.
|
113
|
+
- Example lambdas: `->(b) { b.first }`, `->(b) { b.slice(:id, :name) }`, `-> (b) { b.deep_stringify_keys )}`
|
114
|
+
- `ApiResponse.config.struct` struct for pack your extracted value from body.
|
115
|
+
- Default: `nil`
|
116
|
+
- Note: packing only into classes with key value constructors (e.g. `MyAwesomeStruct.new(**attrs)`, not `Struct.new(attrs)`)
|
117
|
+
- Recommend to use [dry-struct](https://github.com/dry-rb/dry-struct) or [Ruby#OpenStruct](https://ruby-doc.org/stdlib-3.0.0/libdoc/ostruct/rdoc/OpenStruct.html)
|
118
|
+
- `ApiResponse.config.raw_response` returns raw response, that you passes into class.
|
119
|
+
- Default: `false`
|
120
|
+
- Example: `ApiResponse::Presenter.call(Faraday::Response<...>, raw_response: true) # => Faraday::Response<...>`
|
121
|
+
- `ApiResponse.config.error_json` returns error message from response body if it is JSON (parsed with symbolize keys)
|
122
|
+
- Default: `false`
|
123
|
+
- Example: `ApiResponse::Presenter.call(Response<body: "{\"error\": \"some_error\"}">, error_json: true) # => {error: "some_error"}`
|
124
|
+
- `ApiResponse.config.default_return_value` default value for `ApiResponse::Presenter.call` if response is not success
|
125
|
+
- Default: `nil`
|
126
|
+
- Example: `ApiResponse::Presenter.call(response, default_return_value: []) # => []`
|
127
|
+
|
128
|
+
NOTE: You can override global settings on instance config, provide block (see: BasicUsage). Params options has higher priority than global settings and block settings.
|
129
|
+
|
130
|
+
### Examples:
|
131
|
+
|
132
|
+
#### Interactors:
|
133
|
+
```ruby
|
134
|
+
class ExternalApiCaller < ApplicationInteractor
|
135
|
+
class Response < Dry::Struct
|
136
|
+
attribute :data, Types::Array
|
137
|
+
end
|
138
|
+
|
139
|
+
def call
|
140
|
+
response = RestClient.get('https://api.example.com/data') # => body: "{\"data\": [{\"id\": 1, \"name\": \"John\"}]}"
|
141
|
+
ApiResponse::Presenter.call(response) do |config|
|
142
|
+
config.adapter = :rest_client
|
143
|
+
config.monad = true
|
144
|
+
config.struct = Response
|
145
|
+
config.default_status = 400 # no matter what status came in fact
|
146
|
+
config.symbol_status = true # return :bad_request instead of 400
|
147
|
+
config.default_error = 'ExternalApiCaller api error' # instead of response error field (e.g. body[:error])
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def MyController
|
153
|
+
def index
|
154
|
+
result = ExternalApiCaller.call
|
155
|
+
if result.success?
|
156
|
+
render json: result.success # => ExternalApiCaller::Response<data: [{id: 1, name: "John"}]> => {data: [{id: 1, name: "John"}]}
|
157
|
+
else
|
158
|
+
render json: {error: result.failure[:error]}, status: result.failure[:status] # => {error: "ExternalApiCaller api error"}, status: 400
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
#### ExternalApi services
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
|
168
|
+
class EmployeeApiService
|
169
|
+
class Employee < Dry::Struct
|
170
|
+
attribute :id, Types::Integer
|
171
|
+
attribute :name, Types::String
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.get_employees(monad: false, adapter: :faraday, **options)
|
175
|
+
# or (params, presenter_options = {})
|
176
|
+
response = Faraday.get('https://api.example.com/data', params) # => body: "{\"data\": [{\"id\": 1, \"name\": \"John\"}]}"
|
177
|
+
ApiResponse::Presenter.call(response, monad: monad, adapter: adapter) do |c|
|
178
|
+
c.extract_from_body = ->(body) { Kaminari.paginate_array(body[:data]).page(1).per(5) }
|
179
|
+
c.struct = Employee
|
180
|
+
c.default_return_value = []
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
class MyController
|
186
|
+
def index
|
187
|
+
employees = EmployeeApiService.get_employees(page: 1, per: 5)
|
188
|
+
if employees.any?
|
189
|
+
render json: employees # => [Employee<id: 1, name: "John">] => [{id: 1, name: "John"}]
|
190
|
+
else
|
191
|
+
render json: {error: 'No employees found'}, status: 404
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
```
|
196
|
+
|
197
|
+
|
198
|
+
### Customization
|
199
|
+
|
200
|
+
#### Processors
|
201
|
+
You can customize the response processing by providing a block to `ApiResponse::Presenter.call` or redefine global processors and parser:
|
202
|
+
All of them must implement `.new(response, config: ApiResponse.config).call` method.
|
203
|
+
You can use not default config in your processor, just pass it as a second named argument.
|
204
|
+
|
205
|
+
1. Redefine `ApiResponse::Processor::Success` # contains logic for success response (status/code 100-399)
|
206
|
+
2. Redefine `ApiResponse::Processor::Failure` # contains logic for failure response (status/code 400-599)
|
207
|
+
3. Redefine `ApiResponse::Parser` # contains logic for parsing response body (e.g. `Oj.load(response.body)`)
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
class MyClass
|
211
|
+
def initialize(response, config: ApiResponse.config)
|
212
|
+
@response = response
|
213
|
+
@config = config
|
214
|
+
end
|
215
|
+
|
216
|
+
def call
|
217
|
+
# your custom logic
|
218
|
+
end
|
219
|
+
end
|
220
|
+
```
|
221
|
+
|
222
|
+
or with `Dry::Initializer`
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
require 'dry/initializer'
|
226
|
+
|
227
|
+
class MyClass
|
228
|
+
extend Dry::Initializer
|
229
|
+
option :response
|
230
|
+
option :config, default: -> { ApiResponse.config }
|
231
|
+
|
232
|
+
def call
|
233
|
+
# your custom logic
|
234
|
+
end
|
235
|
+
end
|
236
|
+
```
|
237
|
+
|
238
|
+
You can use your custom processor or parser in `ApiResponse::Presenter.call` or redefine in global settings:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
ApiResponse.config.success_processor = MyClass
|
242
|
+
ApiResponse.config.failure_processor = MyClass
|
243
|
+
ApiResponse.config.parser = MyClass
|
244
|
+
```
|
245
|
+
|
246
|
+
or
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
ApiResponse::Presenter.call(response, success_processor: MyClass, failure_processor: MyClass, parser: MyClass)
|
250
|
+
```
|
251
|
+
|
252
|
+
#### Options
|
253
|
+
|
254
|
+
Also you can add custom options to `ApiResponse.config.options = {}` and use it in your processor or parser:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
ApiResponse.config do |config|
|
258
|
+
config.options[:my_option] = 'my_value'
|
259
|
+
config.options[:my_another_option] = 'my_another_value'
|
260
|
+
end
|
261
|
+
```
|
262
|
+
|
263
|
+
or
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
ApiResponse::Presenter.call(response, success_processor: MyClass, options: {my_option: 'my_value', my_another_option: 'my_another_value'})
|
267
|
+
```
|
268
|
+
|
269
|
+
Example:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
|
273
|
+
class MyCustomParser
|
274
|
+
attr_reader :response, :config
|
275
|
+
|
276
|
+
def initialize(response, config: ApiResponse.config)
|
277
|
+
@response = response
|
278
|
+
@config = config
|
279
|
+
end
|
280
|
+
|
281
|
+
def call
|
282
|
+
JSON.parse(response.body, symbolize_names: true) # or Oj.load(response.body, symbol_keys: true)
|
283
|
+
rescue JSON::ParserError => e
|
284
|
+
raise ::ParseError.new(e) if config.options[:raise_on_failure]
|
285
|
+
response.body
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
class MyCustomFailureProcessor
|
290
|
+
class BadRequestError < StandardError; end
|
291
|
+
|
292
|
+
attr_reader :response, :config
|
293
|
+
|
294
|
+
def initialize(response, config: ApiResponse.config)
|
295
|
+
@response = response
|
296
|
+
@config = config
|
297
|
+
end
|
298
|
+
|
299
|
+
def call
|
300
|
+
parsed_body = config.parser.new(response).call
|
301
|
+
raise BadRequestError.new(parsed_body) if config.options[:raise_on_failure]
|
302
|
+
|
303
|
+
{error: parsed_body, status: response.status || config.default_status, error_key: :external_api_error}
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
ApiResponse.config do |config|
|
308
|
+
config.failure_processor = MyCustomFailureProcessor
|
309
|
+
config.parser = MyCustomParser
|
310
|
+
config.options[:raise_on_failure] = true
|
311
|
+
end
|
312
|
+
|
313
|
+
response = Faraday.get('https://api.example.com/endpoint_that_will_fail')
|
314
|
+
ApiResponse::Presenter.call(response) # => raise BadRequestError
|
315
|
+
```
|
316
|
+
|
317
|
+
## Contributing
|
318
|
+
|
319
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/golifox/api_response.git)
|
320
|
+
|
321
|
+
## License
|
322
|
+
See `LICENSE` file.
|
323
|
+
|
324
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'api_response/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = 'api-response-presenter'
|
9
|
+
s.author = 'David Rybolovlev'
|
10
|
+
s.email = 'i@golifox.ru'
|
11
|
+
s.license = 'MIT'
|
12
|
+
s.version = ApiResponse::VERSION.dup
|
13
|
+
|
14
|
+
s.summary = 'Gem for presenting API responses from Faraday and RestClient.'
|
15
|
+
s.description = s.summary
|
16
|
+
s.files = Dir['README.md', 'LICENSE', 'CHANGELOG.md', 'api_response_presenter.gemspec', 'lib/**/*.rb']
|
17
|
+
s.executables = []
|
18
|
+
s.require_paths = ['lib']
|
19
|
+
|
20
|
+
s.metadata = {'rubygems_mfa_required' => 'true'}
|
21
|
+
|
22
|
+
s.required_ruby_version = '>= 2.7.4'
|
23
|
+
|
24
|
+
s.add_runtime_dependency 'dry-configurable', '~> 1.0'
|
25
|
+
s.add_runtime_dependency 'dry-initializer', '~> 3.0'
|
26
|
+
s.add_runtime_dependency 'dry-monads', '~> 1.6'
|
27
|
+
s.add_runtime_dependency 'dry-types', '~> 1.5'
|
28
|
+
s.add_runtime_dependency 'oj', '~> 3.13'
|
29
|
+
s.add_runtime_dependency 'zeitwerk', '~> 2.4'
|
30
|
+
|
31
|
+
s.add_development_dependency 'rake'
|
32
|
+
s.add_development_dependency 'rspec'
|
33
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'oj'
|
4
|
+
|
5
|
+
module ApiResponse
|
6
|
+
class Parser
|
7
|
+
extend Dry::Initializer
|
8
|
+
|
9
|
+
param :response, type: Types.Interface(:body)
|
10
|
+
option :config, default: -> { ApiResponse.config }
|
11
|
+
|
12
|
+
def call
|
13
|
+
Oj.load(response.body, mode: :compat, symbol_keys: true)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiResponse
|
4
|
+
class Presenter
|
5
|
+
extend Dry::Initializer
|
6
|
+
|
7
|
+
param :response
|
8
|
+
option :config, default: -> { ApiResponse.config.dup }
|
9
|
+
option :options, type: Types::Hash, default: -> { {} }
|
10
|
+
|
11
|
+
def self.call(response, **options, &block)
|
12
|
+
new(response, options: options).call(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(&block)
|
16
|
+
if block
|
17
|
+
Processor.call(response, config: config, options: options, &block)
|
18
|
+
else
|
19
|
+
Processor.call(response, config: config, options: options)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-monads'
|
4
|
+
|
5
|
+
module ApiResponse
|
6
|
+
class Processor
|
7
|
+
class Failure
|
8
|
+
include Dry::Monads[:result]
|
9
|
+
extend Dry::Initializer
|
10
|
+
|
11
|
+
param :response, type: Types.Interface(:body)
|
12
|
+
option :config, default: -> { ApiResponse.config }
|
13
|
+
|
14
|
+
def call
|
15
|
+
return response if config.raw_response
|
16
|
+
return build_error_monad if config.monad
|
17
|
+
|
18
|
+
begin
|
19
|
+
return response_body if config.error_json
|
20
|
+
rescue StandardError
|
21
|
+
return config.default_return_value || response.body
|
22
|
+
end
|
23
|
+
|
24
|
+
config.default_return_value
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def response_body
|
30
|
+
@response_body ||= config.parser.new(response, config: config).call
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_error_monad
|
34
|
+
status = config.default_status || prepare_status(response)
|
35
|
+
error = config.default_error || response_body.fetch(:error, nil) || response_body
|
36
|
+
error_key = config.default_error_key || response_body.fetch(:error_key, nil)
|
37
|
+
|
38
|
+
Failure({error: error, error_key: error_key, status: status})
|
39
|
+
end
|
40
|
+
|
41
|
+
def prepare_status(response)
|
42
|
+
code = case config.adapter
|
43
|
+
when :faraday, :excon then response.status
|
44
|
+
else response&.code
|
45
|
+
end
|
46
|
+
|
47
|
+
prepared_default_status || prepared_response_status(code)
|
48
|
+
end
|
49
|
+
|
50
|
+
def prepared_response_status(code)
|
51
|
+
config.symbol_status ? ApiResponse::Types::STATUS_CODE_TO_SYMBOL[code.to_i] : code.to_i
|
52
|
+
end
|
53
|
+
|
54
|
+
def prepared_default_status
|
55
|
+
if config.symbol_status && config.default_status.is_a?(Integer)
|
56
|
+
ApiResponse::Types::SYMBOL_TO_STATUS_CODE[config.default_status]
|
57
|
+
else
|
58
|
+
config.default_status
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/monads'
|
4
|
+
|
5
|
+
module ApiResponse
|
6
|
+
class Processor
|
7
|
+
class Success
|
8
|
+
class ExtractError < StandardError; end
|
9
|
+
|
10
|
+
class StructError < StandardError; end
|
11
|
+
|
12
|
+
include Dry::Monads[:result]
|
13
|
+
extend Dry::Initializer
|
14
|
+
|
15
|
+
param :response, type: Types.Interface(:body)
|
16
|
+
option :config, default: -> { ApiResponse.config }
|
17
|
+
|
18
|
+
def call
|
19
|
+
return response if config.raw_response
|
20
|
+
|
21
|
+
result = extract_from_body
|
22
|
+
result = build_struct(result) if config.struct
|
23
|
+
|
24
|
+
config.monad ? Success(result) : result
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def response_body
|
30
|
+
@response_body ||= config.parser.new(response, config: config).call
|
31
|
+
end
|
32
|
+
|
33
|
+
def extract_from_body
|
34
|
+
config.extract_from_body.call(response_body) || response_body
|
35
|
+
rescue EncodingError => e
|
36
|
+
raise ExtractError, e.message
|
37
|
+
rescue StandardError
|
38
|
+
response.body
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_struct(extracted)
|
42
|
+
case extracted
|
43
|
+
when Hash then config.struct.new(extracted)
|
44
|
+
when Array then extracted.map { |item| config.struct.new(**item) }
|
45
|
+
end
|
46
|
+
rescue StandardError => e
|
47
|
+
raise StructError, e.message
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiResponse
|
4
|
+
class Processor
|
5
|
+
extend Dry::Initializer
|
6
|
+
|
7
|
+
param :response
|
8
|
+
option :config, default: -> { ApiResponse.config.dup }
|
9
|
+
option :options, default: -> { {} }
|
10
|
+
|
11
|
+
def self.call(response, options:, config: ApiResponse.config.dup, &block)
|
12
|
+
config = config.dup
|
13
|
+
yield(config) if block
|
14
|
+
config.update(options)
|
15
|
+
|
16
|
+
new(response, config: config, options: options).call
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
processor = success? ? config.success_processor : config.failure_processor
|
21
|
+
|
22
|
+
processor.new(response, config: config).call
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def success?
|
28
|
+
case config.adapter
|
29
|
+
when :faraday, :excon
|
30
|
+
response.status.to_i < 400
|
31
|
+
else
|
32
|
+
response.code.to_i < 400
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-types'
|
4
|
+
|
5
|
+
module ApiResponse
|
6
|
+
module Types
|
7
|
+
include Dry.Types
|
8
|
+
|
9
|
+
STATUS_CODE_TO_SYMBOL = {
|
10
|
+
100 => :continue,
|
11
|
+
101 => :switching_protocols,
|
12
|
+
102 => :processing,
|
13
|
+
103 => :early_hints,
|
14
|
+
200 => :ok,
|
15
|
+
201 => :created,
|
16
|
+
202 => :accepted,
|
17
|
+
203 => :non_authoritative_information,
|
18
|
+
204 => :no_content,
|
19
|
+
205 => :reset_content,
|
20
|
+
206 => :partial_content,
|
21
|
+
207 => :multi_status,
|
22
|
+
208 => :already_reported,
|
23
|
+
226 => :im_used,
|
24
|
+
300 => :multiple_choices,
|
25
|
+
301 => :moved_permanently,
|
26
|
+
302 => :found,
|
27
|
+
303 => :see_other,
|
28
|
+
304 => :not_modified,
|
29
|
+
305 => :use_proxy,
|
30
|
+
306 => :'(unused)',
|
31
|
+
307 => :temporary_redirect,
|
32
|
+
308 => :permanent_redirect,
|
33
|
+
400 => :bad_request,
|
34
|
+
401 => :unauthorized,
|
35
|
+
402 => :payment_required,
|
36
|
+
403 => :forbidden,
|
37
|
+
404 => :not_found,
|
38
|
+
405 => :method_not_allowed,
|
39
|
+
406 => :not_acceptable,
|
40
|
+
407 => :proxy_authentication_required,
|
41
|
+
408 => :request_timeout,
|
42
|
+
409 => :conflict,
|
43
|
+
410 => :gone,
|
44
|
+
411 => :length_required,
|
45
|
+
412 => :precondition_failed,
|
46
|
+
413 => :payload_too_large,
|
47
|
+
414 => :uri_too_long,
|
48
|
+
415 => :unsupported_media_type,
|
49
|
+
416 => :range_not_satisfiable,
|
50
|
+
417 => :expectation_failed,
|
51
|
+
421 => :misdirected_request,
|
52
|
+
422 => :unprocessable_entity,
|
53
|
+
423 => :locked,
|
54
|
+
424 => :failed_dependency,
|
55
|
+
425 => :too_early,
|
56
|
+
426 => :upgrade_required,
|
57
|
+
428 => :precondition_required,
|
58
|
+
429 => :too_many_requests,
|
59
|
+
431 => :request_header_fields_too_large,
|
60
|
+
451 => :unavailable_for_legal_reasons,
|
61
|
+
500 => :internal_server_error,
|
62
|
+
501 => :not_implemented,
|
63
|
+
502 => :bad_gateway,
|
64
|
+
503 => :service_unavailable,
|
65
|
+
504 => :gateway_timeout,
|
66
|
+
505 => :http_version_not_supported,
|
67
|
+
506 => :variant_also_negotiates,
|
68
|
+
507 => :insufficient_storage,
|
69
|
+
508 => :loop_detected,
|
70
|
+
509 => :bandwidth_limit_exceeded,
|
71
|
+
510 => :not_extended,
|
72
|
+
511 => :network_authentication_required
|
73
|
+
}.freeze
|
74
|
+
end
|
75
|
+
end
|
data/lib/api_response.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zeitwerk'
|
4
|
+
loader = Zeitwerk::Loader.for_gem
|
5
|
+
loader.setup
|
6
|
+
|
7
|
+
require 'dry-configurable'
|
8
|
+
require 'dry-initializer'
|
9
|
+
|
10
|
+
module ApiResponse
|
11
|
+
extend Dry::Configurable
|
12
|
+
|
13
|
+
setting :adapter, default: :faraday
|
14
|
+
setting :monad, default: false
|
15
|
+
setting :extract_from_body, default: ->(b) { b }
|
16
|
+
setting :struct, default: nil
|
17
|
+
setting :raw_response, default: false
|
18
|
+
setting :error_json, default: false
|
19
|
+
setting :default_return_value, default: nil
|
20
|
+
setting :default_status, default: :conflict
|
21
|
+
setting :symbol_status, default: true
|
22
|
+
setting :default_error_key, default: :external_api_error
|
23
|
+
setting :default_error, default: 'External Api error'
|
24
|
+
|
25
|
+
setting :success_processor, default: Processor::Success
|
26
|
+
setting :failure_processor, default: Processor::Failure
|
27
|
+
setting :parser, default: Parser
|
28
|
+
setting :options, default: {}
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: api-response-presenter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Rybolovlev
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-03-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-configurable
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dry-initializer
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: dry-monads
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.6'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.6'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: dry-types
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.5'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.5'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: oj
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.13'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.13'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: zeitwerk
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.4'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.4'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: Gem for presenting API responses from Faraday and RestClient.
|
126
|
+
email: i@golifox.ru
|
127
|
+
executables: []
|
128
|
+
extensions: []
|
129
|
+
extra_rdoc_files: []
|
130
|
+
files:
|
131
|
+
- CHANGELOG.md
|
132
|
+
- LICENSE
|
133
|
+
- README.md
|
134
|
+
- api_response_presenter.gemspec
|
135
|
+
- lib/api_response.rb
|
136
|
+
- lib/api_response/parser.rb
|
137
|
+
- lib/api_response/presenter.rb
|
138
|
+
- lib/api_response/processor.rb
|
139
|
+
- lib/api_response/processor/failure.rb
|
140
|
+
- lib/api_response/processor/success.rb
|
141
|
+
- lib/api_response/types.rb
|
142
|
+
- lib/api_response/version.rb
|
143
|
+
homepage:
|
144
|
+
licenses:
|
145
|
+
- MIT
|
146
|
+
metadata:
|
147
|
+
rubygems_mfa_required: 'true'
|
148
|
+
post_install_message:
|
149
|
+
rdoc_options: []
|
150
|
+
require_paths:
|
151
|
+
- lib
|
152
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
153
|
+
requirements:
|
154
|
+
- - ">="
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: 2.7.4
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
requirements:
|
159
|
+
- - ">="
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '0'
|
162
|
+
requirements: []
|
163
|
+
rubygems_version: 3.1.6
|
164
|
+
signing_key:
|
165
|
+
specification_version: 4
|
166
|
+
summary: Gem for presenting API responses from Faraday and RestClient.
|
167
|
+
test_files: []
|