bing_ads_ruby_sdk 0.0.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|