google-ads-savon 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/BUILD +7 -0
- data/CONTRIBUTING +24 -0
- data/ChangeLog +3 -0
- data/LICENSE +21 -0
- data/README.md +17 -0
- data/Rakefile +5 -0
- data/google-ads-savon.gemspec +35 -0
- data/lib/ads_savon.rb +23 -0
- data/lib/ads_savon/client.rb +163 -0
- data/lib/ads_savon/config.rb +46 -0
- data/lib/ads_savon/core_ext/string.rb +30 -0
- data/lib/ads_savon/error.rb +6 -0
- data/lib/ads_savon/hooks/group.rb +68 -0
- data/lib/ads_savon/hooks/hook.rb +61 -0
- data/lib/ads_savon/http/error.rb +42 -0
- data/lib/ads_savon/log_message.rb +50 -0
- data/lib/ads_savon/logger.rb +39 -0
- data/lib/ads_savon/model.rb +102 -0
- data/lib/ads_savon/null_logger.rb +10 -0
- data/lib/ads_savon/soap.rb +21 -0
- data/lib/ads_savon/soap/fault.rb +72 -0
- data/lib/ads_savon/soap/invalid_response_error.rb +13 -0
- data/lib/ads_savon/soap/request.rb +86 -0
- data/lib/ads_savon/soap/request_builder.rb +205 -0
- data/lib/ads_savon/soap/response.rb +130 -0
- data/lib/ads_savon/soap/xml.rb +252 -0
- data/lib/ads_savon/version.rb +5 -0
- metadata +224 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
module GoogleAdsSavon
|
2
|
+
module Hooks
|
3
|
+
|
4
|
+
# = GoogleAdsSavon::Hooks::Hook
|
5
|
+
#
|
6
|
+
# A hook used somewhere in the system.
|
7
|
+
class Hook
|
8
|
+
|
9
|
+
HOOKS = [
|
10
|
+
|
11
|
+
# :soap_request
|
12
|
+
#
|
13
|
+
# Around filter wrapping the POST request executed to call a SOAP service.
|
14
|
+
# See: GoogleAdsSavon::SOAP::Request#response
|
15
|
+
#
|
16
|
+
# Arguments
|
17
|
+
#
|
18
|
+
# [callback] A block to execute the SOAP request
|
19
|
+
# [request] The current <tt>GoogleAdsSavon::SOAP::Request</tt>
|
20
|
+
#
|
21
|
+
# Examples
|
22
|
+
#
|
23
|
+
# Log the time before and after the SOAP call:
|
24
|
+
#
|
25
|
+
# GoogleAdsSavon.config.hooks.define(:my_hook, :soap_request) do |callback, request|
|
26
|
+
# Timer.log(:start, Time.now)
|
27
|
+
# response = callback.call
|
28
|
+
# Timer.log(:end, Time.now)
|
29
|
+
# response
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# Replace the SOAP call and return a custom response:
|
33
|
+
#
|
34
|
+
# GoogleAdsSavon.config.hooks.define(:mock_hook, :soap_request) do |_, request|
|
35
|
+
# HTTPI::Response.new(200, {}, "")
|
36
|
+
# end
|
37
|
+
:soap_request
|
38
|
+
|
39
|
+
]
|
40
|
+
|
41
|
+
# Expects an +id+, the name of the +hook+ to use and a +block+ to be called.
|
42
|
+
def initialize(id, hook, &block)
|
43
|
+
unless HOOKS.include?(hook)
|
44
|
+
raise ArgumentError, "No such hook: #{hook}. Expected one of: #{HOOKS.join(', ')}"
|
45
|
+
end
|
46
|
+
|
47
|
+
self.id = id
|
48
|
+
self.hook = hook
|
49
|
+
self.block = block
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_accessor :id, :hook, :block
|
53
|
+
|
54
|
+
# Calls the +block+ with the given +args+.
|
55
|
+
def call(*args)
|
56
|
+
block.call(*args)
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "ads_savon/error"
|
2
|
+
require "ads_savon/soap/xml"
|
3
|
+
|
4
|
+
module GoogleAdsSavon
|
5
|
+
module HTTP
|
6
|
+
|
7
|
+
# = GoogleAdsSavon::HTTP::Error
|
8
|
+
#
|
9
|
+
# Represents an HTTP error. Contains the original <tt>HTTPI::Response</tt>.
|
10
|
+
class Error < Error
|
11
|
+
|
12
|
+
# Expects an <tt>HTTPI::Response</tt>.
|
13
|
+
def initialize(http)
|
14
|
+
self.http = http
|
15
|
+
end
|
16
|
+
|
17
|
+
# Accessor for the <tt>HTTPI::Response</tt>.
|
18
|
+
attr_accessor :http
|
19
|
+
|
20
|
+
# Returns whether an HTTP error is present.
|
21
|
+
def present?
|
22
|
+
http.error?
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns the HTTP error message.
|
26
|
+
def to_s
|
27
|
+
return "" unless present?
|
28
|
+
|
29
|
+
@message ||= begin
|
30
|
+
message = "HTTP error (#{http.code})"
|
31
|
+
message << ": #{http.body}" unless http.body.empty?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the HTTP response as a Hash.
|
36
|
+
def to_hash
|
37
|
+
@hash = { :code => http.code, :headers => http.headers, :body => http.body }
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module GoogleAdsSavon
|
2
|
+
class LogMessage
|
3
|
+
|
4
|
+
def initialize(message, filters, options = {})
|
5
|
+
@message = message
|
6
|
+
@filters = filters
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
return @message unless pretty? || filter?
|
12
|
+
|
13
|
+
doc = Nokogiri.XML(@message)
|
14
|
+
doc = apply_filter(doc) if filter?
|
15
|
+
doc.to_xml(pretty_options)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def filter?
|
21
|
+
@options[:filter] && @filters.any?
|
22
|
+
end
|
23
|
+
|
24
|
+
def pretty?
|
25
|
+
@options[:pretty]
|
26
|
+
end
|
27
|
+
|
28
|
+
def apply_filter(doc)
|
29
|
+
return doc unless doc.errors.empty?
|
30
|
+
|
31
|
+
@filters.each do |filter|
|
32
|
+
apply_filter!(doc, filter)
|
33
|
+
end
|
34
|
+
|
35
|
+
doc
|
36
|
+
end
|
37
|
+
|
38
|
+
def apply_filter!(doc, filter)
|
39
|
+
doc.xpath("//*[local-name()='#{filter}']").each do |node|
|
40
|
+
node.content = "***FILTERED***"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def pretty_options
|
45
|
+
return {} unless pretty?
|
46
|
+
{ :indent => 2 }
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "nokogiri"
|
3
|
+
require "ads_savon/log_message"
|
4
|
+
|
5
|
+
module GoogleAdsSavon
|
6
|
+
class Logger
|
7
|
+
|
8
|
+
def initialize(device = $stdout)
|
9
|
+
self.device = device
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :device
|
13
|
+
|
14
|
+
def log(message, options = {})
|
15
|
+
log_raw LogMessage.new(message, filter, options).to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_writer :subject, :level, :filter
|
19
|
+
|
20
|
+
def subject
|
21
|
+
@subject ||= ::Logger.new(device)
|
22
|
+
end
|
23
|
+
|
24
|
+
def level
|
25
|
+
@level ||= :debug
|
26
|
+
end
|
27
|
+
|
28
|
+
def filter
|
29
|
+
@filter ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def log_raw(message)
|
35
|
+
subject.send(level, message)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module GoogleAdsSavon
|
2
|
+
|
3
|
+
# = GoogleAdsSavon::Model
|
4
|
+
#
|
5
|
+
# Model for SOAP service oriented applications.
|
6
|
+
module Model
|
7
|
+
|
8
|
+
def self.extended(base)
|
9
|
+
base.setup
|
10
|
+
end
|
11
|
+
|
12
|
+
def setup
|
13
|
+
class_action_module
|
14
|
+
instance_action_module
|
15
|
+
end
|
16
|
+
|
17
|
+
# Accepts one or more SOAP actions and generates both class and instance methods named
|
18
|
+
# after the given actions. Each generated method accepts an optional SOAP body Hash and
|
19
|
+
# a block to be passed to <tt>GoogleAdsSavon::Client#request</tt> and executes a SOAP request.
|
20
|
+
def actions(*actions)
|
21
|
+
actions.each do |action|
|
22
|
+
define_class_action(action)
|
23
|
+
define_instance_action(action)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Defines a class-level SOAP action method.
|
30
|
+
def define_class_action(action)
|
31
|
+
class_action_module.module_eval %{
|
32
|
+
def #{action.to_s.snakecase}(body = nil, &block)
|
33
|
+
client.request :wsdl, #{action.inspect}, :body => body, &block
|
34
|
+
end
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
# Defines an instance-level SOAP action method.
|
39
|
+
def define_instance_action(action)
|
40
|
+
instance_action_module.module_eval %{
|
41
|
+
def #{action.to_s.snakecase}(body = nil, &block)
|
42
|
+
self.class.#{action.to_s.snakecase} body, &block
|
43
|
+
end
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Class methods.
|
48
|
+
def class_action_module
|
49
|
+
@class_action_module ||= Module.new do
|
50
|
+
|
51
|
+
# Returns the memoized <tt>GoogleAdsSavon::Client</tt>.
|
52
|
+
def client(&block)
|
53
|
+
@client ||= GoogleAdsSavon::Client.new(&block)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Sets the SOAP endpoint to the given +uri+.
|
57
|
+
def endpoint(uri)
|
58
|
+
client.wsdl.endpoint = uri
|
59
|
+
end
|
60
|
+
|
61
|
+
# Sets the target namespace.
|
62
|
+
def namespace(uri)
|
63
|
+
client.wsdl.namespace = uri
|
64
|
+
end
|
65
|
+
|
66
|
+
# Sets the WSDL document to the given +uri+.
|
67
|
+
def document(uri)
|
68
|
+
client.wsdl.document = uri
|
69
|
+
end
|
70
|
+
|
71
|
+
# Sets the HTTP headers.
|
72
|
+
def headers(headers)
|
73
|
+
client.http.headers = headers
|
74
|
+
end
|
75
|
+
|
76
|
+
# Sets basic auth +login+ and +password+.
|
77
|
+
def basic_auth(login, password)
|
78
|
+
client.http.auth.basic(login, password)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Sets WSSE auth credentials.
|
82
|
+
def wsse_auth(*args)
|
83
|
+
client.wsse.credentials(*args)
|
84
|
+
end
|
85
|
+
|
86
|
+
end.tap { |mod| extend(mod) }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Instance methods.
|
90
|
+
def instance_action_module
|
91
|
+
@instance_action_module ||= Module.new do
|
92
|
+
|
93
|
+
# Returns the <tt>GoogleAdsSavon::Client</tt> from the class instance.
|
94
|
+
def client(&block)
|
95
|
+
self.class.client(&block)
|
96
|
+
end
|
97
|
+
|
98
|
+
end.tap { |mod| include(mod) }
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module GoogleAdsSavon
|
2
|
+
|
3
|
+
# = GoogleAdsSavon::SOAP
|
4
|
+
#
|
5
|
+
# Contains various SOAP details.
|
6
|
+
module SOAP
|
7
|
+
|
8
|
+
# Default SOAP version.
|
9
|
+
DEFAULT_VERSION = 1
|
10
|
+
|
11
|
+
# Supported SOAP versions.
|
12
|
+
VERSIONS = 1..2
|
13
|
+
|
14
|
+
# SOAP namespaces by SOAP version.
|
15
|
+
NAMESPACE = {
|
16
|
+
1 => "http://schemas.xmlsoap.org/soap/envelope/",
|
17
|
+
2 => "http://www.w3.org/2003/05/soap-envelope"
|
18
|
+
}
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "ads_savon/error"
|
2
|
+
require "ads_savon/soap/xml"
|
3
|
+
|
4
|
+
module GoogleAdsSavon
|
5
|
+
module SOAP
|
6
|
+
|
7
|
+
# = GoogleAdsSavon::SOAP::Fault
|
8
|
+
#
|
9
|
+
# Represents a SOAP fault. Contains the original <tt>HTTPI::Response</tt>.
|
10
|
+
class Fault < Error
|
11
|
+
|
12
|
+
# Expects an <tt>HTTPI::Response</tt>.
|
13
|
+
def initialize(http)
|
14
|
+
self.http = http
|
15
|
+
end
|
16
|
+
|
17
|
+
# Accessor for the <tt>HTTPI::Response</tt>.
|
18
|
+
attr_accessor :http
|
19
|
+
|
20
|
+
# Returns whether a SOAP fault is present.
|
21
|
+
def present?
|
22
|
+
@present ||= http.body.include?("Fault>") && (soap1_fault? || soap2_fault?)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns the SOAP fault message.
|
26
|
+
def to_s
|
27
|
+
return "" unless present?
|
28
|
+
@message ||= message_by_version to_hash[:fault]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the SOAP response body as a Hash.
|
32
|
+
def to_hash
|
33
|
+
@hash ||= nori.parse(http.body)[:envelope][:body]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Returns whether the response contains a SOAP 1.1 fault.
|
39
|
+
def soap1_fault?
|
40
|
+
http.body.include?("faultcode>") && http.body.include?("faultstring>")
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns whether the response contains a SOAP 1.2 fault.
|
44
|
+
def soap2_fault?
|
45
|
+
http.body.include?("Code>") && http.body.include?("Reason>")
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the SOAP fault message by version.
|
49
|
+
def message_by_version(fault)
|
50
|
+
if fault[:faultcode]
|
51
|
+
"(#{fault[:faultcode]}) #{fault[:faultstring]}"
|
52
|
+
elsif fault[:code]
|
53
|
+
"(#{fault[:code][:value]}) #{fault[:reason][:text]}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def nori
|
58
|
+
return @nori if @nori
|
59
|
+
|
60
|
+
nori_options = {
|
61
|
+
:strip_namespaces => true,
|
62
|
+
:convert_tags_to => lambda { |tag| tag.snakecase.to_sym },
|
63
|
+
:advanced_typecasting => false
|
64
|
+
}
|
65
|
+
|
66
|
+
non_nil_nori_options = nori_options.reject { |_, value| value.nil? }
|
67
|
+
@nori = Nori.new(non_nil_nori_options)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require "httpi"
|
2
|
+
require "ads_savon/soap/response"
|
3
|
+
|
4
|
+
module GoogleAdsSavon
|
5
|
+
module SOAP
|
6
|
+
|
7
|
+
# = GoogleAdsSavon::SOAP::Request
|
8
|
+
#
|
9
|
+
# Executes SOAP requests.
|
10
|
+
class Request
|
11
|
+
|
12
|
+
# Content-Types by SOAP version.
|
13
|
+
CONTENT_TYPE = { 1 => "text/xml;charset=UTF-8", 2 => "application/soap+xml;charset=UTF-8" }
|
14
|
+
|
15
|
+
# Expects an <tt>HTTPI::Request</tt> and a <tt>GoogleAdsSavon::SOAP::XML</tt> object
|
16
|
+
# to execute a SOAP request and returns the response.
|
17
|
+
def self.execute(config, http, soap)
|
18
|
+
new(config, http, soap).response
|
19
|
+
end
|
20
|
+
|
21
|
+
# Expects an <tt>HTTPI::Request</tt>, a <tt>GoogleAdsSavon::SOAP::XML</tt> object
|
22
|
+
# and a <tt>GoogleAdsSavon::Config</tt>.
|
23
|
+
def initialize(config, http, soap)
|
24
|
+
self.config = config
|
25
|
+
self.soap = soap
|
26
|
+
self.http = configure(http)
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_accessor :soap, :http, :config
|
30
|
+
|
31
|
+
# Executes the request and returns the response.
|
32
|
+
def response
|
33
|
+
@response ||= begin
|
34
|
+
response = config.hooks.fire(:soap_request, self) { with_logging { HTTPI.post(http) } }
|
35
|
+
SOAP::Response.new(config, response)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Configures a given +http+ from the +soap+ object.
|
42
|
+
def configure(http)
|
43
|
+
http.url = soap.endpoint
|
44
|
+
|
45
|
+
if soap.signature?
|
46
|
+
# First generate the document so that Signature can digest sections
|
47
|
+
soap.wsse.signature.document = soap.to_xml(true)
|
48
|
+
|
49
|
+
# Then re-generate the document so that Signature can sign the digest
|
50
|
+
soap.wsse.signature.document = soap.to_xml(true)
|
51
|
+
|
52
|
+
# The third time we generate the document, we should have a signature
|
53
|
+
http.body = soap.to_xml(true)
|
54
|
+
else
|
55
|
+
http.body = soap.to_xml
|
56
|
+
end
|
57
|
+
|
58
|
+
http.headers["Content-Type"] = CONTENT_TYPE[soap.version]
|
59
|
+
http.headers["Content-Length"] = soap.to_xml.bytesize.to_s
|
60
|
+
http
|
61
|
+
end
|
62
|
+
|
63
|
+
# Logs the HTTP request, yields to a given +block+ and returns a <tt>GoogleAdsSavon::SOAP::Response</tt>.
|
64
|
+
def with_logging
|
65
|
+
log_request http.url, http.headers, http.body
|
66
|
+
response = yield
|
67
|
+
log_response response.code, response.body
|
68
|
+
response
|
69
|
+
end
|
70
|
+
|
71
|
+
# Logs the SOAP request +url+, +headers+ and +body+.
|
72
|
+
def log_request(url, headers, body)
|
73
|
+
config.logger.log "SOAP request: #{url}"
|
74
|
+
config.logger.log headers.map { |key, value| "#{key}: #{value}" }.join(", ")
|
75
|
+
config.logger.log body, :pretty => config.pretty_print_xml, :filter => true
|
76
|
+
end
|
77
|
+
|
78
|
+
# Logs the SOAP response +code+ and +body+.
|
79
|
+
def log_response(code, body)
|
80
|
+
config.logger.log "SOAP response (status #{code}):"
|
81
|
+
config.logger.log body, :pretty => config.pretty_print_xml
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|