google-ads-savon 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.
- 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
|