http_api_client 0.1.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 +15 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +143 -0
- data/Rakefile +1 -0
- data/http_api_client.gemspec +33 -0
- data/lib/http_api_client/client.rb +159 -0
- data/lib/http_api_client/config.rb +58 -0
- data/lib/http_api_client/connection_factory.rb +58 -0
- data/lib/http_api_client/errors.rb +90 -0
- data/lib/http_api_client/rails_params_encoder.rb +9 -0
- data/lib/http_api_client/timed_result.rb +24 -0
- data/lib/http_api_client/version.rb +3 -0
- data/lib/http_api_client.rb +67 -0
- data/spec/config/http_api_clients.yml +6 -0
- data/spec/config/http_api_clients_with_basic_auth.yml +8 -0
- data/spec/config/http_api_clients_with_request_id.yml +7 -0
- data/spec/config/http_api_clients_with_self_signed_cert.yml +7 -0
- data/spec/config/http_api_clients_with_ssl.yml +6 -0
- data/spec/http_api_client_spec.rb +58 -0
- data/spec/http_client/client_spec.rb +157 -0
- data/spec/http_client/config_spec.rb +32 -0
- data/spec/http_client/connection_factory_spec.rb +99 -0
- data/spec/http_client/errors_spec.rb +35 -0
- data/spec/http_client/timed_result_spec.rb +31 -0
- metadata +196 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ODliNzQyOGIyOGNmOGEyYjFlZGRjNTg2MGU1MjExODdjMDIzYzg3OQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NjgxOGE3OWRjYzQ0ZmYxMGM3MjA0MWE5ZDE1YTI0ZTU5MDEwMGM0OA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YzZiZWE4ZWFjMTllNjZjYTg4MDVkYWRmNDUwZmQzY2ViNDE0ZDRmOTkyZDQw
|
10
|
+
MTM1NGI2MDY1NzU5NDY5MzM3MjVmNjc5MjdjYjdhZjEyNGMyN2JlYTU1YzBl
|
11
|
+
Nzg1ZjAwMjY1NTYxYzQzN2ZhNzc5OGQxYzBiN2Y4ZTc3YjE4NmQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NzNiMzk1MjE2MjIwMTkxNzBmOGNmNjFlYzMxNTVhNjIwY2QxZDg1OTc2MzIw
|
14
|
+
NzAwMDQyOGFhZmJlZjc2YWEwZTk0OGQxMGZmYWRjMzE4MTYwZGViZWM0ZjIy
|
15
|
+
ZDE3NmVhY2UxYjBkOTc4ZWZhNGZhYWY5ZGFlZTVjM2IyMzZlYmE=
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Rob Monie
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
# HttpApiClient
|
2
|
+
Basic shared http related utils and error translation for places applications.
|
3
|
+
|
4
|
+
Currently:
|
5
|
+
- Http client - faraday based, configurable, threadsafe, ssl capable.
|
6
|
+
- Error translation. Translates http status codes to named errors for more precise error handling in application code.
|
7
|
+
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
Create a http client by extending `HttpApiClient::Client` and providing a configuration key for the config relating to that client:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
module ApiClients
|
15
|
+
class Foursquare < HttpApiClient::Client
|
16
|
+
|
17
|
+
include Singleton
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
super(:foursquare)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
This will construct a http client configured as 'places_api' in the `config/http_api_clients.yml` configuration file.
|
27
|
+
|
28
|
+
Eg:
|
29
|
+
```
|
30
|
+
production:
|
31
|
+
foursquare:
|
32
|
+
protocol: https
|
33
|
+
server: api.foursquare.com
|
34
|
+
#port: 443 (not required)
|
35
|
+
#base_uri: '' (not required)
|
36
|
+
|
37
|
+
development:
|
38
|
+
foursquare:
|
39
|
+
protocol: https
|
40
|
+
server: api.foursquare.com
|
41
|
+
#port: 443 (not required)
|
42
|
+
#base_uri: '' (not required)
|
43
|
+
|
44
|
+
|
45
|
+
#etc. Regular yaml defaults / overrides etc can be used to keep DRY
|
46
|
+
|
47
|
+
```
|
48
|
+
|
49
|
+
### Possible keys
|
50
|
+
|
51
|
+
* ```protocol``` - protocol, e.g. **https** or **http**
|
52
|
+
* ```server``` - the host name, e.g. **api.foursquare.com**
|
53
|
+
* ```port``` - self-explanatory (not required)
|
54
|
+
* ```base_uri``` - base path/uri e.g. **/api** (not required)
|
55
|
+
* ```http_basic_username``` - username for HTTP Basic Auth (not required)
|
56
|
+
* ```http_basic_password``` - password for HTTP Basic Auth (not required)
|
57
|
+
* ```ca_file``` - the path to a self-signed cert authority file, e.g. **/usr/local/etc/nginx/my-server.crt** (not required)
|
58
|
+
|
59
|
+
By implementing your http clients as singletons, you can make the most of faraday's persistent http connections via net_http_persistent. This can have a significant impact on performance for chatty apps assuming that the target server implements keep-alive.
|
60
|
+
|
61
|
+
### Specifiying Token Authentication Params
|
62
|
+
|
63
|
+
Some http apis such as foursquare or instagram require auth params to be passed. These can be defined at the client level by implementing the `auth_params` method.
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
module ApiClients
|
67
|
+
class Foursquare < HttpApiClient::Client
|
68
|
+
|
69
|
+
include Singleton
|
70
|
+
|
71
|
+
def initialize
|
72
|
+
super(:foursquare)
|
73
|
+
end
|
74
|
+
|
75
|
+
def auth_params
|
76
|
+
{
|
77
|
+
client_id: Application.config.foursquare_client_id,
|
78
|
+
client_secret: Application.config.foursquare_secret,
|
79
|
+
v: Date.today.strftime('%Y%m%d')
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
### Current API
|
88
|
+
|
89
|
+
All api calls will return ruby hashed version of json responsea and translate error codes to appropriate Errors (Eg. 404 -> HttpApiClient::NotFound)
|
90
|
+
|
91
|
+
|
92
|
+
#### Raw Http Api
|
93
|
+
```ruby
|
94
|
+
# GET
|
95
|
+
client.get(base_path, params, headers)
|
96
|
+
|
97
|
+
# POST
|
98
|
+
client.create(base_path, payload)
|
99
|
+
|
100
|
+
# DELETE
|
101
|
+
client.destroy(base_path, id)
|
102
|
+
|
103
|
+
# UPDATE - Not yet implemented (if you need it, add it)
|
104
|
+
|
105
|
+
```
|
106
|
+
|
107
|
+
#### Higher level Api
|
108
|
+
|
109
|
+
Not sure if this belongs here yet. It may be removed. These all just pass through to `client.get`
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
# GET
|
113
|
+
client.find(base_path, id, query = {})
|
114
|
+
|
115
|
+
client.find_nested(base_path, id, nested_path)
|
116
|
+
|
117
|
+
client.find_all(base_path, query = {})
|
118
|
+
|
119
|
+
```
|
120
|
+
|
121
|
+
#### Request Id Tracking
|
122
|
+
In order to provide a common request id from api call to the service provider for the purposes of monitoring, a Request-Id header can be
|
123
|
+
added to all requests. In order to do this, the following config options is required:
|
124
|
+
|
125
|
+
`include_request_id_header: true`
|
126
|
+
|
127
|
+
In addition to this, your client code should have set a thread local variable keyed under `request_id`.
|
128
|
+
|
129
|
+
Eg: `Thread.current[:request_id] = request_id`
|
130
|
+
|
131
|
+
With these in place, a request header will be added to the http request which can then be picked up and logged throughout the service provider application code.
|
132
|
+
|
133
|
+
## SSL Support
|
134
|
+
|
135
|
+
SSL is supported but requires certificates for the major certificate authorities to be installed when used on OSX. Linux should have these already.
|
136
|
+
|
137
|
+
`brew install curl-ca-bundle`
|
138
|
+
|
139
|
+
This will install `/usr/local/opt/curl-ca-bundle/share/ca-bundle.crt`
|
140
|
+
|
141
|
+
## TODO:
|
142
|
+
|
143
|
+
* Consider enforcing an SSL connection when using HTTP Basic Auth
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'http_api_client/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'http_api_client'
|
8
|
+
spec.version = HttpApiClient::VERSION
|
9
|
+
spec.authors = ['Rob Monie', 'Andrei Miulescu', 'Stuart Liston', 'Chris Rhode']
|
10
|
+
spec.email = ['robmonie@gmail.com']
|
11
|
+
spec.description = %q{Http client wrapper for simplified api access}
|
12
|
+
spec.summary = %q{}
|
13
|
+
spec.homepage = ''
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
22
|
+
spec.add_development_dependency 'rspec', '~> 2.14'
|
23
|
+
spec.add_development_dependency 'pry', '~> 0.9'
|
24
|
+
spec.add_development_dependency 'pry-debugger', '~> 0.2'
|
25
|
+
|
26
|
+
spec.add_dependency 'activesupport', '>= 3.1'
|
27
|
+
spec.add_dependency 'faraday', '~> 0.9'
|
28
|
+
spec.add_dependency 'net-http-persistent', '~> 2.9'
|
29
|
+
spec.add_dependency 'oj', '~> 2.7'
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'active_support/core_ext/object/to_query'
|
4
|
+
require "active_support/json"
|
5
|
+
require 'faraday'
|
6
|
+
require 'oj'
|
7
|
+
require 'http_api_client'
|
8
|
+
require 'http_api_client/errors'
|
9
|
+
require 'http_api_client/connection_factory'
|
10
|
+
require 'http_api_client/timed_result'
|
11
|
+
|
12
|
+
module HttpApiClient
|
13
|
+
class Client
|
14
|
+
|
15
|
+
include HttpApiClient::Errors::Factory
|
16
|
+
|
17
|
+
attr_reader :config
|
18
|
+
|
19
|
+
def initialize(client_id, config_file = nil)
|
20
|
+
raise "You must supply a http client config id (as defined in #{config_file || Config::DEFAULT_CONFIG_FILE_LOCATION}" unless client_id
|
21
|
+
|
22
|
+
if config_file
|
23
|
+
@config = Config.new(config_file).send(client_id)
|
24
|
+
else
|
25
|
+
@config = Config.new.send(client_id)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def find(base_path, id, query = {})
|
31
|
+
get("#{base_path}/#{id}", query)
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_nested(base_path, id, nested_path)
|
35
|
+
get("#{base_path}/#{id}/#{nested_path}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def find_all(base_path, query = {})
|
39
|
+
get("#{base_path}", query)
|
40
|
+
end
|
41
|
+
|
42
|
+
def get(path, query = {}, custom_headers = {})
|
43
|
+
|
44
|
+
log_data = { method: 'get', host: config.server, path: path_with_query(path, query) }
|
45
|
+
|
46
|
+
response = TimedResult.time('http_api_client_request', log_data) do
|
47
|
+
connection.get(full_path(path), with_auth(query), request_headers(get_headers, custom_headers))
|
48
|
+
end
|
49
|
+
|
50
|
+
handle_response(response, :get, path)
|
51
|
+
end
|
52
|
+
|
53
|
+
def create(path, payload, custom_headers = {})
|
54
|
+
|
55
|
+
log_data = { method: 'post', host: config.server, path: full_path(path) }
|
56
|
+
|
57
|
+
response = TimedResult.time('http_api_client_request', log_data) do
|
58
|
+
connection.post(full_path(path), JSON.fast_generate(with_auth(payload)), request_headers(update_headers, custom_headers))
|
59
|
+
end
|
60
|
+
|
61
|
+
handle_response(response, :post, path)
|
62
|
+
end
|
63
|
+
|
64
|
+
def destroy(base_path, id, custom_headers = {})
|
65
|
+
|
66
|
+
path = "#{base_path}/#{id}"
|
67
|
+
log_data = { method: 'delete', host: config.server, path: full_path(path) }
|
68
|
+
|
69
|
+
response = TimedResult.time('http_api_client_request', log_data) do
|
70
|
+
connection.delete(full_path(path), request_headers(update_headers, custom_headers))
|
71
|
+
end
|
72
|
+
|
73
|
+
handle_response(response, :delete, path)
|
74
|
+
end
|
75
|
+
|
76
|
+
def connection
|
77
|
+
@connection ||= ConnectionFactory.new(config).create
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def params_encoder
|
83
|
+
@params_encoder ||= HttpApiClient.params_encoder
|
84
|
+
end
|
85
|
+
|
86
|
+
def auth_params
|
87
|
+
{}
|
88
|
+
end
|
89
|
+
|
90
|
+
def handle_response(response, method, path)
|
91
|
+
if ok?(response) || validation_failed?(response)
|
92
|
+
if response.body
|
93
|
+
#Don't use regular load method - any strings starting with ':' ( :-) from example) will be interpreted as a symbol
|
94
|
+
Oj.strict_load(response.body)
|
95
|
+
else
|
96
|
+
true
|
97
|
+
end
|
98
|
+
else
|
99
|
+
error_class = error_for_status(response.status)
|
100
|
+
message = "#{response.status} #{method}: #{path}"
|
101
|
+
HttpApiClient.logger.warn("Http Client #{error_class}: #{message}")
|
102
|
+
raise error_class.new(message, response.body)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def ok?(response)
|
107
|
+
Integer(response.status).between?(200, 299)
|
108
|
+
end
|
109
|
+
|
110
|
+
def validation_failed?(response)
|
111
|
+
Integer(response.status) == 422
|
112
|
+
end
|
113
|
+
|
114
|
+
def full_path(path)
|
115
|
+
path = "/#{config.base_uri}/#{path}".gsub(/\/+/, '/')
|
116
|
+
path
|
117
|
+
end
|
118
|
+
|
119
|
+
def path_with_query(path, query)
|
120
|
+
path = full_path(path)
|
121
|
+
path += "?#{params_encoder.encode(query)}" unless query.keys.empty?
|
122
|
+
path
|
123
|
+
end
|
124
|
+
|
125
|
+
def with_auth(query)
|
126
|
+
query.merge(auth_params)
|
127
|
+
end
|
128
|
+
|
129
|
+
def request_headers(base_headers, custom_headers = {})
|
130
|
+
all_headers = base_headers.merge(custom_headers)
|
131
|
+
all_headers.merge!({'X-Request-Id' => Thread.current[:request_id]}) if config.include_request_id_header
|
132
|
+
all_headers
|
133
|
+
end
|
134
|
+
|
135
|
+
def get_headers
|
136
|
+
{
|
137
|
+
'Accept' => 'application/json',
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
def update_headers
|
142
|
+
{
|
143
|
+
'Accept' => 'application/json',
|
144
|
+
'Content-Type' => 'application/json'
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
|
150
|
+
# def params_encoder
|
151
|
+
# if HttpApiClient::rails_loaded?
|
152
|
+
# RailsParamsEncoder
|
153
|
+
# else
|
154
|
+
|
155
|
+
# end
|
156
|
+
# end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'ostruct'
|
5
|
+
require 'http_api_client'
|
6
|
+
|
7
|
+
module HttpApiClient
|
8
|
+
class Config
|
9
|
+
|
10
|
+
DEFAULT_CONFIG_FILE_LOCATION = 'config/http_api_clients.yml'
|
11
|
+
|
12
|
+
def initialize(config_file = DEFAULT_CONFIG_FILE_LOCATION)
|
13
|
+
if File.exists?(config_file)
|
14
|
+
@config = symbolize_keys(config_for(config_file, HttpApiClient.env))
|
15
|
+
else
|
16
|
+
raise "Could not load config file: #{config_file}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :config
|
23
|
+
|
24
|
+
def method_missing(method, *args, &block)
|
25
|
+
if config[method]
|
26
|
+
OpenStruct.new(config[method])
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def config_for(config_file, environment)
|
33
|
+
all_config = YAML.load_file(config_file)
|
34
|
+
env_config = all_config[environment]
|
35
|
+
if env_config
|
36
|
+
env_config
|
37
|
+
else
|
38
|
+
raise "You must supply a http config for the '#{environment}' environment in '#{config_file}'."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def symbolize_keys(hash)
|
43
|
+
hash.inject({}) do |result, (key, value)|
|
44
|
+
new_key = case key
|
45
|
+
when String then key.to_sym
|
46
|
+
else key
|
47
|
+
end
|
48
|
+
new_value = case value
|
49
|
+
when Hash then symbolize_keys(value)
|
50
|
+
else value
|
51
|
+
end
|
52
|
+
result[new_key] = new_value
|
53
|
+
result
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'http_api_client/rails_params_encoder'
|
2
|
+
|
3
|
+
module HttpApiClient
|
4
|
+
class ConnectionFactory
|
5
|
+
|
6
|
+
OSX_CERT_PATH = '/usr/local/opt/curl-ca-bundle/share/ca-bundle.crt'
|
7
|
+
|
8
|
+
attr_reader :config
|
9
|
+
|
10
|
+
def initialize(config)
|
11
|
+
@config = config
|
12
|
+
end
|
13
|
+
|
14
|
+
def create
|
15
|
+
|
16
|
+
Faraday.new(connection_options) do |connection|
|
17
|
+
connection.port = config.port if config.port
|
18
|
+
connection.request :url_encoded # form-encode POST params
|
19
|
+
connection.adapter :net_http_persistent
|
20
|
+
# connection.use :http_cache
|
21
|
+
# connection.response :logger
|
22
|
+
|
23
|
+
if config.http_basic_username
|
24
|
+
connection.basic_auth(config.http_basic_username, config.http_basic_password)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def connection_options
|
33
|
+
options = { url: "#{config.protocol}://#{config.server}" }
|
34
|
+
options.merge!(ssl_config) if config.protocol == 'https'
|
35
|
+
options.merge!({ request: { params_encoder: HttpApiClient.params_encoder } }) if {}.respond_to?(:to_query)
|
36
|
+
options
|
37
|
+
end
|
38
|
+
|
39
|
+
def ssl_config
|
40
|
+
return { ssl: { ca_file: config.ca_file } } if config.ca_file
|
41
|
+
return { ssl: { ca_file: osx_ssl_ca_file } } if osx?
|
42
|
+
return { ssl: { ca_path: '/etc/ssl/certs' } }
|
43
|
+
end
|
44
|
+
|
45
|
+
def osx?
|
46
|
+
`uname`.chomp == 'Darwin'
|
47
|
+
end
|
48
|
+
|
49
|
+
def osx_ssl_ca_file
|
50
|
+
if File.exists?(OSX_CERT_PATH)
|
51
|
+
OSX_CERT_PATH
|
52
|
+
else
|
53
|
+
raise "Unable to load certificate authority file at #{OSX_CERT_PATH}. Try `brew install curl-ca-bundle`"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'http_api_client'
|
2
|
+
|
3
|
+
module HttpApiClient
|
4
|
+
|
5
|
+
module Errors
|
6
|
+
|
7
|
+
class BaseError < StandardError
|
8
|
+
|
9
|
+
attr_reader :response_body, :nested_error
|
10
|
+
|
11
|
+
def initialize(message, response_body, nested_error = nil)
|
12
|
+
super(message)
|
13
|
+
@message = message
|
14
|
+
@response_body = response_body
|
15
|
+
@nested_error = nested_error
|
16
|
+
end
|
17
|
+
|
18
|
+
def message
|
19
|
+
messages = [@message]
|
20
|
+
messages << nested_error.message if nested_error
|
21
|
+
messages << response_body
|
22
|
+
messages.join("\n\n")
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
#400 Range
|
28
|
+
class BadRequest < BaseError ; end
|
29
|
+
class Unauthorized < BaseError ; end
|
30
|
+
class Forbidden < BaseError ; end
|
31
|
+
class NotFound < BaseError ; end
|
32
|
+
class MethodNotAllowed < BaseError ; end
|
33
|
+
class NotAcceptable < BaseError ; end
|
34
|
+
class RequestTimeout < BaseError ; end
|
35
|
+
class UnknownStatus < BaseError ; end
|
36
|
+
class UnprocessableEntity < BaseError ; end
|
37
|
+
class TooManyRequests < BaseError ; end
|
38
|
+
|
39
|
+
#500 Range
|
40
|
+
class InternalServerError < BaseError ; end
|
41
|
+
class NotImplemented < BaseError ; end
|
42
|
+
class BadGateway < BaseError ; end
|
43
|
+
class ServiceUnavailable < BaseError ; end
|
44
|
+
class GatewayTimeout < BaseError ; end
|
45
|
+
|
46
|
+
module Factory
|
47
|
+
|
48
|
+
def error_for_status(status)
|
49
|
+
case status
|
50
|
+
when 400
|
51
|
+
BadRequest
|
52
|
+
when 401
|
53
|
+
Unauthorized
|
54
|
+
when 403
|
55
|
+
Forbidden
|
56
|
+
when 404
|
57
|
+
NotFound
|
58
|
+
when 405
|
59
|
+
MethodNotAllowed
|
60
|
+
when 406
|
61
|
+
NotAcceptable
|
62
|
+
when 408
|
63
|
+
RequestTimeout
|
64
|
+
|
65
|
+
when 422
|
66
|
+
UnprocessableEntity
|
67
|
+
|
68
|
+
when 429
|
69
|
+
TooManyRequests
|
70
|
+
|
71
|
+
when 500
|
72
|
+
InternalServerError
|
73
|
+
when 501
|
74
|
+
NotImplemented
|
75
|
+
when 502
|
76
|
+
BadGateway
|
77
|
+
when 503
|
78
|
+
ServiceUnavailable
|
79
|
+
when 504
|
80
|
+
GatewayTimeout
|
81
|
+
|
82
|
+
else
|
83
|
+
UnknownStatus
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
class TimedResult
|
3
|
+
|
4
|
+
def self.time(event, log_data = {})
|
5
|
+
start_time = Time.now
|
6
|
+
yield
|
7
|
+
ensure
|
8
|
+
|
9
|
+
time = millis_since(start_time)
|
10
|
+
|
11
|
+
log_entries = ["event=#{event}"]
|
12
|
+
log_entries << "request_id=#{Thread.current[:request_id]}" if Thread.current[:request_id]
|
13
|
+
log_entries << "timing=#{time}"
|
14
|
+
log_entries.concat(log_data.to_param.split('&'))
|
15
|
+
|
16
|
+
HttpApiClient.logger.info(log_entries.join(", "))
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.millis_since(start_time)
|
21
|
+
(Time.now - start_time) * 1000
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|