wrappi 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +24 -3
- data/Gemfile +1 -1
- data/README.md +190 -32
- data/lib/wrappi.rb +6 -1
- data/lib/wrappi/cached_response.rb +52 -0
- data/lib/wrappi/client.rb +1 -0
- data/lib/wrappi/endpoint.rb +37 -19
- data/lib/wrappi/executer.rb +49 -0
- data/lib/wrappi/executer/cacher.rb +40 -0
- data/lib/wrappi/executer/retryer.rb +46 -0
- data/lib/wrappi/request.rb +9 -0
- data/lib/wrappi/response.rb +15 -3
- data/lib/wrappi/uncalled_request.rb +36 -0
- data/lib/wrappi/version.rb +1 -1
- data/wrappi.gemspec +2 -1
- metadata +23 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 244907ad87d770e992be773f840eedaa08e370cc1b08f66981e13f45b12c4e30
|
4
|
+
data.tar.gz: 5024e8339f63640f24008f221288f3ee79805b8e80ac9d54980967390188ad66
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2a655cb94421b1e9901ed275ca23a243ecce04e6afb97f24c34ff726f081bad2e8170657064c8a309f0271da46982a451759cc1be61212a52b62e2d284718651
|
7
|
+
data.tar.gz: 1cc627f259122b2c75ed6f239bb31676e8cb9a4e42c34fd23a5968f6a5373b4128da6cddc3fec52c5cec08c9e9f5e2bb0532aba540010004ee4b95e2265214f6
|
data/.travis.yml
CHANGED
@@ -1,5 +1,26 @@
|
|
1
|
-
sudo: false
|
2
1
|
language: ruby
|
2
|
+
cache: bundler
|
3
|
+
services:
|
4
|
+
- docker
|
3
5
|
rvm:
|
4
|
-
- 2.
|
5
|
-
|
6
|
+
- 2.3.7
|
7
|
+
- 2.4.4
|
8
|
+
- 2.5.1
|
9
|
+
- ruby-head
|
10
|
+
before_install:
|
11
|
+
- docker build -t dummy -f spec/dummy/Dockerfile .
|
12
|
+
- docker run -d -p 127.0.0.1:9873:9873 dummy
|
13
|
+
- docker ps -a
|
14
|
+
- gem install bundler -v 1.17.3
|
15
|
+
script:
|
16
|
+
- bundle exec rspec
|
17
|
+
# Code climate test reporter
|
18
|
+
env:
|
19
|
+
global:
|
20
|
+
- CC_TEST_REPORTER_ID=6cebf6c67b6e2d8a53a608ea98ebd897f4cba357bf1e3431bfdb77ff4207399e
|
21
|
+
before_script:
|
22
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
23
|
+
- chmod +x ./cc-test-reporter
|
24
|
+
- ./cc-test-reporter before-build
|
25
|
+
after_script:
|
26
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/arturictus/wrappi.svg?branch=master)](https://travis-ci.org/arturictus/wrappi)
|
2
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/8751a0b6523a52b5e23e/maintainability)](https://codeclimate.com/github/arturictus/wrappi/maintainability)
|
3
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/8751a0b6523a52b5e23e/test_coverage)](https://codeclimate.com/github/arturictus/wrappi/test_coverage)
|
4
|
+
|
1
5
|
# Wrappi
|
2
6
|
|
3
7
|
Framework to create API clients.
|
@@ -46,43 +50,77 @@ end
|
|
46
50
|
|
47
51
|
```ruby
|
48
52
|
user = Github::User.new(username: 'arturictus')
|
49
|
-
user.success?
|
50
|
-
# =>
|
51
|
-
user.
|
52
|
-
# =>
|
53
|
-
user.body
|
54
|
-
# => {"login"=>"arturictus", "id"=>1930175, ...}
|
53
|
+
user.success? # => true
|
54
|
+
user.error? # => false
|
55
|
+
user.status_code # => 200
|
56
|
+
user.body # => {"login"=>"arturictus", "id"=>1930175, ...}
|
55
57
|
```
|
56
58
|
|
59
|
+
### Configurations
|
60
|
+
|
61
|
+
#### Client
|
62
|
+
|
63
|
+
| Name | Type | Default | Required |
|
64
|
+
|-----------------|--------------------------|--------------------------------------------------------------------------|----------|
|
65
|
+
| domain | String | | * |
|
66
|
+
| params | Hash | | |
|
67
|
+
| logger | Logger | Logger.new(STDOUT) | |
|
68
|
+
| headers | Hash | { 'Content-Type' => 'application/json', 'Accept' => 'application/json' } | |
|
69
|
+
| ssl_context | OpenSSL::SSL::SSLContext | | |
|
70
|
+
| use_ssl_context | Boolean | false | |
|
71
|
+
|
72
|
+
#### Endpoint
|
73
|
+
|
74
|
+
| Name | Type | Default | Required |
|
75
|
+
|------------------|-----------------------------------|-------------------------|----------|
|
76
|
+
| client | Wrappi::Client | | * |
|
77
|
+
| path | String | | * |
|
78
|
+
| verb | Symbol | :get | * |
|
79
|
+
| default_params | Hash | {} | |
|
80
|
+
| headers | block | proc { client.headers } | |
|
81
|
+
| basic_auth | Hash, keys: user, pass | | |
|
82
|
+
| follow_redirects | Boolean | true | |
|
83
|
+
| body_type | Symbol, one of: :json,:form,:body | :json | |
|
84
|
+
| cache | Boolean | false | |
|
85
|
+
| retry_if | block | | |
|
86
|
+
| retry_options | block | | |
|
87
|
+
| around_request | block | | |
|
88
|
+
|
57
89
|
### Client
|
58
90
|
|
59
91
|
Is the main configuration for your service.
|
60
92
|
|
61
93
|
It holds the common configuration for all the endpoints (`Wrappi::Endpoint`).
|
62
94
|
|
63
|
-
Required:
|
95
|
+
#### Required:
|
64
96
|
|
65
|
-
-
|
97
|
+
- __domain:__ Yep, you know.
|
66
98
|
```ruby
|
67
99
|
config.domain = 'https://api.github.com'
|
68
100
|
```
|
69
101
|
|
70
|
-
Optionals:
|
102
|
+
#### Optionals:
|
71
103
|
|
72
|
-
-
|
104
|
+
- __params:__ Set global params for all the `Endpoints`.
|
73
105
|
This is a great place to put the `api_key`.
|
74
106
|
```ruby
|
75
107
|
config.params = { "api_key" => "asdfasdfoerkwlejrwer" }
|
76
108
|
```
|
77
109
|
default: `{}`
|
78
110
|
|
79
|
-
-
|
111
|
+
- __logger:__ Set your logger.
|
112
|
+
|
113
|
+
default: `Logger.new(STDOUT)`
|
80
114
|
```ruby
|
81
115
|
config.logger = Rails.logger
|
82
116
|
```
|
83
|
-
default: `Logger.new(STDOUT)`
|
84
117
|
|
85
|
-
-
|
118
|
+
- __headers:__ Headers for all the endpoints. Format, Authentication.
|
119
|
+
|
120
|
+
default:
|
121
|
+
```ruby
|
122
|
+
{ 'Content-Type' => 'application/json', 'Accept' => 'application/json' }
|
123
|
+
```
|
86
124
|
```ruby
|
87
125
|
config.headers = {
|
88
126
|
"Content-Type" => "application/json",
|
@@ -90,57 +128,167 @@ Optionals:
|
|
90
128
|
"Auth-Token" => "verysecret"
|
91
129
|
}
|
92
130
|
```
|
93
|
-
default:
|
94
|
-
```ruby
|
95
|
-
{ 'Content-Type' => 'application/json', 'Accept' => 'application/json' }
|
96
|
-
```
|
97
131
|
|
98
|
-
-
|
132
|
+
- __ssl_context:__ If you need to set an ssl_context.
|
133
|
+
|
134
|
+
default: `nil`
|
99
135
|
```ruby
|
100
136
|
config.ssl_context = OpenSSL::SSL::SSLContext.new.tap do |ctx|
|
101
137
|
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
102
138
|
end
|
103
139
|
```
|
104
|
-
default: `nil`
|
105
140
|
|
106
|
-
-
|
141
|
+
- __use_ssl_context:__ It has to be set to `true` for using the `ssl_context`
|
107
142
|
|
108
143
|
default: `false`
|
109
144
|
|
110
145
|
### Endpoint
|
111
146
|
|
112
|
-
Required:
|
113
|
-
-
|
114
|
-
|
147
|
+
#### Required:
|
148
|
+
- __client:__ `Wrappi::Client` `class`
|
149
|
+
```ruby
|
150
|
+
client MyClient
|
151
|
+
```
|
152
|
+
|
153
|
+
- __path:__ The path to the resource.
|
154
|
+
You can use doted notation and they will be interpolated with the params
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
class MyEndpoint < Wrappi::Endpoint
|
158
|
+
client MyClient
|
159
|
+
verb :get
|
160
|
+
path "/users/:id"
|
161
|
+
end
|
162
|
+
endpoint = MyEndpoint.new(id: "the_id", other: "foo")
|
163
|
+
endpoint.url_with_params #=> "http://domain.com/users/the_id?other=foo"
|
164
|
+
endpoint.url #=> "http://domain.com/users/the_id"
|
165
|
+
endpoint.consummated_params #=> {"other"=>"foo"}
|
166
|
+
```
|
167
|
+
Notice how interpolated params are removed from the query or the body
|
168
|
+
|
169
|
+
- __verb:__
|
115
170
|
|
116
|
-
|
117
|
-
- `verb`:
|
171
|
+
default: `:get`
|
118
172
|
- `:get`
|
119
173
|
- `:post`
|
120
174
|
- `:delete`
|
121
175
|
- `:put`
|
122
176
|
|
123
|
-
default: `:get`
|
124
177
|
|
125
|
-
Optional:
|
178
|
+
#### Optional:
|
126
179
|
|
127
|
-
-
|
180
|
+
- __default_params:__ Default params for the request. This params will be added
|
181
|
+
to all the instances unless you override them.
|
128
182
|
|
129
183
|
default: `{}`
|
130
|
-
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
class MyEndpoint < Wrappi::Endpoint
|
187
|
+
client MyClient
|
188
|
+
verb :get
|
189
|
+
path "/users/:id"
|
190
|
+
default_params do
|
191
|
+
{ other: "bar", foo: "foo" }
|
192
|
+
end
|
193
|
+
end
|
194
|
+
endpoint = MyEndpoint.new(id: "the_id", other: "foo")
|
195
|
+
endpoint.consummated_params #=> {"other"=>"foo","foo" => "foo" }
|
196
|
+
```
|
197
|
+
|
198
|
+
- __headers:__ You can modify the client headers here. Notice that if you want
|
199
|
+
to use the client headers as well you will have to merge them.
|
131
200
|
|
132
201
|
default: `proc { client.headers }`
|
202
|
+
```ruby
|
203
|
+
class MyEndpoint < Wrappi::Endpoint
|
204
|
+
client MyClient
|
205
|
+
verb :get
|
206
|
+
path "/users"
|
207
|
+
headers do
|
208
|
+
client.headers #=> { 'Content-Type' => 'application/json', 'Accept' => 'application/json' }
|
209
|
+
client.headers.merge('Agent' => 'wrappi')
|
210
|
+
end
|
211
|
+
end
|
212
|
+
endpoint = MyEndpoint.new()
|
213
|
+
endpoint.headers #=> { 'Agent' => 'wrappi', 'Content-Type' => 'application/json', 'Accept' => 'application/json'}
|
214
|
+
```
|
133
215
|
|
134
|
-
-
|
216
|
+
- __basic_auth:__ If your endpoint requires basic_auth here is the place. keys
|
217
|
+
have to be: `user` and `pass`.
|
135
218
|
|
136
219
|
default: `nil`
|
137
|
-
|
220
|
+
```ruby
|
221
|
+
basic_auth do
|
222
|
+
{ user: 'wrappi', pass: 'secret'}
|
223
|
+
end
|
224
|
+
```
|
225
|
+
|
226
|
+
- __follow_redirects:__ If first request responds a redirect it will follow them.
|
138
227
|
|
139
228
|
default: `true`
|
140
|
-
|
229
|
+
|
230
|
+
- __body_type:__ Body type.
|
141
231
|
|
142
232
|
default: `:json`
|
143
233
|
|
234
|
+
- :json
|
235
|
+
- :form
|
236
|
+
- :body (Binary data)
|
237
|
+
|
238
|
+
#### Flow Control:
|
239
|
+
|
240
|
+
This configs allows you fine tune your request adding middleware, retries and cache.
|
241
|
+
The are executed in this nested stack:
|
242
|
+
```
|
243
|
+
cache
|
244
|
+
|- retry
|
245
|
+
|- around_request
|
246
|
+
```
|
247
|
+
Check [specs](/blob/master/spec/wrappi/executer_spec.rb) for more examples.
|
248
|
+
|
249
|
+
- __cache:__ Cache the request if successful.
|
250
|
+
|
251
|
+
default: `false`
|
252
|
+
- __retry_if:__ Block to evaluate if request has to be retried. In the block are
|
253
|
+
yielded `Response` and `Endpoint` instances. If the block returns `true` the request will be retried.
|
254
|
+
```ruby
|
255
|
+
retry_if do |response, endpoint|
|
256
|
+
endpoint.class #=> MyEndpoint
|
257
|
+
response.error? # => true or false
|
258
|
+
end
|
259
|
+
```
|
260
|
+
|
261
|
+
Use case:
|
262
|
+
|
263
|
+
We have a service that returns an aggregation of hotels available to book for a city. The service will start the aggregation in the background and will return `200` if the aggregation is completed if the aggregation is not completed will return `201` making us know that we should call again to retrieve all the data. This behavior only occurs if we pass the param: `onlyIfComplete`.
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
retry_if do |response, endpoint|
|
267
|
+
endpoint.consummated_params["onlyIfComplete"] &&
|
268
|
+
response.status_code == 201
|
269
|
+
end
|
270
|
+
```
|
271
|
+
Notice that this block will never be executed if an error occur (like timeouts). For retrying on errors use the `retry_options`
|
272
|
+
|
273
|
+
- __retry_options:__ We are using the great gem [retryable](https://github.com/nfedyashev/retryable) to accomplish this behavior.
|
274
|
+
Check the documentation for fine tuning. I just paste some examples for convenience.
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
retry_options do
|
278
|
+
{ tries: 5, on: [ArgumentError, Wrappi::TimeoutError] } # or
|
279
|
+
{ tries: :infinite, sleep: 0 }
|
280
|
+
end
|
281
|
+
```
|
282
|
+
- __around_request:__ This block is executed surrounding the request. The request
|
283
|
+
will only get executed if you call `request.call`.
|
284
|
+
```ruby
|
285
|
+
around_request do |request, endpoint|
|
286
|
+
endpoint.logger.info("making a request to #{endpoint.url} with params: #{endpoint.consummated_params}")
|
287
|
+
request.call # IMPORTANT
|
288
|
+
endpoint.logger.info("response status is: #{request.status_code}")
|
289
|
+
end
|
290
|
+
```
|
291
|
+
|
144
292
|
## Development
|
145
293
|
|
146
294
|
After checking out the repo, run `bin/setup` to install dependencies.
|
@@ -157,7 +305,17 @@ bundle exec rspec
|
|
157
305
|
|
158
306
|
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
159
307
|
|
308
|
+
#### Docker
|
160
309
|
|
310
|
+
Run dummy server with docker:
|
311
|
+
```
|
312
|
+
docker build -t wrappi/dummy -f spec/dummy/Dockerfile .
|
313
|
+
docker run -d -p 127.0.0.1:9873:9873 wrappy/dummy /bin/sh -c "bin/rails server -b 0.0.0.0 -p 9873"
|
314
|
+
```
|
315
|
+
Try:
|
316
|
+
```
|
317
|
+
curl 127.0.0.1:9873 #=> {"controller":"pages","action":"show_body"}
|
318
|
+
```
|
161
319
|
|
162
320
|
## Contributing
|
163
321
|
|
data/lib/wrappi.rb
CHANGED
@@ -2,12 +2,17 @@ require "wrappi/version"
|
|
2
2
|
require 'http'
|
3
3
|
require 'fusu'
|
4
4
|
require 'miller'
|
5
|
+
require 'retryable'
|
5
6
|
|
6
7
|
module Wrappi
|
7
|
-
|
8
|
+
class TimeoutError < StandardError; end
|
9
|
+
class NotAuthorizedAccessError < StandardError; end
|
8
10
|
end
|
9
11
|
|
10
12
|
require 'wrappi/client'
|
13
|
+
require 'wrappi/executer'
|
11
14
|
require 'wrappi/endpoint'
|
12
15
|
require 'wrappi/request'
|
13
16
|
require 'wrappi/path_gen'
|
17
|
+
require 'wrappi/uncalled_request'
|
18
|
+
require 'wrappi/cached_response'
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Wrappi
|
2
|
+
class CachedResponse
|
3
|
+
# input is a <Response>.to_h
|
4
|
+
# Example input
|
5
|
+
# {
|
6
|
+
# raw_body: '{"foo": "bar"}',
|
7
|
+
# code: 200,
|
8
|
+
# uri: "http://hello.com",
|
9
|
+
# success: true
|
10
|
+
# }
|
11
|
+
def initialize(cached_data)
|
12
|
+
@cached_data = Fusu::HashWithIndifferentAccess.new(cached_data)
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def called?
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
def body
|
24
|
+
@body ||= JSON.parse(cached_data[:raw_body])
|
25
|
+
end
|
26
|
+
|
27
|
+
def success?
|
28
|
+
cached_data[:success]
|
29
|
+
end
|
30
|
+
|
31
|
+
def error?
|
32
|
+
!success?
|
33
|
+
end
|
34
|
+
|
35
|
+
def raw_body
|
36
|
+
cached_data[:raw_body]
|
37
|
+
end
|
38
|
+
|
39
|
+
def uri
|
40
|
+
cached_data[:uri]
|
41
|
+
end
|
42
|
+
|
43
|
+
def status
|
44
|
+
cached_data[:code]
|
45
|
+
end
|
46
|
+
alias_method :status_code, :status
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def cached_data; @cached_data end
|
51
|
+
end
|
52
|
+
end
|
data/lib/wrappi/client.rb
CHANGED
data/lib/wrappi/endpoint.rb
CHANGED
@@ -3,7 +3,7 @@ module Wrappi
|
|
3
3
|
class Endpoint < Miller.base(
|
4
4
|
:verb, :client, :path, :default_params,
|
5
5
|
:headers, :follow_redirects, :basic_auth,
|
6
|
-
:body_type,
|
6
|
+
:body_type, :retry_options, :cache,
|
7
7
|
default_config: {
|
8
8
|
verb: :get,
|
9
9
|
client: proc { raise 'client not set' }, # TODO: add proper error
|
@@ -11,7 +11,8 @@ module Wrappi
|
|
11
11
|
default_params: {},
|
12
12
|
headers: proc { client.headers },
|
13
13
|
follow_redirects: true,
|
14
|
-
body_type: :json
|
14
|
+
body_type: :json,
|
15
|
+
cache: false
|
15
16
|
}
|
16
17
|
)
|
17
18
|
attr_reader :input_params, :options
|
@@ -43,23 +44,8 @@ module Wrappi
|
|
43
44
|
params
|
44
45
|
end
|
45
46
|
|
46
|
-
# overridable
|
47
|
-
def before_request
|
48
|
-
true
|
49
|
-
end
|
50
|
-
|
51
|
-
# overridable
|
52
|
-
def after_request(response)
|
53
|
-
true
|
54
|
-
end
|
55
|
-
|
56
47
|
def response
|
57
|
-
|
58
|
-
@response ||= Response.new do
|
59
|
-
Request.new(self).call
|
60
|
-
end.tap(&:request)
|
61
|
-
after_request(@response)
|
62
|
-
@response
|
48
|
+
@response ||= Executer.call(self)
|
63
49
|
end
|
64
50
|
alias_method :call, :response
|
65
51
|
|
@@ -67,10 +53,42 @@ module Wrappi
|
|
67
53
|
def success?; response.success? end
|
68
54
|
def status; response.status end
|
69
55
|
|
56
|
+
# AROUND REQUEST
|
57
|
+
def self.around_request(&block)
|
58
|
+
@around_request = block
|
59
|
+
end
|
60
|
+
def around_request
|
61
|
+
self.class.instance_variable_get(:@around_request)
|
62
|
+
end
|
63
|
+
|
64
|
+
# RETRY
|
65
|
+
def self.retry_if(&block)
|
66
|
+
@retry_if = block
|
67
|
+
end
|
68
|
+
def retry_if
|
69
|
+
self.class.instance_variable_get(:@retry_if)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Cache
|
73
|
+
def cache_key
|
74
|
+
# TODO: think headers have to be in the key as well
|
75
|
+
@cache_key ||= "[#{verb.to_s.upcase}]##{url}#{params_cache_key}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def logger
|
79
|
+
client.logger
|
80
|
+
end
|
81
|
+
|
70
82
|
private
|
71
83
|
|
84
|
+
def params_cache_key
|
85
|
+
return if params.empty?
|
86
|
+
d = Digest::MD5.hexdigest params.to_json
|
87
|
+
"?#{d}"
|
88
|
+
end
|
89
|
+
|
72
90
|
def _url
|
73
|
-
URI.join("#{client.domain}/", path_gen.path)
|
91
|
+
URI.join("#{client.domain}/", path_gen.path) # TODO: remove heading "/" of path
|
74
92
|
end
|
75
93
|
|
76
94
|
def params
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Wrappi
|
2
|
+
class Executer
|
3
|
+
|
4
|
+
def self.call(*args)
|
5
|
+
new(*args).call
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :endpoint, :retryer, :cacher
|
9
|
+
def initialize(endpoint)
|
10
|
+
@endpoint = endpoint
|
11
|
+
@retryer = Retryer.new(endpoint)
|
12
|
+
@cacher = Cacher.new(endpoint)
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
if cache?
|
17
|
+
cacher.call do
|
18
|
+
request_with_retry
|
19
|
+
end
|
20
|
+
else
|
21
|
+
request_with_retry
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def cache?
|
28
|
+
cacher.cache?
|
29
|
+
end
|
30
|
+
|
31
|
+
def request_with_retry
|
32
|
+
retryer.call do
|
33
|
+
make_request
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def around_request
|
38
|
+
endpoint.around_request || proc { |res, endpoint| res.call }
|
39
|
+
end
|
40
|
+
|
41
|
+
def make_request
|
42
|
+
res = Response.new { Request.new(endpoint).call }
|
43
|
+
around_request.call(res, endpoint)
|
44
|
+
res.called? ? res : UncalledRequest.new
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
require 'wrappi/executer/retryer'
|
49
|
+
require 'wrappi/executer/cacher'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Wrappi
|
2
|
+
class Executer
|
3
|
+
class Cacher
|
4
|
+
attr_reader :endpoint
|
5
|
+
def initialize(endpoint)
|
6
|
+
@endpoint = endpoint
|
7
|
+
end
|
8
|
+
|
9
|
+
def call
|
10
|
+
cached = cache.read(cache_key)
|
11
|
+
return CachedResponse.new(cached) if cached
|
12
|
+
response = yield
|
13
|
+
cache.write(cache_key, response.to_h) if response.success?
|
14
|
+
response
|
15
|
+
end
|
16
|
+
|
17
|
+
def cache?
|
18
|
+
endpoint.client.cache && endpoint.cache && cache_allowed_verb?
|
19
|
+
end
|
20
|
+
|
21
|
+
def cache_allowed_verb?
|
22
|
+
if [:get, :post].include?(endpoint.verb)
|
23
|
+
true
|
24
|
+
else
|
25
|
+
puts "Cache is only available to no side effect requests: :get and :post" # TODO: make a warning
|
26
|
+
false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def cache
|
31
|
+
endpoint.client.cache
|
32
|
+
end
|
33
|
+
|
34
|
+
def cache_key
|
35
|
+
endpoint.cache_key
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Wrappi
|
2
|
+
class Executer
|
3
|
+
class Retryer
|
4
|
+
class RetryError < StandardError; end
|
5
|
+
attr_reader :endpoint
|
6
|
+
def initialize(endpoint)
|
7
|
+
@endpoint = endpoint
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
if retry?
|
12
|
+
Retryable.retryable(retry_options) do
|
13
|
+
res = yield
|
14
|
+
raise RetryError if retry_if.call(res, endpoint)
|
15
|
+
res
|
16
|
+
end
|
17
|
+
else
|
18
|
+
yield
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def retry_options
|
23
|
+
default = { tries: 3, on: [RetryError] }
|
24
|
+
if endpoint.retry_options
|
25
|
+
end_opts = endpoint.retry_options.dup
|
26
|
+
{}.tap do |h|
|
27
|
+
h[:tries] = end_opts[:tries] || default[:tries]
|
28
|
+
if on = end_opts.delete(:on)
|
29
|
+
h[:on] = default[:on] + Fusu::Array.wrap(on)
|
30
|
+
end
|
31
|
+
end.merge(end_opts)
|
32
|
+
else
|
33
|
+
default
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def retry?
|
38
|
+
!!endpoint.retry_if
|
39
|
+
end
|
40
|
+
|
41
|
+
def retry_if
|
42
|
+
endpoint.retry_if
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/wrappi/request.rb
CHANGED
@@ -23,6 +23,15 @@ module Wrappi
|
|
23
23
|
def call
|
24
24
|
@call ||= strategy.call
|
25
25
|
end
|
26
|
+
alias_method :http, :call
|
27
|
+
|
28
|
+
def to_h
|
29
|
+
@to_h ||= {
|
30
|
+
raw_body: http.raw_body,
|
31
|
+
code: http.code,
|
32
|
+
uri: http.uri
|
33
|
+
}
|
34
|
+
end
|
26
35
|
end
|
27
36
|
end
|
28
37
|
require 'wrappi/request/template'
|
data/lib/wrappi/response.rb
CHANGED
@@ -3,10 +3,8 @@ module Wrappi
|
|
3
3
|
# check documentation at:
|
4
4
|
# https://github.com/httprb/http/wiki/Response-Handling
|
5
5
|
class Response
|
6
|
-
class TimeoutError < StandardError; end
|
7
|
-
class NotAuthorizedAccessError < StandardError; end
|
8
|
-
attr_reader :block
|
9
6
|
|
7
|
+
attr_reader :block
|
10
8
|
def initialize(&block)
|
11
9
|
@block = block
|
12
10
|
end
|
@@ -17,6 +15,10 @@ module Wrappi
|
|
17
15
|
end
|
18
16
|
alias_method :call, :request
|
19
17
|
|
18
|
+
def called?
|
19
|
+
!!@request
|
20
|
+
end
|
21
|
+
|
20
22
|
def body
|
21
23
|
@body ||= request.parse
|
22
24
|
end
|
@@ -40,6 +42,16 @@ module Wrappi
|
|
40
42
|
def status
|
41
43
|
request.code
|
42
44
|
end
|
45
|
+
alias_method :status_code, :status
|
46
|
+
|
47
|
+
def to_h
|
48
|
+
@to_h ||= {
|
49
|
+
raw_body: raw_body,
|
50
|
+
code: code,
|
51
|
+
uri: uri,
|
52
|
+
success: success?
|
53
|
+
}
|
54
|
+
end
|
43
55
|
|
44
56
|
def method_missing(method_name, *arguments, &block)
|
45
57
|
if request.respond_to?(method_name)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Wrappi
|
2
|
+
class UncalledRequest
|
3
|
+
def request
|
4
|
+
nil
|
5
|
+
end
|
6
|
+
alias_method :call, :request
|
7
|
+
|
8
|
+
def called?
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
def body
|
13
|
+
{ "message" => "uncalled response" }
|
14
|
+
end
|
15
|
+
|
16
|
+
def success?
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def error?
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
def raw_body
|
25
|
+
body.to_json
|
26
|
+
end
|
27
|
+
|
28
|
+
def uri
|
29
|
+
'uncalled_response'
|
30
|
+
end
|
31
|
+
|
32
|
+
def status
|
33
|
+
100
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/wrappi/version.rb
CHANGED
data/wrappi.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ["arturictus@gmail.com"]
|
11
11
|
|
12
12
|
spec.summary = %q{Framework to create HTTP API clients}
|
13
|
-
spec.description = %q{Framework to create HTTP API clients
|
13
|
+
spec.description = %q{Framework to create HTTP API clients. The aim is to abstract the best practices using a declarative interface.}
|
14
14
|
spec.homepage = "https://github.com/arturictus/wrappi"
|
15
15
|
spec.license = "MIT"
|
16
16
|
|
@@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.add_dependency 'http', "~> 2.2"
|
27
27
|
spec.add_dependency 'fusu', "~> 0.2.1"
|
28
28
|
spec.add_dependency 'miller', "~> 0.1.1"
|
29
|
+
spec.add_dependency 'retryable'
|
29
30
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wrappi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Artur Pañach
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-01-
|
11
|
+
date: 2019-01-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -108,8 +108,22 @@ dependencies:
|
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: 0.1.1
|
111
|
-
|
112
|
-
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: retryable
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: Framework to create HTTP API clients. The aim is to abstract the best
|
126
|
+
practices using a declarative interface.
|
113
127
|
email:
|
114
128
|
- arturictus@gmail.com
|
115
129
|
executables: []
|
@@ -128,14 +142,19 @@ files:
|
|
128
142
|
- bin/dev_server
|
129
143
|
- bin/setup
|
130
144
|
- lib/wrappi.rb
|
145
|
+
- lib/wrappi/cached_response.rb
|
131
146
|
- lib/wrappi/client.rb
|
132
147
|
- lib/wrappi/endpoint.rb
|
148
|
+
- lib/wrappi/executer.rb
|
149
|
+
- lib/wrappi/executer/cacher.rb
|
150
|
+
- lib/wrappi/executer/retryer.rb
|
133
151
|
- lib/wrappi/path_gen.rb
|
134
152
|
- lib/wrappi/request.rb
|
135
153
|
- lib/wrappi/request/get.rb
|
136
154
|
- lib/wrappi/request/template.rb
|
137
155
|
- lib/wrappi/request/with_body.rb
|
138
156
|
- lib/wrappi/response.rb
|
157
|
+
- lib/wrappi/uncalled_request.rb
|
139
158
|
- lib/wrappi/version.rb
|
140
159
|
- wrappi.gemspec
|
141
160
|
homepage: https://github.com/arturictus/wrappi
|