wrappi 0.1.2 → 0.2.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 +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
|
+
[](https://travis-ci.org/arturictus/wrappi)
|
2
|
+
[](https://codeclimate.com/github/arturictus/wrappi/maintainability)
|
3
|
+
[](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
|