google-adwords-api 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,564 @@
1
+ #!/usr/bin/ruby
2
+ #
3
+ # Author:: api.sgomes@gmail.com (Sérgio Gomes)
4
+ #
5
+ # Copyright:: Copyright 2011, Google Inc. All Rights Reserved.
6
+ #
7
+ # License:: Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16
+ # implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ #
20
+ # Generates the wrappers for AdWords API services. Only used during the
21
+ # 'rake generate' step of library setup.
22
+
23
+ module AdwordsApi
24
+ module Soap4r
25
+
26
+ # Contains the methods that handle wrapper code generation.
27
+ module Soap4rGenerator
28
+ ARRAY_CLASSNAME = 'SOAP::SOAPArray'
29
+
30
+ # Should be overriden for specific APIs, to contain the API config
31
+ # module.
32
+ def api_config
33
+ nil
34
+ end
35
+
36
+ # Should be overriden for specific APIs, to contain the extension config
37
+ # module.
38
+ def extension_config
39
+ nil
40
+ end
41
+
42
+ # Should be overriden for specific APIs, to contain an instance of
43
+ # AdsCommon::Config with the configs for the appropriate library.
44
+ def config
45
+ nil
46
+ end
47
+
48
+ # Converts from camelCase names to underscore_separated names.
49
+ #
50
+ # Args:
51
+ # - text: the text to be converted
52
+ #
53
+ def underscore(text)
54
+ text.gsub(/[a-z0-9][A-Z]/) do |match|
55
+ match[0,1] + '_' + match[1,1].downcase
56
+ end
57
+ end
58
+
59
+ # Generate the wrapper class for a given service.
60
+ # These classes make it easier to invoke the API methods, by removing the
61
+ # need to instance a <MethodName> object, instead allowing passing of the
62
+ # call parameters directly.
63
+ #
64
+ # Args:
65
+ # - version: the API version (as an integer)
66
+ # - service: the service name (as a string)
67
+ #
68
+ # Returns:
69
+ # The Ruby code for the class, as a string.
70
+ #
71
+ def generate_wrapper_class(version, service)
72
+ wrapper = service.to_s + "Wrapper"
73
+ module_name = api_config.module_name(version, service)
74
+ driver = api_config.interface_name(version, service)
75
+ driver_class = eval(driver)
76
+ api_name = api_config.api_name
77
+
78
+ registry =
79
+ eval("#{module_name}::DefaultMappingRegistry::LiteralRegistry")
80
+
81
+ class_def = <<-EOS
82
+ # This file was automatically generated during the "rake generate" step of
83
+ # library setup.
84
+ require '#{api_config.api_path}/#{version}/#{service}Driver.rb'
85
+
86
+ module #{api_name}
87
+ module #{version.to_s.upcase}
88
+ module #{service}
89
+
90
+ # Wrapper class for the #{version.to_s} #{service} service.
91
+ # This class is automatically generated.
92
+ class #{wrapper}
93
+
94
+ # Holds the API object to which the wrapper belongs.
95
+ attr_reader :api
96
+
97
+ # Version and service utility fields.
98
+ attr_reader :version, :service
99
+
100
+ REGISTRY = #{module_name}::DefaultMappingRegistry::LiteralRegistry
101
+ # This takes advantage of the code generated by soap4r to get the
102
+ # correct namespace for a given service. It accesses one of the fields
103
+ # in the description of the service's methods, which indicates the
104
+ # namespace.
105
+ # Since we're using a fixed version of soap4r (1.5.8), and this is
106
+ # automatically generated as part of the stub generation, it will
107
+ # always point to what we want.
108
+ NAMESPACE = '#{driver_class::Methods[0][2][0][2][1]}'
109
+
110
+ # Holds a shortcut to the parent module.
111
+ # Use this to avoid typing the full class name when creating classes
112
+ # belonging to this service, e.g.
113
+ # service_object.module::ClassName
114
+ # instead of
115
+ # #{api_name}::#{version.to_s.upcase}::#{service}::ClassName
116
+ # This will make it easier to migrate your code between API versions.
117
+ attr_reader :module
118
+
119
+ public
120
+
121
+ # Constructor for #{wrapper}.
122
+ #
123
+ # Args:
124
+ # - driver: SOAP::RPC::Driver object with the remote SOAP methods for
125
+ # this service
126
+ # - api: the API object to which the wrapper belongs
127
+ #
128
+ def initialize(driver, api)
129
+ @driver = driver
130
+ @api = api
131
+ @module = #{api_name}::#{version.to_s.upcase}::#{service}
132
+ @version = :#{version}
133
+ @service = :#{service}
134
+ end
135
+
136
+ # Returns the namespace for this service.
137
+ def namespace
138
+ return NAMESPACE
139
+ end
140
+
141
+ private
142
+
143
+ # Converts from underscore_separated names to camelCase names.
144
+ #
145
+ # Args:
146
+ # - text: the text to be converted
147
+ #
148
+ def camel_case(text)
149
+ text.gsub(/_\\w/) {|match| match[1..-1].upcase}
150
+ end
151
+
152
+ # Converts from camelCase names to underscore_separated names.
153
+ #
154
+ # Args:
155
+ # - text: the text to be converted
156
+ #
157
+ def underscore(text)
158
+ text.gsub(/[a-z0-9][A-Z]/) do |match|
159
+ match[0,1] + '_' + match[1,1].downcase
160
+ end
161
+ end
162
+
163
+ # Validates whether an object is of the correct type.
164
+ # This method is invoked by the hash to object converter during
165
+ # runtime to check the type validity of every object.
166
+ #
167
+ # Args:
168
+ # - object: the hash "object" being evaluated
169
+ # - type: the expected type (the class object itself)
170
+ #
171
+ # Returns:
172
+ # nil, upon success
173
+ #
174
+ # Raises:
175
+ # - ArgumentError: in case of an unexpected type
176
+ #
177
+ def validate_object(object, type)
178
+ return nil if object.is_a? type
179
+
180
+ wsdl_type_obj = type.new
181
+
182
+ if object.is_a? Hash
183
+ xsi_type = object[:xsi_type] or object['xsi_type']
184
+ if xsi_type
185
+ begin
186
+ subtype = @module.class_eval(xsi_type)
187
+ user_type_obj = subtype.new
188
+ rescue
189
+ raise ArgumentError, "Specified xsi_type '" + xsi_type +
190
+ "' is unknown"
191
+ end
192
+ unless user_type_obj.is_a? type
193
+ raise ArgumentError, "Specified xsi_type '" + xsi_type +
194
+ "' is not a subclass of " + type.to_s
195
+ end
196
+ else
197
+ object.each do |key, value|
198
+ if key.to_s != 'xsi_type'
199
+ if !wsdl_type_obj.respond_to?(camel_case(key.to_s).to_sym)
200
+ raise ArgumentError, "Unknown property '" + key.to_s +
201
+ "' for type " + type.to_s
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ return nil
208
+ end
209
+
210
+ # Sets a property on a real (soap4r-generated) object.
211
+ #
212
+ # Args:
213
+ # - object: the object being modified
214
+ # - property: the property being set
215
+ # - value: the value it's being set to
216
+ #
217
+ def set_object_property(object, property, value)
218
+ begin
219
+ object.send(property.to_s + '=', value)
220
+ rescue
221
+ object_class = object.class.name.split('::').last
222
+ error = AdsCommon::Errors::MissingPropertyError.new(
223
+ property, object_class)
224
+ message = "'Missing property `" + property.to_s +
225
+ "' for object class `" + object_class + "'"
226
+ raise(error, message)
227
+ end
228
+ end
229
+
230
+ public
231
+
232
+ # Converts dynamic objects (property hashes) into real soap4r objects.
233
+ # This is meant to be called when setting properties on a class, so
234
+ # the method receives an optional parameter specifying the class and
235
+ # property. This way, it's possible to determine the default type for
236
+ # the object if none is provided.
237
+ #
238
+ # Args:
239
+ # - object: the object being converted
240
+ # - parent_class: the class whose property is being set
241
+ # - property: the property being set
242
+ #
243
+ def convert_to_object(object, parent_class = nil, property = nil)
244
+ property = camel_case(property.to_s) if property
245
+ if object.is_a? Hash
246
+ # Process a hash.
247
+ specified_class = object[:xsi_type] or object['xsi_type']
248
+ default_class = nil
249
+ # Determine default class for this object, given the property
250
+ # being set.
251
+ if parent_class and property
252
+ parent = REGISTRY.schema_definition_from_class(parent_class)
253
+ element = parent.elements.entries.find do |entry|
254
+ entry.varname.to_s == property.to_s
255
+ end
256
+ default_class = element.mapped_class if element
257
+ end
258
+ validate_object(object, default_class)
259
+ real_class = nil
260
+ if specified_class
261
+ real_class = @module.class_eval(specified_class)
262
+ else
263
+ real_class = default_class
264
+ end
265
+ # Instance real object.
266
+ real_object = real_class.new
267
+ # Set each of its properties.
268
+ object.each do |entry, value|
269
+ entry = entry.to_s
270
+ unless entry == 'xsi_type'
271
+ if @api.config.read('service.use_ruby_names', true)
272
+ entry = camel_case(entry)
273
+ end
274
+ if value.is_a? Hash
275
+ # Recurse.
276
+ set_object_property(real_object, entry,
277
+ convert_to_object(value, real_class, entry))
278
+ elsif value.is_a? Array
279
+ set_object_property(real_object, entry,
280
+ value.map do |item|
281
+ # Recurse.
282
+ convert_to_object(item, real_class, entry)
283
+ end
284
+ )
285
+ else
286
+ set_object_property(real_object, entry, value)
287
+ end
288
+ end
289
+ end
290
+ return real_object
291
+ elsif object.is_a? Array
292
+ # Process an array
293
+ return object.map do |entry|
294
+ # Recurse.
295
+ convert_to_object(entry, parent_class, property)
296
+ end
297
+ else
298
+ return object
299
+ end
300
+ end
301
+
302
+ # Converts real soap4r objects into dynamic ones (property hashes).
303
+ # This is meant to be called for return objects of remote calls.
304
+ #
305
+ # Args:
306
+ # - object: the object being converted
307
+ #
308
+ def convert_from_object(object)
309
+ if object.class.name =~
310
+ /#{api_config.api_name}::#{version.to_s.upcase}::\\w+::\\w+/
311
+ # Handle soap4r object
312
+ object_class = REGISTRY.schema_definition_from_class(object.class)
313
+ if object_class.elements and !object_class.elements.entries.empty?
314
+ # Process complex object.
315
+ hash = {}
316
+ hash[:xsi_type] = object.class.name.split('::').last
317
+ object_class.elements.entries.each do |entry|
318
+ property = entry.varname.to_s
319
+ if object.respond_to? property and !property.include?('_Type')
320
+ value = object.send(property)
321
+ property_name = nil
322
+ if @api.config.read('service.use_ruby_names', true)
323
+ property_name = underscore(property).to_sym
324
+ else
325
+ property_name = property.to_sym
326
+ end
327
+ # Recurse.
328
+ hash[property_name] = convert_from_object(value) if value
329
+ end
330
+ end
331
+ return hash
332
+ else
333
+ # Process simple object.
334
+ parent = object.class.superclass
335
+ return parent.new(object)
336
+ end
337
+ elsif object.is_a? Array
338
+ # Handle arrays
339
+ return object.map do |entry|
340
+ # Recurse.
341
+ convert_from_object(entry)
342
+ end
343
+ else
344
+ # Handle native objects
345
+ return object
346
+ end
347
+ end
348
+
349
+
350
+ public
351
+
352
+ EOS
353
+
354
+ # Add service methods
355
+ methods = driver_class::Methods
356
+ module_name = api_config.module_name(version, service)
357
+ methods.each do |method|
358
+ name = method[1]
359
+ doc_link = doc_link(version, service, name)
360
+ method_def = <<-EOS
361
+ # Calls the {#{name}}[#{doc_link}] method of the #{service} service.
362
+ # Check {the online documentation for this method}[#{doc_link}].
363
+ EOS
364
+
365
+ begin
366
+ method_class = eval("#{module_name}::#{fix_case_up(name)}")
367
+ arguments =
368
+ registry.schema_definition_from_class(method_class).elements
369
+ rescue
370
+ method_class = nil
371
+ arguments = nil
372
+ end
373
+
374
+ if arguments and arguments.size > 0
375
+ method_def += <<-EOS
376
+ #
377
+ # Args:
378
+ EOS
379
+ end
380
+
381
+ if arguments
382
+ # Add list of arguments to the RDoc comment
383
+ arguments.each_with_index do |elem, index|
384
+ if type(elem) == ARRAY_CLASSNAME
385
+ method_def += <<-EOS
386
+ # - #{elem.varname}: #{type(elem)} of #{elem.mapped_class}
387
+ EOS
388
+ else
389
+ method_def += <<-EOS
390
+ # - #{elem.varname}: #{type(elem)}
391
+ EOS
392
+ end
393
+ end
394
+ end
395
+
396
+ begin
397
+ response_class =
398
+ eval("#{module_name}::#{fix_case_up(name)}Response")
399
+ returns =
400
+ registry.schema_definition_from_class(response_class).elements
401
+
402
+ if returns.size > 0
403
+ method_def += <<-EOS
404
+ #
405
+ # Returns:
406
+ EOS
407
+ end
408
+
409
+ # Add list of returns to the RDoc comment
410
+ returns.each_with_index do |elem, index|
411
+ if type(elem) == ARRAY_CLASSNAME
412
+ method_def += <<-EOS
413
+ # - #{elem.varname}: #{type(elem)} of #{elem.mapped_class}
414
+ EOS
415
+ else
416
+ method_def += <<-EOS
417
+ # - #{elem.varname}: #{type(elem)}
418
+ EOS
419
+ end
420
+ end
421
+ rescue
422
+ method_def += <<-EOS
423
+ #
424
+ # Returns:
425
+ EOS
426
+ end
427
+
428
+ arg_names = arguments ? arguments.map {|elem| elem.varname} : []
429
+ arg_list = arg_names.join(', ')
430
+
431
+ method_def += <<-EOS
432
+ #
433
+ # Raises:
434
+ # Error::ApiError (or a subclass thereof) if a SOAP fault occurs.
435
+ #
436
+ def #{name}(#{arg_list})
437
+ begin
438
+ arg_array = []
439
+ EOS
440
+
441
+ # Add validation for every argument
442
+ if arguments
443
+ arguments.each_with_index do |elem, index|
444
+ method_def += <<-EOS
445
+ validate_object(#{arg_names[index]}, #{type(elem)})
446
+ arg_array << convert_to_object(#{elem.varname}, #{method_class},
447
+ '#{elem.varname}')
448
+ EOS
449
+ end
450
+ end
451
+
452
+ method_def += <<-EOS
453
+ # Construct request object and make API call
454
+ EOS
455
+
456
+ if arguments
457
+ method_def += <<-EOS
458
+ obj = #{module_name}::#{fix_case_up(name)}.new(*arg_array)
459
+ reply = convert_from_object(@driver.#{name}(obj))
460
+ EOS
461
+ else
462
+ method_def += <<-EOS
463
+ reply = convert_from_object(@driver.#{name}())
464
+ EOS
465
+ end
466
+
467
+ method_def += <<-EOS
468
+ reply = reply[:rval] if reply.include?(:rval)
469
+ return reply
470
+ rescue SOAP::FaultError => fault
471
+ raise #{api_config.api_name}::Errors.create_api_exception(fault,
472
+ self)
473
+ end
474
+ end
475
+
476
+ EOS
477
+ class_def += method_def
478
+
479
+ if name != underscore(name)
480
+ class_def += <<-EOS
481
+ alias #{underscore(name)} #{name}\n
482
+
483
+ EOS
484
+ end
485
+ end
486
+
487
+ # Add extension methods, if any
488
+ extensions = extension_config.extensions[[version, service]]
489
+ unless extensions.nil?
490
+ extensions.each do |ext|
491
+ params = extension_config.methods[ext].join(', ')
492
+ arglist = 'self'
493
+ arglist += ", #{params}" if params != ''
494
+ method_def = <<-EOS
495
+ # <i>Extension method</i> -- Calls the
496
+ # #{api_config.api_name}::Extensions.#{ext} method with +self+ as the
497
+ # first parameter.
498
+ def #{ext}(#{params})
499
+ return #{api_config.api_name}::Extensions.#{ext}(#{arglist})
500
+ end
501
+
502
+ EOS
503
+ class_def += method_def
504
+ end
505
+ end
506
+
507
+ class_def += <<-EOS
508
+ end
509
+ end
510
+ end
511
+ end
512
+ EOS
513
+ return class_def
514
+ end
515
+
516
+ # Helper method to fix a method name from lowerCamelCase to CamelCase.
517
+ #
518
+ # Args:
519
+ # - name: the method name
520
+ #
521
+ # Returns:
522
+ # The fixed name.
523
+ #
524
+ def fix_case_up(name)
525
+ return name[0, 1].upcase + name[1..-1]
526
+ end
527
+
528
+ # Helper method to create a link to a method's entry in the API online
529
+ # docs.
530
+ #
531
+ # Args:
532
+ # - version: the API version (as an integer)
533
+ # - service: the service name (as a string)
534
+ # - method: the method name (as a string)
535
+ #
536
+ # Returns:
537
+ # The URL to the method's entry in the documentation (as a string).
538
+ # +nil+ if none.
539
+ #
540
+ def doc_link(version, service, method)
541
+ return nil
542
+ end
543
+
544
+ # Helper method to return the expected type for a parameter, given the
545
+ # SchemaElementDefinition.
546
+ #
547
+ # Args:
548
+ # - element: SOAP::Mapping::SchemaElementDefinition element for the
549
+ # parameter (taken from the schema definition of the class)
550
+ #
551
+ # Returns:
552
+ # The full name for the expected parameter type (as a String)
553
+ #
554
+ def type(element)
555
+ # Check if it's an array
556
+ if element.as_array?
557
+ return ARRAY_CLASSNAME
558
+ else
559
+ return element.mapped_class
560
+ end
561
+ end
562
+ end
563
+ end
564
+ end