google-ads-common 0.6.4 → 0.7.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.
- data/ChangeLog +4 -0
- data/README +1 -1
- data/lib/ads_common/api.rb +66 -86
- data/lib/ads_common/api_config.rb +37 -64
- data/lib/ads_common/auth/base_handler.rb +9 -19
- data/lib/ads_common/auth/client_login_handler.rb +43 -44
- data/lib/ads_common/auth/oauth_handler.rb +63 -77
- data/lib/ads_common/build/savon_generator.rb +1 -5
- data/lib/ads_common/build/savon_service_generator.rb +3 -16
- data/lib/ads_common/credential_handler.rb +31 -9
- data/lib/ads_common/http.rb +20 -21
- data/lib/ads_common/parameters_validator.rb +2 -1
- data/lib/ads_common/results_extractor.rb +183 -0
- data/lib/ads_common/savon_headers/base_header_handler.rb +26 -21
- data/lib/ads_common/savon_headers/oauth_header_handler.rb +4 -29
- data/lib/ads_common/savon_service.rb +28 -174
- data/lib/ads_common/version.rb +1 -1
- data/test/coverage.rb +35 -0
- data/test/suite_unittests.rb +30 -0
- data/test/test_client_login_handler.rb +41 -8
- data/test/test_config.rb +3 -4
- data/test/test_credential_handler.rb +55 -0
- data/test/test_parameters_validator.rb +24 -1
- data/test/test_results_extractor.rb +165 -0
- data/test/test_savon_service.rb +14 -167
- metadata +38 -25
- data/lib/ads_common/savon_headers/simple_header_handler.rb +0 -63
@@ -25,22 +25,6 @@ require 'ads_common/savon_headers/httpi_request_proxy'
|
|
25
25
|
module AdsCommon
|
26
26
|
module SavonHeaders
|
27
27
|
class OAuthHeaderHandler < BaseHeaderHandler
|
28
|
-
# Enriches soap object with API-specific headers like namespaces, login
|
29
|
-
# credentials etc. Sets the default namespace for the body to the one
|
30
|
-
# specified in initializer.
|
31
|
-
#
|
32
|
-
# Args:
|
33
|
-
# - request: a HTTPI Request for extra configuration
|
34
|
-
# - soap: a Savon soap object to fill fields in
|
35
|
-
# - args: request parameters to adjust for namespaces
|
36
|
-
#
|
37
|
-
# Returns:
|
38
|
-
# - Modified soap structure
|
39
|
-
#
|
40
|
-
def prepare_request(request, soap, args)
|
41
|
-
super(request, soap, args)
|
42
|
-
generate_headers(request, soap)
|
43
|
-
end
|
44
28
|
|
45
29
|
private
|
46
30
|
|
@@ -55,20 +39,11 @@ module AdsCommon
|
|
55
39
|
# - Hash containing a header with filled in credentials
|
56
40
|
#
|
57
41
|
def generate_headers(request, soap)
|
42
|
+
super(request, soap)
|
58
43
|
credentials = @credential_handler.credentials
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
request.url = soap.endpoint
|
63
|
-
request.headers['Authorization'] =
|
64
|
-
@auth_handler.generate_oauth_parameters_string(credentials,
|
65
|
-
request)
|
66
|
-
else
|
67
|
-
request_header[prepend_namespace(header)] = value
|
68
|
-
end
|
69
|
-
request_header
|
70
|
-
end
|
71
|
-
soap.header[prepend_namespace(@element_name)] = request_header
|
44
|
+
request.url = soap.endpoint
|
45
|
+
request.headers['Authorization'] =
|
46
|
+
@auth_handler.auth_string(credentials, request)
|
72
47
|
end
|
73
48
|
end
|
74
49
|
end
|
@@ -19,37 +19,34 @@
|
|
19
19
|
#
|
20
20
|
# Base class for all generated API services based on Savon backend.
|
21
21
|
|
22
|
-
require 'httpi'
|
23
22
|
require 'savon'
|
24
23
|
|
25
24
|
require 'ads_common/http'
|
26
25
|
require 'ads_common/parameters_validator'
|
26
|
+
require 'ads_common/results_extractor'
|
27
27
|
|
28
28
|
module AdsCommon
|
29
29
|
class SavonService
|
30
|
-
|
31
|
-
|
30
|
+
|
31
|
+
attr_accessor :header_handler
|
32
|
+
attr_reader :config
|
32
33
|
attr_reader :version
|
33
34
|
attr_reader :namespace
|
34
35
|
|
35
36
|
# Creates a new service.
|
36
|
-
def initialize(
|
37
|
+
def initialize(config, endpoint, namespace, version)
|
37
38
|
if self.class() == AdsCommon::SavonService
|
38
39
|
raise NoMethodError, 'Tried to instantiate an abstract class'
|
39
40
|
end
|
40
|
-
@
|
41
|
-
@headerhandler = []
|
41
|
+
@config, @version, @namespace = config, version, namespace
|
42
42
|
@client = create_savon_client(endpoint, namespace)
|
43
43
|
end
|
44
44
|
|
45
45
|
private
|
46
46
|
|
47
|
-
#
|
48
|
-
def
|
49
|
-
|
50
|
-
config.log_level = :debug
|
51
|
-
config.logger = logger
|
52
|
-
end
|
47
|
+
# Returns currently configured Logger.
|
48
|
+
def get_logger()
|
49
|
+
return @config.read('library.logger')
|
53
50
|
end
|
54
51
|
|
55
52
|
# Returns ServiceRegistry for the current service. Has to be overridden.
|
@@ -68,26 +65,35 @@ module AdsCommon
|
|
68
65
|
client = Savon::Client.new do |wsdl, httpi|
|
69
66
|
wsdl.endpoint = endpoint
|
70
67
|
wsdl.namespace = namespace
|
71
|
-
AdsCommon::Http.configure_httpi(@
|
68
|
+
AdsCommon::Http.configure_httpi(@config, httpi)
|
69
|
+
end
|
70
|
+
Savon.configure do |config|
|
71
|
+
config.raise_errors = false
|
72
|
+
config.log_level = :debug
|
73
|
+
config.logger = get_logger()
|
72
74
|
end
|
73
75
|
return client
|
74
76
|
end
|
75
77
|
|
76
78
|
# Executes SOAP action specified as a string with given arguments.
|
77
79
|
def execute_action(action_name, args, &block)
|
78
|
-
|
80
|
+
registry = get_service_registry()
|
81
|
+
validator = ParametersValidator.new(registry)
|
79
82
|
args = validator.validate_args(action_name, args)
|
80
83
|
response = execute_soap_request(
|
81
84
|
action_name.to_sym, args, validator.extra_namespaces)
|
82
85
|
log_headers(response.http.headers)
|
83
86
|
handle_errors(response)
|
84
|
-
|
87
|
+
extractor = ResultsExtractor.new(registry)
|
88
|
+
result = extractor.extract_result(response, action_name, &block)
|
89
|
+
run_user_block(extractor, response, result, &block) if block_given?
|
90
|
+
return result
|
85
91
|
end
|
86
92
|
|
87
93
|
# Logs response headers.
|
88
94
|
# TODO: this needs to go on http or httpi level.
|
89
95
|
def log_headers(headers)
|
90
|
-
|
96
|
+
get_logger().debug(headers.map {|k, v| [k, v].join(': ')}.join(', '))
|
91
97
|
end
|
92
98
|
|
93
99
|
# Executes the SOAP request with original SOAP name.
|
@@ -96,16 +102,15 @@ module AdsCommon
|
|
96
102
|
get_service_registry.get_method_signature(action)[:original_name]
|
97
103
|
original_action_name = action if original_action_name.nil?
|
98
104
|
response = @client.request(original_action_name) do |soap|
|
99
|
-
|
105
|
+
soap.body = args
|
106
|
+
set_headers(soap, extra_namespaces)
|
100
107
|
end
|
101
108
|
return response
|
102
109
|
end
|
103
110
|
|
104
111
|
# Executes each handler to generate SOAP headers.
|
105
|
-
def set_headers(soap,
|
106
|
-
@
|
107
|
-
handler.prepare_request(@client.http, soap, args)
|
108
|
-
end
|
112
|
+
def set_headers(soap, extra_namespaces)
|
113
|
+
header_handler.prepare_request(@client.http, soap)
|
109
114
|
soap.namespaces.merge!(extra_namespaces) unless extra_namespaces.nil?
|
110
115
|
end
|
111
116
|
|
@@ -144,22 +149,10 @@ module AdsCommon
|
|
144
149
|
end
|
145
150
|
end
|
146
151
|
|
147
|
-
# Extracts the finest results possible for the given result. Returns the
|
148
|
-
# response itself in worst case (contents unknown).
|
149
|
-
def extract_result(response, action_name, &block)
|
150
|
-
method = get_service_registry.get_method_signature(action_name)
|
151
|
-
action = method[:output][:name].to_sym
|
152
|
-
result = response.to_hash
|
153
|
-
result = result[action] if result.include?(action)
|
154
|
-
result = normalize_output(result, method)
|
155
|
-
run_user_block(response, result, &block) if block_given?
|
156
|
-
return result
|
157
|
-
end
|
158
|
-
|
159
152
|
# Yields to user-specified block with additional information such as
|
160
153
|
# headers.
|
161
|
-
def run_user_block(response, body, &block)
|
162
|
-
header = extract_header_data(response)
|
154
|
+
def run_user_block(extractor, response, body, &block)
|
155
|
+
header = extractor.extract_header_data(response)
|
163
156
|
case block.arity
|
164
157
|
when 1 then yield(header)
|
165
158
|
when 2 then yield(header, body)
|
@@ -169,144 +162,5 @@ module AdsCommon
|
|
169
162
|
end
|
170
163
|
return nil
|
171
164
|
end
|
172
|
-
|
173
|
-
# Extracts misc data from response header.
|
174
|
-
def extract_header_data(response)
|
175
|
-
header_type = get_full_type_signature(:SoapResponseHeader)
|
176
|
-
headers = response.header[:response_header].dup
|
177
|
-
process_attributes(headers, false)
|
178
|
-
result = headers.inject({}) do |result, (key, v)|
|
179
|
-
normalize_output_field(headers, header_type[:fields], key)
|
180
|
-
result[key] = headers[key]
|
181
|
-
result
|
182
|
-
end
|
183
|
-
return result
|
184
|
-
end
|
185
|
-
|
186
|
-
# Normalizes output starting with root node "rval".
|
187
|
-
def normalize_output(output_data, method_definition)
|
188
|
-
fields_list = method_definition[:output][:fields]
|
189
|
-
result = normalize_output_field(output_data, fields_list, :rval)
|
190
|
-
return result[:rval] || result
|
191
|
-
end
|
192
|
-
|
193
|
-
# Normalizes one field of a given data recursively.
|
194
|
-
# Args:
|
195
|
-
# - output_data: XML data to normalize
|
196
|
-
# - fields_list: expected list of fields from signature
|
197
|
-
# - field_name: specifies field name to normalize
|
198
|
-
def normalize_output_field(output_data, fields_list, field_name)
|
199
|
-
return nil if output_data.nil?
|
200
|
-
|
201
|
-
process_attributes(output_data, true)
|
202
|
-
|
203
|
-
field_definition = get_field_by_name(fields_list, field_name)
|
204
|
-
if field_definition.nil?
|
205
|
-
@api.logger.warn("Can not determine type for field: %s" % field_name)
|
206
|
-
return output_data
|
207
|
-
end
|
208
|
-
|
209
|
-
field_sym = field_name.to_sym
|
210
|
-
field_data = normalize_type(output_data[field_sym], field_definition)
|
211
|
-
output_data[field_sym] = field_data if field_data
|
212
|
-
|
213
|
-
sub_type = get_full_type_signature(field_definition[:type])
|
214
|
-
if sub_type and sub_type[:fields]
|
215
|
-
# go recursive
|
216
|
-
sub_type[:fields].each do |sub_type_field|
|
217
|
-
field_data = output_data[field_sym]
|
218
|
-
if field_data.is_a?(Array)
|
219
|
-
field_data.each do |item|
|
220
|
-
normalize_output_field(item, sub_type_field,
|
221
|
-
sub_type_field[:name])
|
222
|
-
end
|
223
|
-
else
|
224
|
-
normalize_output_field(field_data, sub_type_field,
|
225
|
-
sub_type_field[:name])
|
226
|
-
end
|
227
|
-
end
|
228
|
-
end
|
229
|
-
return output_data
|
230
|
-
end
|
231
|
-
|
232
|
-
# Converts XML input string into a native format.
|
233
|
-
def normalize_type(data, field)
|
234
|
-
type_name = field[:type]
|
235
|
-
result = case data
|
236
|
-
when Array
|
237
|
-
data.map {|item| normalize_item(type_name, item)}
|
238
|
-
else
|
239
|
-
normalize_item(type_name, data)
|
240
|
-
end
|
241
|
-
# If field signature allows an array, forcing array structure even for one
|
242
|
-
# item.
|
243
|
-
if !field[:min_occurs].nil? and
|
244
|
-
(field[:max_occurs] == :unbounded ||
|
245
|
-
(!field[:max_occurs].nil? and field[:max_occurs] > 1))
|
246
|
-
result = arrayize(result)
|
247
|
-
end
|
248
|
-
return result
|
249
|
-
end
|
250
|
-
|
251
|
-
# Converts one leaf item to a built-in type.
|
252
|
-
def normalize_item(type_name, item)
|
253
|
-
return (item.nil?) ? item :
|
254
|
-
case type_name
|
255
|
-
when 'long', 'int' then Integer(item)
|
256
|
-
when 'double', 'float' then Float(item)
|
257
|
-
when 'boolean' then item.kind_of?(String) ?
|
258
|
-
item.casecmp('true') == 0 : item
|
259
|
-
else item
|
260
|
-
end
|
261
|
-
end
|
262
|
-
|
263
|
-
# Finds a field in a list by its name.
|
264
|
-
def get_field_by_name(fields_list, name)
|
265
|
-
fields_array = arrayize(fields_list)
|
266
|
-
index = fields_array.find_index {|field| field[:name].eql?(name)}
|
267
|
-
return (index.nil?) ? nil : fields_array.at(index)
|
268
|
-
end
|
269
|
-
|
270
|
-
# Makes sure object is an array.
|
271
|
-
def arrayize(object)
|
272
|
-
return [] if object.nil?
|
273
|
-
return object.is_a?(Array) ? object : [object]
|
274
|
-
end
|
275
|
-
|
276
|
-
# Returns all inherited fields of superclasses for given type.
|
277
|
-
def implode_parent(data_type)
|
278
|
-
result = []
|
279
|
-
if data_type[:base]
|
280
|
-
parent_type = get_service_registry.get_type_signature(data_type[:base])
|
281
|
-
result += implode_parent(parent_type)
|
282
|
-
end
|
283
|
-
data_type[:fields].each do |field|
|
284
|
-
# If the parent type includes a field with the same name, overwrite it.
|
285
|
-
result.reject! {|parent_field| parent_field[:name].eql?(field[:name])}
|
286
|
-
result << field
|
287
|
-
end
|
288
|
-
return result
|
289
|
-
end
|
290
|
-
|
291
|
-
# Returns type signature with all inherited fields.
|
292
|
-
def get_full_type_signature(type_name)
|
293
|
-
result = (type_name.nil?) ? nil :
|
294
|
-
get_service_registry.get_type_signature(type_name)
|
295
|
-
if result and result[:base]
|
296
|
-
result[:fields] = implode_parent(result)
|
297
|
-
end
|
298
|
-
return result
|
299
|
-
end
|
300
|
-
|
301
|
-
# Handles attributes received from Savon.
|
302
|
-
def process_attributes(data, keep_xsi_type = false)
|
303
|
-
if data.kind_of?(Hash)
|
304
|
-
if keep_xsi_type
|
305
|
-
xsi_type = data.delete(:"@xsi:type")
|
306
|
-
data[:xsi_type] = xsi_type if xsi_type
|
307
|
-
end
|
308
|
-
data.reject! {|key, value| key.to_s.start_with?('@')}
|
309
|
-
end
|
310
|
-
end
|
311
165
|
end
|
312
166
|
end
|
data/lib/ads_common/version.rb
CHANGED
data/test/coverage.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Encoding: utf-8
|
3
|
+
#
|
4
|
+
# Author:: api.dklimkin@gmail.com (Danial Klimkin)
|
5
|
+
#
|
6
|
+
# Copyright:: Copyright 2012, Google Inc. All Rights Reserved.
|
7
|
+
#
|
8
|
+
# License:: Licensed under the Apache License, Version 2.0 (the "License");
|
9
|
+
# you may not use this file except in compliance with the License.
|
10
|
+
# You may obtain a copy of the License at
|
11
|
+
#
|
12
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
13
|
+
#
|
14
|
+
# Unless required by applicable law or agreed to in writing, software
|
15
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
16
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
17
|
+
# implied.
|
18
|
+
# See the License for the specific language governing permissions and
|
19
|
+
# limitations under the License.
|
20
|
+
#
|
21
|
+
# Runs test with coverage tool.
|
22
|
+
|
23
|
+
require 'simplecov'
|
24
|
+
|
25
|
+
SimpleCov.start
|
26
|
+
|
27
|
+
$:.unshift File.expand_path('../../', __FILE__)
|
28
|
+
require File.join(File.dirname(__FILE__), 'suite_unittests.rb')
|
29
|
+
|
30
|
+
# Now loading all files in the library to make sure we hit all untested files.
|
31
|
+
lib_base_path = File.expand_path('../../lib', __FILE__)
|
32
|
+
$:.unshift lib_base_path
|
33
|
+
|
34
|
+
code_files_mask = File.join(lib_base_path, '**/*.rb')
|
35
|
+
Dir.glob(code_files_mask).each {|file| require file}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Encoding: utf-8
|
3
|
+
#
|
4
|
+
# Author:: api.dklimkin@gmail.com (Danial Klimkin)
|
5
|
+
#
|
6
|
+
# Copyright:: Copyright 2012, Google Inc. All Rights Reserved.
|
7
|
+
#
|
8
|
+
# License:: Licensed under the Apache License, Version 2.0 (the "License");
|
9
|
+
# you may not use this file except in compliance with the License.
|
10
|
+
# You may obtain a copy of the License at
|
11
|
+
#
|
12
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
13
|
+
#
|
14
|
+
# Unless required by applicable law or agreed to in writing, software
|
15
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
16
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
17
|
+
# implied.
|
18
|
+
# See the License for the specific language governing permissions and
|
19
|
+
# limitations under the License.
|
20
|
+
#
|
21
|
+
# Test suite for unit tests.
|
22
|
+
|
23
|
+
require 'test/unit'
|
24
|
+
|
25
|
+
$:.unshift File.expand_path('../../lib/', __FILE__)
|
26
|
+
$:.unshift File.expand_path('../../', __FILE__)
|
27
|
+
|
28
|
+
# Ads Common units tests.
|
29
|
+
test_files_mask = File.join(File.dirname(__FILE__), 'test_*.rb')
|
30
|
+
Dir.glob(test_files_mask).each {|file| require file}
|
@@ -28,14 +28,19 @@ require 'ads_common/auth/client_login_handler'
|
|
28
28
|
module AdsCommon
|
29
29
|
module Auth
|
30
30
|
class ClientLoginHandler
|
31
|
+
|
31
32
|
public :parse_token_text
|
32
33
|
public :handle_login_error
|
34
|
+
public :validate_credentials
|
35
|
+
public :create_token_from_string
|
33
36
|
end
|
34
37
|
end
|
35
38
|
end
|
36
39
|
|
40
|
+
|
37
41
|
# Stub class for HTTP response.
|
38
42
|
class ResponseStub
|
43
|
+
|
39
44
|
attr_reader :code
|
40
45
|
attr_reader :body
|
41
46
|
|
@@ -44,14 +49,15 @@ class ResponseStub
|
|
44
49
|
end
|
45
50
|
end
|
46
51
|
|
47
|
-
class
|
48
|
-
|
52
|
+
class TestClientLoginHandler < Test::Unit::TestCase
|
53
|
+
|
54
|
+
def setup()
|
49
55
|
config = AdsCommon::Config.new({})
|
50
56
|
@handler = AdsCommon::Auth::ClientLoginHandler.new(
|
51
|
-
config, 'http://www.google.com',
|
57
|
+
config, 'http://www.google.com', 'adwords')
|
52
58
|
end
|
53
59
|
|
54
|
-
def test_handle_login_error_captcha
|
60
|
+
def test_handle_login_error_captcha()
|
55
61
|
assert_raises (AdsCommon::Errors::CaptchaRequiredError) do
|
56
62
|
response = ResponseStub.new(403, '')
|
57
63
|
results = {
|
@@ -62,15 +68,15 @@ class TestParametersValidator < Test::Unit::TestCase
|
|
62
68
|
end
|
63
69
|
end
|
64
70
|
|
65
|
-
def test_handle_login_error_other
|
66
|
-
assert_raises
|
71
|
+
def test_handle_login_error_other()
|
72
|
+
assert_raises(AdsCommon::Errors::AuthError) do
|
67
73
|
response = ResponseStub.new(403, 'Body')
|
68
74
|
results = {'Error' => 'SomeError', 'Info' => 'SomeInfo'}
|
69
75
|
@handler.handle_login_error({}, response, results)
|
70
76
|
end
|
71
77
|
end
|
72
78
|
|
73
|
-
def test_parse_token_text_simple
|
79
|
+
def test_parse_token_text_simple()
|
74
80
|
error_str = "BadAuthentication"
|
75
81
|
text = "Error=%s\n" % error_str
|
76
82
|
result = @handler.parse_token_text(text)
|
@@ -78,7 +84,7 @@ class TestParametersValidator < Test::Unit::TestCase
|
|
78
84
|
assert_equal(['Error'], result.keys)
|
79
85
|
end
|
80
86
|
|
81
|
-
def test_parse_token_text_captcha
|
87
|
+
def test_parse_token_text_captcha()
|
82
88
|
captcha_token = "3u6_27iOel71j525g2tg252ge6t35g345XJtRuHYEYiTyAxsMPz2222442"
|
83
89
|
captcha_url = "Captcha?ctoken=3u245245rgfwrg5g2fw5x3xGqQBrk_AoXXJtRuHY%3a-V"
|
84
90
|
error_str = "CaptchaRequired"
|
@@ -95,4 +101,31 @@ class TestParametersValidator < Test::Unit::TestCase
|
|
95
101
|
assert_equal(['CaptchaToken', 'CaptchaUrl', 'Error', 'Url'],
|
96
102
|
result.keys.sort)
|
97
103
|
end
|
104
|
+
|
105
|
+
def test_validate_credentials_valid()
|
106
|
+
credentials1 = {:email => 'email@example.com', :password => 'qwerty'}
|
107
|
+
credentials2 = {:auth_token => 'QazSWXEDEDCE434234'}
|
108
|
+
assert_nothing_raised do
|
109
|
+
@handler.validate_credentials(credentials1)
|
110
|
+
end
|
111
|
+
assert_nothing_raised do
|
112
|
+
@handler.validate_credentials(credentials2)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_validate_credentials_invalid()
|
117
|
+
credentials1 = {:email => 'email@example.com'}
|
118
|
+
credentials2 = {:password => 'qwerty'}
|
119
|
+
assert_raises(AdsCommon::Errors::AuthError) do
|
120
|
+
@handler.validate_credentials(credentials1)
|
121
|
+
end
|
122
|
+
assert_raises(AdsCommon::Errors::AuthError) do
|
123
|
+
@handler.validate_credentials(credentials2)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_create_token_from_string()
|
128
|
+
test_text = 'fooBar'
|
129
|
+
assert_equal(test_text, @handler.create_token_from_string(test_text))
|
130
|
+
end
|
98
131
|
end
|