bing_ads_ruby_sdk 0.0.0 → 1.0.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/.gitignore +6 -1
- data/.rspec +3 -1
- data/Gemfile +0 -3
- data/LICENSE.txt +1 -1
- data/README.md +70 -9
- data/Rakefile +2 -0
- data/bin/setup +1 -3
- data/bing_ads_ruby_sdk.gemspec +17 -22
- data/changelog.md +19 -0
- data/lib/bing_ads_ruby_sdk.rb +32 -3
- data/lib/bing_ads_ruby_sdk/api.rb +81 -0
- data/lib/bing_ads_ruby_sdk/augmented_parser.rb +79 -0
- data/lib/bing_ads_ruby_sdk/configuration.rb +26 -0
- data/lib/bing_ads_ruby_sdk/errors/error_handler.rb +65 -0
- data/lib/bing_ads_ruby_sdk/errors/errors.rb +192 -0
- data/lib/bing_ads_ruby_sdk/header.rb +44 -0
- data/lib/bing_ads_ruby_sdk/http_client.rb +65 -0
- data/lib/bing_ads_ruby_sdk/log_message.rb +57 -0
- data/lib/bing_ads_ruby_sdk/oauth2/authorization_handler.rb +94 -0
- data/lib/bing_ads_ruby_sdk/oauth2/fs_store.rb +34 -0
- data/lib/bing_ads_ruby_sdk/postprocessors/cast_long_arrays.rb +35 -0
- data/lib/bing_ads_ruby_sdk/postprocessors/snakize.rb +35 -0
- data/lib/bing_ads_ruby_sdk/preprocessors/camelize.rb +45 -0
- data/lib/bing_ads_ruby_sdk/preprocessors/order.rb +58 -0
- data/lib/bing_ads_ruby_sdk/services/ad_insight.rb +10 -0
- data/lib/bing_ads_ruby_sdk/services/base.rb +83 -0
- data/lib/bing_ads_ruby_sdk/services/bulk.rb +26 -0
- data/lib/bing_ads_ruby_sdk/services/campaign_management.rb +116 -0
- data/lib/bing_ads_ruby_sdk/services/customer_billing.rb +10 -0
- data/lib/bing_ads_ruby_sdk/services/customer_management.rb +26 -0
- data/lib/bing_ads_ruby_sdk/services/reporting.rb +10 -0
- data/lib/bing_ads_ruby_sdk/soap_client.rb +143 -0
- data/lib/bing_ads_ruby_sdk/string_utils.rb +23 -0
- data/lib/bing_ads_ruby_sdk/version.rb +4 -1
- data/lib/bing_ads_ruby_sdk/wsdl/v12/production/ad_insight.xml +1 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/production/bulk.xml +1 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/production/campaign_management.xml +1 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/production/customer_billing.xml +1 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/production/customer_management.xml +1 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/production/reporting.xml +1 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/sandbox/ad_insight.xml +1 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/sandbox/bulk.xml +1 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/sandbox/campaign_management.xml +1 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/sandbox/customer_billing.xml +1 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/sandbox/customer_management.xml +1 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/sandbox/reporting.xml +1 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/test/ad_insight.xml +3065 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/test/bulk.xml +1424 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/test/campaign_management.xml +9949 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/test/customer_billing.xml +899 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/test/customer_management.xml +3966 -0
- data/lib/bing_ads_ruby_sdk/wsdl/v12/test/reporting.xml +3742 -0
- data/lib/bing_ads_ruby_sdk/wsdl/wsdl_source.txt +25 -0
- data/lib/bing_ads_ruby_sdk/wsdl_operation_wrapper.rb +39 -0
- data/tasks/bing_ads_ruby_sdk.rake +28 -0
- metadata +137 -38
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BingAdsRubySdk
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :pretty_print_xml, :filters, :log
|
6
|
+
attr_writer :logger
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@log = false
|
10
|
+
@pretty_print_xml = false
|
11
|
+
@filters = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def logger
|
15
|
+
@logger ||= default_logger
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def default_logger
|
21
|
+
Logger.new(File.join(BingAdsRubySdk::ROOT_PATH, "log", "bing-sdk.log")).tap do |l|
|
22
|
+
l.level = Logger::INFO
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BingAdsRubySdk
|
4
|
+
module Errors
|
5
|
+
# Parses the response from the API to raise errors if they are returned
|
6
|
+
class ErrorHandler
|
7
|
+
def initialize(response)
|
8
|
+
@response = response
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
# Some operations don't return a response, for example:
|
13
|
+
# https://msdn.microsoft.com/en-us/library/bing-ads-customer-management-deleteaccount.aspx
|
14
|
+
return unless response.is_a? Hash
|
15
|
+
raise fault_class.new(response) if contains_error?
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :response
|
21
|
+
|
22
|
+
def contains_error?
|
23
|
+
partial_error_keys.any? || contains_fault?
|
24
|
+
end
|
25
|
+
|
26
|
+
def contains_fault?
|
27
|
+
(ERROR_KEYS & response.keys).any?
|
28
|
+
end
|
29
|
+
|
30
|
+
def fault_class
|
31
|
+
ERRORS_MAPPING.fetch(hash_with_error.keys.first, BASE_FAULT)
|
32
|
+
end
|
33
|
+
|
34
|
+
def hash_with_error
|
35
|
+
response[:detail] || partial_errors || {}
|
36
|
+
end
|
37
|
+
|
38
|
+
def partial_errors
|
39
|
+
response.select {|key| partial_error_keys.include?(key)}
|
40
|
+
end
|
41
|
+
|
42
|
+
# Gets populated partial error keys from the response
|
43
|
+
# @return [Array] array of symbols for keys in the response
|
44
|
+
# that are populated with errors
|
45
|
+
def partial_error_keys
|
46
|
+
@partial_error_keys ||= (PARTIAL_ERROR_KEYS & response.keys).reject do |key|
|
47
|
+
response[key].nil? || response[key].is_a?(String)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
BASE_FAULT = BingAdsRubySdk::Errors::GeneralError
|
52
|
+
PARTIAL_ERROR_KEYS = %i[partial_errors nested_partial_errors].freeze
|
53
|
+
ERROR_KEYS = %i[faultcode error_code]
|
54
|
+
ERRORS_MAPPING = {
|
55
|
+
api_fault_detail: BingAdsRubySdk::Errors::ApiFaultDetail,
|
56
|
+
ad_api_fault_detail: BingAdsRubySdk::Errors::AdApiFaultDetail,
|
57
|
+
editorial_api_fault_detail: BingAdsRubySdk::Errors::EditorialApiFaultDetail,
|
58
|
+
api_batch_fault: BingAdsRubySdk::Errors::ApiBatchFault,
|
59
|
+
api_fault: BingAdsRubySdk::Errors::ApiFault,
|
60
|
+
nested_partial_errors: BingAdsRubySdk::Errors::NestedPartialError,
|
61
|
+
partial_errors: BingAdsRubySdk::Errors::PartialError
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BingAdsRubySdk
|
4
|
+
module Errors
|
5
|
+
# Base exception class for reporting API errors
|
6
|
+
class GeneralError < ::StandardError
|
7
|
+
attr_accessor :raw_response, :message
|
8
|
+
|
9
|
+
def initialize(response)
|
10
|
+
@raw_response = response
|
11
|
+
|
12
|
+
code = response[:error_code] || 'Bing Ads API error'
|
13
|
+
|
14
|
+
message = response[:message] ||
|
15
|
+
response[:faultstring] ||
|
16
|
+
'See exception details for more information.'
|
17
|
+
|
18
|
+
@message = format_message(code, message)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Format the message separated by hyphen if
|
24
|
+
# there is a code and a message
|
25
|
+
def format_message(code, message)
|
26
|
+
[code, message].compact.join(' - ')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class ServerError < GeneralError
|
31
|
+
def initialize(server_error)
|
32
|
+
super "Server raised error #{server_error}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Base exception class for handling errors where the detail is supplied
|
37
|
+
class ApplicationFault < GeneralError
|
38
|
+
def initialize(response)
|
39
|
+
super
|
40
|
+
|
41
|
+
populate_error_lists
|
42
|
+
end
|
43
|
+
|
44
|
+
def message
|
45
|
+
error_list = all_errors
|
46
|
+
return @message if error_list.empty?
|
47
|
+
|
48
|
+
first_message = first_error_message(error_list)
|
49
|
+
if error_list.count > 1
|
50
|
+
"API raised #{ error_list.count } errors, including: #{first_message}"
|
51
|
+
else
|
52
|
+
first_message
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def populate_error_lists
|
59
|
+
self.class.error_lists.each do |key|
|
60
|
+
instance_variable_set("@#{key}", array_wrap(fault_hash[key]))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def all_errors
|
65
|
+
self.class.error_lists.flat_map do |list_name|
|
66
|
+
list = send(list_name)
|
67
|
+
|
68
|
+
# Call sometimes returns an empty string instead of
|
69
|
+
# nil for empty lists
|
70
|
+
list.nil? || list.empty? ? nil : list
|
71
|
+
end.compact
|
72
|
+
end
|
73
|
+
|
74
|
+
# The fault hash from the API response detail element
|
75
|
+
# @return [Hash] containing the fault information if provided
|
76
|
+
# @return [Hash] empty hash if no fault information
|
77
|
+
def fault_hash
|
78
|
+
raw_response[:detail][fault_key] || {}
|
79
|
+
end
|
80
|
+
|
81
|
+
# The fault key that corresponds to the inherited class
|
82
|
+
# @return [Symbol] the fault key
|
83
|
+
def fault_key
|
84
|
+
class_name = self.class.name.split('::').last
|
85
|
+
BingAdsRubySdk::StringUtils.snakize(class_name).to_sym
|
86
|
+
end
|
87
|
+
|
88
|
+
def first_error_message(error_list)
|
89
|
+
error = error_list.first.values.first
|
90
|
+
format_message(error[:error_code], error[:message])
|
91
|
+
end
|
92
|
+
|
93
|
+
def array_wrap(value)
|
94
|
+
case value
|
95
|
+
when Array then value
|
96
|
+
when nil, "" then []
|
97
|
+
else
|
98
|
+
[value]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class << self
|
103
|
+
def error_lists=(value)
|
104
|
+
@error_lists = value
|
105
|
+
end
|
106
|
+
|
107
|
+
def error_lists
|
108
|
+
@error_lists ||= []
|
109
|
+
end
|
110
|
+
|
111
|
+
def define_error_lists(*error_list_array)
|
112
|
+
self.error_lists += error_list_array
|
113
|
+
|
114
|
+
error_list_array.each { |attr| attr_accessor attr }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Base class for handling partial errors
|
120
|
+
class PartialErrorBase < ApplicationFault
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# The parent hash for this type of error is the root of the response
|
125
|
+
def fault_hash
|
126
|
+
raw_response[fault_key] || {}
|
127
|
+
end
|
128
|
+
|
129
|
+
# Gets the first error message in the list. This is
|
130
|
+
# overridden because partial errors are structured differently
|
131
|
+
# to application faults
|
132
|
+
# @return [Hash] containing the details of the error
|
133
|
+
def first_error_message(error_list)
|
134
|
+
error = error_list.first
|
135
|
+
format_message(error[:error_code], error[:message])
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class PartialError < PartialErrorBase
|
140
|
+
define_error_lists :batch_error
|
141
|
+
|
142
|
+
private
|
143
|
+
def fault_key
|
144
|
+
:partial_errors
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
class NestedPartialError < PartialErrorBase
|
149
|
+
define_error_lists :batch_error_collection
|
150
|
+
|
151
|
+
private
|
152
|
+
def fault_key
|
153
|
+
:nested_partial_errors
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# For handling API errors of the same name.
|
158
|
+
# Documentation:
|
159
|
+
# https://msdn.microsoft.com/en-gb/library/bing-ads-overview-adapifaultdetail.aspx
|
160
|
+
class AdApiFaultDetail < ApplicationFault
|
161
|
+
define_error_lists :errors
|
162
|
+
end
|
163
|
+
|
164
|
+
# For handling API errors of the same name.
|
165
|
+
# Documentation:
|
166
|
+
# https://msdn.microsoft.com/en-gb/library/bing-ads-overview-apifaultdetail.aspx
|
167
|
+
class ApiFaultDetail < ApplicationFault
|
168
|
+
define_error_lists :batch_errors, :operation_errors
|
169
|
+
end
|
170
|
+
|
171
|
+
# For handling API errors of the same name.
|
172
|
+
# Documentation:
|
173
|
+
# https://msdn.microsoft.com/en-gb/library/bing-ads-overview-editorialapifaultdetail.aspx
|
174
|
+
class EditorialApiFaultDetail < ApplicationFault
|
175
|
+
define_error_lists :batch_errors, :editorial_errors, :operation_errors
|
176
|
+
end
|
177
|
+
|
178
|
+
# For handling API errors of the same name.
|
179
|
+
# Documentation:
|
180
|
+
# https://msdn.microsoft.com/en-gb/library/bing-ads-apibatchfault-customer-billing.aspx
|
181
|
+
class ApiBatchFault < ApplicationFault
|
182
|
+
define_error_lists :batch_errors, :operation_errors
|
183
|
+
end
|
184
|
+
|
185
|
+
# For handling API errors of the same name.
|
186
|
+
# Documentation:
|
187
|
+
# https://msdn.microsoft.com/en-gb/library/bing-ads-apifault-customer-billing.aspx
|
188
|
+
class ApiFault < ApplicationFault
|
189
|
+
define_error_lists :operation_errors
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BingAdsRubySdk
|
4
|
+
# Contains the SOAP Request header informations
|
5
|
+
class Header
|
6
|
+
# @param developer_token
|
7
|
+
# @param client_id
|
8
|
+
# @param store instance of a store
|
9
|
+
def initialize(developer_token:, client_id:, store:)
|
10
|
+
@developer_token = developer_token
|
11
|
+
@client_id = client_id
|
12
|
+
@oauth_store = store
|
13
|
+
@customer = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Hash] Authorization and identification data that will be added to the SOAP header
|
17
|
+
def content
|
18
|
+
{
|
19
|
+
"AuthenticationToken" => auth_handler.fetch_or_refresh,
|
20
|
+
"DeveloperToken" => developer_token,
|
21
|
+
"CustomerId" => customer[:customer_id],
|
22
|
+
"CustomerAccountId" => customer[:account_id]
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_customer(account_id:, customer_id:)
|
27
|
+
customer[:account_id] = account_id
|
28
|
+
customer[:customer_id] = customer_id
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :oauth_store, :developer_token, :client_id, :customer
|
35
|
+
|
36
|
+
def auth_handler
|
37
|
+
@auth_handler ||= ::BingAdsRubySdk::OAuth2::AuthorizationHandler.new(
|
38
|
+
developer_token: developer_token,
|
39
|
+
client_id: client_id,
|
40
|
+
store: oauth_store
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "excon"
|
5
|
+
|
6
|
+
module BingAdsRubySdk
|
7
|
+
class HttpClient
|
8
|
+
@http_connections = {}
|
9
|
+
HTTP_OPEN_TIMEOUT = 10
|
10
|
+
HTTP_READ_TIMEOUT = 20
|
11
|
+
HTTP_RETRY_COUNT_ON_TIMEOUT = 2
|
12
|
+
HTTP_INTERVAL_RETRY_COUNT_ON_TIMEOUT = 1
|
13
|
+
HTTP_ERRORS = [ Net::HTTPServerError, Net::HTTPClientError ]
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def post(request)
|
17
|
+
uri = URI(request.url)
|
18
|
+
conn = self.connection(request.url)
|
19
|
+
raw_response = conn.post(
|
20
|
+
path: uri.path,
|
21
|
+
body: request.content,
|
22
|
+
headers: request.headers,
|
23
|
+
)
|
24
|
+
|
25
|
+
if contains_error?(raw_response)
|
26
|
+
BingAdsRubySdk.log(:warn) { BingAdsRubySdk::LogMessage.new(raw_response.body).to_s }
|
27
|
+
raise BingAdsRubySdk::Errors::ServerError, raw_response.body
|
28
|
+
else
|
29
|
+
BingAdsRubySdk.log(:debug) { BingAdsRubySdk::LogMessage.new(raw_response.body).to_s }
|
30
|
+
end
|
31
|
+
|
32
|
+
raw_response.body
|
33
|
+
end
|
34
|
+
|
35
|
+
def close_http_connections
|
36
|
+
self.http_connections.values.each do |connection|
|
37
|
+
connection.reset
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
attr_reader :http_connections
|
44
|
+
|
45
|
+
def contains_error?(response)
|
46
|
+
HTTP_ERRORS.any? { |http_error_class| response.class <= http_error_class }
|
47
|
+
end
|
48
|
+
|
49
|
+
def connection(host)
|
50
|
+
self.http_connections[host] ||= Excon.new(
|
51
|
+
host,
|
52
|
+
persistent: true,
|
53
|
+
tcp_nodelay: true,
|
54
|
+
retry_limit: HTTP_RETRY_COUNT_ON_TIMEOUT,
|
55
|
+
idempotent: true,
|
56
|
+
retry_interval: HTTP_INTERVAL_RETRY_COUNT_ON_TIMEOUT,
|
57
|
+
connect_timeout: HTTP_OPEN_TIMEOUT,
|
58
|
+
read_timeout: HTTP_READ_TIMEOUT,
|
59
|
+
ssl_version: :TLSv1_2,
|
60
|
+
ciphers: "TLSv1.2:!aNULL:!eNULL",
|
61
|
+
)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BingAdsRubySdk
|
4
|
+
class LogMessage
|
5
|
+
|
6
|
+
def initialize(message)
|
7
|
+
@message = message
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
return message unless message_is_xml
|
12
|
+
return message unless filters.any? || pretty_print
|
13
|
+
|
14
|
+
document = Nokogiri::XML(message)
|
15
|
+
document = apply_filter(document) if filters.any?
|
16
|
+
document.to_xml(nokogiri_options)
|
17
|
+
end
|
18
|
+
|
19
|
+
FILTERED = "***FILTERED***"
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :message
|
24
|
+
|
25
|
+
def message_is_xml
|
26
|
+
message =~ /^</
|
27
|
+
end
|
28
|
+
|
29
|
+
def apply_filter(document)
|
30
|
+
return document unless document.errors.empty?
|
31
|
+
|
32
|
+
filters.each do |filter|
|
33
|
+
apply_filter! document, filter
|
34
|
+
end
|
35
|
+
|
36
|
+
document
|
37
|
+
end
|
38
|
+
|
39
|
+
def apply_filter!(document, filter)
|
40
|
+
document.xpath("//*[local-name()='#{filter}']").each do |node|
|
41
|
+
node.content = FILTERED
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def nokogiri_options
|
46
|
+
pretty_print ? { indent: 2 } : { save_with: Nokogiri::XML::Node::SaveOptions::AS_XML }
|
47
|
+
end
|
48
|
+
|
49
|
+
def pretty_print
|
50
|
+
BingAdsRubySdk.config.pretty_print_xml
|
51
|
+
end
|
52
|
+
|
53
|
+
def filters
|
54
|
+
BingAdsRubySdk.config.filters
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|