hasoffersv3 0.5.5 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eefc584486a52827598e4cc0fe17fb050f19b730
4
- data.tar.gz: 34ccbf8f939fd76476be45be3a52da41c3cc6b02
3
+ metadata.gz: 0ae8c8dcc8a60460f3f3a0954be68f9a9a2208b0
4
+ data.tar.gz: e8c79bdcb82d3806749f5cb3b6cc9c7b541e79df
5
5
  SHA512:
6
- metadata.gz: e35ea5362691e3621e510894a5ff6baa87f7f64baf7803fae74b3aad34ddadde92e43a276b9cd50c2d74a87c6c60c8a1405b0a68ea3863f470e74bc8c674ff9f
7
- data.tar.gz: 5a36c482a56acfb7187fb096c2c7a56d54bc561d4b99e1d3bfefabdcf1ea7cb5ef9319d20df8d82a3d3ce089a058a8fab99e042ac6cc53eb927ada6a90a4238f
6
+ metadata.gz: 976cb9593dfbed447919aac92021c5a268a696aed5e2627f586b83bd1c59a20614306947093f788aba639131f7aa390d6c5c269f1631bcac9e48c7bfcc720431
7
+ data.tar.gz: ea19c6886c520a074a274b6710734f17f26b2b97d38d0c12f02aaa40e5010f242910869ef75c070fb911c3c4051792685c18ec7448b079313b92bbe4635996cf
data/.gitignore CHANGED
@@ -18,3 +18,5 @@ tmp
18
18
  .DS_Store
19
19
  *.swp
20
20
  .ruby-version
21
+ .ruby-gemset
22
+ .idea
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ # 0.6.0
2
+ - [#61] Added configurable (via `config.raise_errors = true`) raising of internal exceptions (`HasOffersV3::Error` and its descendants) on various API error conditions. Incompatible changes: JSON parsing error now raises a `HasOffersV3::ParseError` that wraps the original error from a particular JSON driver used. (@vittorius)
3
+
1
4
  # 0.5.5
2
5
  - [#58] Added `AdvertiserBilling::findAllInvoicesByIds`, `AffiliateBilling::findAllInvoicesByIds` methods. (@kamil89)
3
6
  - [#59] Added `Advertiser::getSingupAnswers` and `Affiliate::getSingupAnswers` methods. (@kamil89)
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  ### Overview
2
2
 
3
3
  [![Build status](https://api.travis-ci.org/applift/hasoffersv3.png?branch=master)](http://travis-ci.org/applift/hasoffersv3)
4
+ [![Gem Version](https://badge.fury.io/rb/hasoffersv3.svg)](https://badge.fury.io/rb/hasoffersv3)
4
5
 
5
6
  ### Synopsis
6
7
 
@@ -28,6 +29,7 @@ HasOffersV3.configure do |config|
28
29
  config.api_key = 'Your HasOffers API Key'
29
30
  config.network_id = 'Your HasOffers Network ID'
30
31
  config.read_timeout = 10
32
+ config.raise_errors = true # add this if you want the hasoffersv3 to raise errors upon detected API error messages in responses; defaults to `false`
31
33
 
32
34
  # Optionally configure a proxy:
33
35
  config.proxy_host = 'yourproxy.com'
@@ -72,6 +74,10 @@ HasOffersV3::Advertiser.signup({
72
74
  })
73
75
  ```
74
76
 
77
+ ### Error handling
78
+
79
+ If `config.raise_errors` was set to `true`, the `hasoffersv3` will raise internal exceptions when error occurs at protocol or business logic level. See the [`HasOffersV3::Error`](https://github.com/applift/hasoffersv3/blob/master/lib/hasoffersv3/error.rb) and its descendants' definitions for more comments and details.
80
+
75
81
  ### Logging
76
82
 
77
83
  To enable log you can set a logger in configuration. All HTTP requests and responses will be logged.
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+ require 'hasoffersv3/error'
3
+
4
+ class HasOffersV3
5
+ # Any error emitted at business logic level of the HasOffersV3 API that is not fatal but tells that a particular operation cannot be performed.
6
+ class APIError < ResponseError
7
+ class << self
8
+ def from_response(response)
9
+ error_class_chain.each do |error_class|
10
+ err_msg = error_class.detect(response)
11
+ break error_class.new(err_msg, response) if err_msg
12
+ end
13
+ end
14
+
15
+ protected
16
+
17
+ def error_class_chain
18
+ # the order is important, more "blocker-like" errors go before "lax" ones; don't forget to add any new error classes to this chain
19
+ @error_class_chain ||= [IPNotWhitelistedError, MissingParamError, FieldError, InternalError, UnknownError]
20
+ end
21
+ end
22
+ end
23
+
24
+ class IPNotWhitelistedError < APIError
25
+ def self.detect(response)
26
+ response.error_messages.grep(/IP \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} is not white-listed/).first
27
+ end
28
+ end
29
+
30
+ class MissingParamError < APIError
31
+ def self.detect(response)
32
+ response.error_messages.grep(/Missing required argument/).first
33
+ end
34
+ end
35
+
36
+ class FieldError < APIError
37
+ def self.detect(response)
38
+ response.error_messages.grep(/Field '.*' does not exist or is not allowed to be used./).first
39
+ end
40
+ end
41
+
42
+ class InternalError < APIError
43
+ def self.detect(response)
44
+ # E.g. "There was a database error with the trackable id [SE-5888e90b944af]. Contact support for more assistance."
45
+ response.error_messages.grep(/error with the trackable id/).first
46
+ end
47
+ end
48
+
49
+ class UnknownError < APIError
50
+ def self.detect(response)
51
+ response.error_messages.first
52
+ end
53
+ end
54
+ end
@@ -1,9 +1,10 @@
1
1
  require 'net/http' if RUBY_VERSION < '2'
2
2
  require 'active_support/core_ext/object/to_query'
3
+ require 'hasoffersv3/error'
4
+ require 'hasoffersv3/api_error'
3
5
 
4
6
  class HasOffersV3
5
7
  class Client
6
-
7
8
  attr_accessor :configuration
8
9
 
9
10
  def initialize(configuration)
@@ -32,7 +33,9 @@ class HasOffersV3
32
33
 
33
34
  logger.log_response(http_response)
34
35
 
35
- Response.new(http_response, @configuration.json_driver)
36
+ with_error_detection do
37
+ Response.new(http_response, @configuration.json_driver)
38
+ end
36
39
  end
37
40
 
38
41
  def execute_request(net_http, raw_request)
@@ -73,5 +76,14 @@ class HasOffersV3
73
76
  configuration.http_logger
74
77
  end
75
78
 
79
+ def with_error_detection
80
+ response = yield
81
+ return response unless configuration.raise_errors
82
+
83
+ raise HTTPError.from_response(response) unless response.http_ok?
84
+ raise APIError.from_response(response) unless response.status_ok?
85
+
86
+ response
87
+ end
76
88
  end
77
89
  end
@@ -20,6 +20,7 @@ class HasOffersV3
20
20
  network_id: '',
21
21
  api_key: '',
22
22
  json_driver: self.default_json_driver,
23
+ raise_errors: false,
23
24
  logger: nil,
24
25
  proxy_host: nil,
25
26
  proxy_port: nil,
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/module/delegation'
3
+
4
+ class HasOffersV3
5
+ class Error < StandardError; end
6
+
7
+ class ResponseError < Error
8
+ attr_reader :response
9
+ delegate :http_status_code, :http_message, :http_headers, to: :response
10
+
11
+ def initialize(message, response)
12
+ super(message)
13
+ @response = response
14
+ end
15
+ end
16
+
17
+ # An error caught when parsing the JSON response body got from API endpoint.
18
+ # Wraps the original JSON driver error emitted.
19
+ # Raised always (does not depend on the config settings).
20
+ class ParseError < ResponseError; end
21
+
22
+ # Any HTTP error that has occurred during the call to the API endpoint (gateway timeout, internal server error etc.)
23
+ class HTTPError < ResponseError
24
+ def self.from_response(response)
25
+ new("HTTP error: #{response.http_message}", response)
26
+ end
27
+ end
28
+ end
@@ -3,14 +3,27 @@ class HasOffersV3
3
3
  attr_reader :body, :http_status_code, :http_message, :http_headers
4
4
 
5
5
  def initialize(response, json=default_json_driver)
6
- @body = json.load(response.body.to_s)
6
+ begin
7
+ @body = json.load(response.body.to_s)
8
+ rescue
9
+ raise ParseError.new('Error parsing response body, examine the `cause` property for details', response)
10
+ end
11
+
7
12
  @http_status_code = response.code
8
13
  @http_message = response.message
9
14
  @http_headers = response.to_hash
10
15
  end
11
16
 
12
17
  def success?
13
- @http_status_code.to_s == '200' and status == 1
18
+ http_ok? && status_ok?
19
+ end
20
+
21
+ def http_ok?
22
+ @http_status_code.to_s == '200'
23
+ end
24
+
25
+ def status_ok?
26
+ status == 1
14
27
  end
15
28
 
16
29
  def status
@@ -1,3 +1,3 @@
1
1
  class HasOffersV3
2
- VERSION = '0.5.5'
2
+ VERSION = '0.6.0'
3
3
  end
@@ -1,30 +1,150 @@
1
+ # frozen_string_literal: true
2
+
1
3
  describe HasOffersV3::Client do
2
4
  let(:config) { HasOffersV3::Configuration.new }
3
- subject { HasOffersV3::Client.new(config) }
5
+ let(:client) { HasOffersV3::Client.new(config) }
6
+ subject { client }
4
7
 
5
8
  describe '#new_http' do
6
-
7
9
  context 'when configuration proxy_host is present' do
8
-
9
- let(:config) {
10
+ let(:config) do
10
11
  result = HasOffersV3::Configuration.new
11
12
  result.proxy_host = 'proxy.com'
12
13
  result.proxy_port = '8080'
13
14
  result
14
- }
15
+ end
15
16
 
16
17
  it 'generates a connection with proxy' do
17
18
  http_client = subject.new_http(URI('http://hasoffers.com:9300/'))
18
19
  expect(http_client.proxyaddr).to eq('proxy.com')
19
20
  expect(http_client.proxyport).to eq('8080')
20
21
  end
21
-
22
22
  end
23
+ end
24
+
25
+ describe '#request' do
26
+ context 'raising errors' do
27
+ let(:raise_errors) { true }
28
+ let(:config) { HasOffersV3::Configuration.new(raise_errors: raise_errors) }
29
+ let(:url) { api_url 'Advertiser' }
30
+ let(:response_body) { JSON.load(default_return[:body]) } # it's more convenient for us to have response_body as a Ruby object
31
+ let(:status) { default_return[:status] }
32
+ let(:headers) { {} }
33
+ let(:response) do
34
+ {
35
+ body: JSON.dump(response_body),
36
+ status: status,
37
+ headers: headers
38
+ }
39
+ end
40
+ let(:http_method) { :post }
41
+
42
+ before { stub_call(http_method, response) }
43
+
44
+ subject { client.request(http_method, 'Advertiser', 'findAll', {}) }
45
+
46
+ shared_examples 'does not raise errors when configured to not raise errors' do
47
+ let(:raise_errors) { false }
48
+
49
+ it 'does not raise errors' do
50
+ expect { subject }.not_to raise_error
51
+ end
52
+ end
53
+
54
+ context 'no errors' do
55
+ it 'does not raise error if no error messages were detected' do
56
+ expect { subject }.not_to raise_error
57
+ end
58
+ end
59
+
60
+ context 'HTTP errors' do
61
+ let(:response_body) { nil }
62
+ let(:headers) { { 'Content-Length' => 0, 'Connection' => 'Close' } }
63
+ let(:status) { [504, 'GATEWAY_TIMEOUT'] }
64
+
65
+ it 'raises an appropriate error when HTTP failure detected' do
66
+ expect { subject }.to raise_error do |error|
67
+ expect(error).to be_a HasOffersV3::HTTPError
68
+ expect(error).to have_attributes(
69
+ message: 'HTTP error: GATEWAY_TIMEOUT',
70
+ http_status_code: '504',
71
+ http_message: 'GATEWAY_TIMEOUT',
72
+ http_headers: {
73
+ 'content-length' => ['0'],
74
+ 'connection' => ['Close']
75
+ }
76
+ )
77
+ end
78
+ end
79
+
80
+ it_behaves_like 'does not raise errors when configured to not raise errors'
81
+ end
23
82
 
83
+ context 'API errors' do
84
+ shared_examples 'API error is detected and raised' do
85
+ let(:response_body) do
86
+ {
87
+ 'response' => {
88
+ 'status' => -1,
89
+ 'httpStatus' => 200,
90
+ 'data' => '',
91
+ 'errors' => [{'publicMessage' => error_message }],
92
+ 'errorMessage' => error_message
93
+ }
94
+ }
95
+ end
96
+
97
+ it 'raises an appropriate error when such an error detected' do
98
+ expect { subject }.to raise_error(error_class, error_message)
99
+ end
100
+
101
+ it_behaves_like 'does not raise errors when configured to not raise errors'
102
+ end
103
+
104
+ context 'IP is not whitelisted' do
105
+ let(:error_message) { 'IP 178.161.91.191 is not white-listed. Please enable it in the application, Support => API' }
106
+ let(:error_class) { HasOffersV3::IPNotWhitelistedError }
107
+
108
+ it_behaves_like 'API error is detected and raised'
109
+ end
110
+
111
+ context 'parameter missing' do
112
+ subject { client.request(http_method, 'Advertiser', 'findById', {}) }
113
+
114
+ let(:error_message) { 'Missing required argument: id' }
115
+ let(:error_class) { HasOffersV3::MissingParamError }
116
+
117
+ it_behaves_like 'API error is detected and raised'
118
+ end
119
+
120
+ context 'field error' do
121
+ let(:url) { api_url 'Report' }
122
+ subject { client.request(http_method, 'Report', 'getStats', { 'fields[]' => 'Offer.nam' }) }
123
+
124
+ let(:error_message) { "Field 'Offer.nam' does not exist or is not allowed to be used." }
125
+ let(:error_class) { HasOffersV3::FieldError }
126
+
127
+ it_behaves_like 'API error is detected and raised'
128
+ end
129
+
130
+ context 'internal error' do
131
+ let(:error_message) { 'There was a database error with the trackable id [SE-5888e90b944af]. Contact support for more assistance.' }
132
+ let(:error_class) { HasOffersV3::InternalError }
133
+
134
+ it_behaves_like 'API error is detected and raised'
135
+ end
136
+
137
+ context 'unknown error' do
138
+ let(:error_message) { 'Something went wrong.' }
139
+ let(:error_class) { HasOffersV3::UnknownError }
140
+
141
+ it_behaves_like 'API error is detected and raised'
142
+ end
143
+ end
144
+ end
24
145
  end
25
146
 
26
147
  describe '#base_uri' do
27
-
28
148
  let(:configuration_to_default_host) { HasOffersV3::Configuration.new }
29
149
  let(:config_for_proxy) {
30
150
  result = HasOffersV3::Configuration.new
@@ -39,7 +159,5 @@ describe HasOffersV3::Client do
39
159
  proxy_connection = HasOffersV3::Client.new(config_for_proxy)
40
160
  expect(proxy_connection.base_uri).to eq('https://api.applift.com/v3')
41
161
  end
42
-
43
162
  end
44
-
45
163
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ require 'hasoffersv3/error'
3
+
4
+ describe HasOffersV3::Response do
5
+ describe '#initialize' do
6
+ it 'raises an appropriate error on malformed response body' do
7
+ expect { described_class.new('"') }.to(
8
+ raise_error(HasOffersV3::ParseError, 'Error parsing response body, examine the `cause` property for details')
9
+ )
10
+ end
11
+ end
12
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hasoffersv3
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.5
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maximilian Seifert
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-04-10 00:00:00.000000000 Z
12
+ date: 2017-07-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -105,12 +105,14 @@ files:
105
105
  - lib/hasoffersv3/affiliate.rb
106
106
  - lib/hasoffersv3/affiliate_billing.rb
107
107
  - lib/hasoffersv3/affiliate_offer.rb
108
+ - lib/hasoffersv3/api_error.rb
108
109
  - lib/hasoffersv3/application.rb
109
110
  - lib/hasoffersv3/base.rb
110
111
  - lib/hasoffersv3/client.rb
111
112
  - lib/hasoffersv3/configuration.rb
112
113
  - lib/hasoffersv3/conversion.rb
113
114
  - lib/hasoffersv3/employee.rb
115
+ - lib/hasoffersv3/error.rb
114
116
  - lib/hasoffersv3/logger.rb
115
117
  - lib/hasoffersv3/offer.rb
116
118
  - lib/hasoffersv3/offer_pixel.rb
@@ -137,6 +139,7 @@ files:
137
139
  - spec/lib/offer_spec.rb
138
140
  - spec/lib/raw_log_spec.rb
139
141
  - spec/lib/report_spec.rb
142
+ - spec/lib/response_spec.rb
140
143
  - spec/spec_helper.rb
141
144
  homepage:
142
145
  licenses:
@@ -158,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
158
161
  version: '0'
159
162
  requirements: []
160
163
  rubyforge_project:
161
- rubygems_version: 2.6.10
164
+ rubygems_version: 2.6.11
162
165
  signing_key:
163
166
  specification_version: 4
164
167
  summary: REST Client for the HasOffers API, version 3.
@@ -181,4 +184,5 @@ test_files:
181
184
  - spec/lib/offer_spec.rb
182
185
  - spec/lib/raw_log_spec.rb
183
186
  - spec/lib/report_spec.rb
187
+ - spec/lib/response_spec.rb
184
188
  - spec/spec_helper.rb