hasoffersv3 0.5.5 → 0.6.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 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