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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -1
  3. data/.rspec +3 -1
  4. data/Gemfile +0 -3
  5. data/LICENSE.txt +1 -1
  6. data/README.md +70 -9
  7. data/Rakefile +2 -0
  8. data/bin/setup +1 -3
  9. data/bing_ads_ruby_sdk.gemspec +17 -22
  10. data/changelog.md +19 -0
  11. data/lib/bing_ads_ruby_sdk.rb +32 -3
  12. data/lib/bing_ads_ruby_sdk/api.rb +81 -0
  13. data/lib/bing_ads_ruby_sdk/augmented_parser.rb +79 -0
  14. data/lib/bing_ads_ruby_sdk/configuration.rb +26 -0
  15. data/lib/bing_ads_ruby_sdk/errors/error_handler.rb +65 -0
  16. data/lib/bing_ads_ruby_sdk/errors/errors.rb +192 -0
  17. data/lib/bing_ads_ruby_sdk/header.rb +44 -0
  18. data/lib/bing_ads_ruby_sdk/http_client.rb +65 -0
  19. data/lib/bing_ads_ruby_sdk/log_message.rb +57 -0
  20. data/lib/bing_ads_ruby_sdk/oauth2/authorization_handler.rb +94 -0
  21. data/lib/bing_ads_ruby_sdk/oauth2/fs_store.rb +34 -0
  22. data/lib/bing_ads_ruby_sdk/postprocessors/cast_long_arrays.rb +35 -0
  23. data/lib/bing_ads_ruby_sdk/postprocessors/snakize.rb +35 -0
  24. data/lib/bing_ads_ruby_sdk/preprocessors/camelize.rb +45 -0
  25. data/lib/bing_ads_ruby_sdk/preprocessors/order.rb +58 -0
  26. data/lib/bing_ads_ruby_sdk/services/ad_insight.rb +10 -0
  27. data/lib/bing_ads_ruby_sdk/services/base.rb +83 -0
  28. data/lib/bing_ads_ruby_sdk/services/bulk.rb +26 -0
  29. data/lib/bing_ads_ruby_sdk/services/campaign_management.rb +116 -0
  30. data/lib/bing_ads_ruby_sdk/services/customer_billing.rb +10 -0
  31. data/lib/bing_ads_ruby_sdk/services/customer_management.rb +26 -0
  32. data/lib/bing_ads_ruby_sdk/services/reporting.rb +10 -0
  33. data/lib/bing_ads_ruby_sdk/soap_client.rb +143 -0
  34. data/lib/bing_ads_ruby_sdk/string_utils.rb +23 -0
  35. data/lib/bing_ads_ruby_sdk/version.rb +4 -1
  36. data/lib/bing_ads_ruby_sdk/wsdl/v12/production/ad_insight.xml +1 -0
  37. data/lib/bing_ads_ruby_sdk/wsdl/v12/production/bulk.xml +1 -0
  38. data/lib/bing_ads_ruby_sdk/wsdl/v12/production/campaign_management.xml +1 -0
  39. data/lib/bing_ads_ruby_sdk/wsdl/v12/production/customer_billing.xml +1 -0
  40. data/lib/bing_ads_ruby_sdk/wsdl/v12/production/customer_management.xml +1 -0
  41. data/lib/bing_ads_ruby_sdk/wsdl/v12/production/reporting.xml +1 -0
  42. data/lib/bing_ads_ruby_sdk/wsdl/v12/sandbox/ad_insight.xml +1 -0
  43. data/lib/bing_ads_ruby_sdk/wsdl/v12/sandbox/bulk.xml +1 -0
  44. data/lib/bing_ads_ruby_sdk/wsdl/v12/sandbox/campaign_management.xml +1 -0
  45. data/lib/bing_ads_ruby_sdk/wsdl/v12/sandbox/customer_billing.xml +1 -0
  46. data/lib/bing_ads_ruby_sdk/wsdl/v12/sandbox/customer_management.xml +1 -0
  47. data/lib/bing_ads_ruby_sdk/wsdl/v12/sandbox/reporting.xml +1 -0
  48. data/lib/bing_ads_ruby_sdk/wsdl/v12/test/ad_insight.xml +3065 -0
  49. data/lib/bing_ads_ruby_sdk/wsdl/v12/test/bulk.xml +1424 -0
  50. data/lib/bing_ads_ruby_sdk/wsdl/v12/test/campaign_management.xml +9949 -0
  51. data/lib/bing_ads_ruby_sdk/wsdl/v12/test/customer_billing.xml +899 -0
  52. data/lib/bing_ads_ruby_sdk/wsdl/v12/test/customer_management.xml +3966 -0
  53. data/lib/bing_ads_ruby_sdk/wsdl/v12/test/reporting.xml +3742 -0
  54. data/lib/bing_ads_ruby_sdk/wsdl/wsdl_source.txt +25 -0
  55. data/lib/bing_ads_ruby_sdk/wsdl_operation_wrapper.rb +39 -0
  56. data/tasks/bing_ads_ruby_sdk.rake +28 -0
  57. 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