google-ads-common 0.6.4 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|