google-ads-savon 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,10 @@
1
+ require "ads_savon/logger"
2
+
3
+ module GoogleAdsSavon
4
+ class NullLogger < Logger
5
+
6
+ def log(*)
7
+ end
8
+
9
+ end
10
+ 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,13 @@
1
+ require "ads_savon/error"
2
+
3
+ module GoogleAdsSavon
4
+ module SOAP
5
+
6
+ # = GoogleAdsSavon::SOAP::InvalidResponseError
7
+ #
8
+ # Represents an error when the response was not a valid SOAP envelope.
9
+ class InvalidResponseError < Error
10
+ end
11
+
12
+ end
13
+ 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