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.
@@ -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