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,205 @@
1
+ module GoogleAdsSavon
2
+ module SOAP
3
+
4
+ # = GoogleAdsSavon::SOAP::RequestBuilder
5
+ #
6
+ # GoogleAdsSavon::SOAP::RequestBuilder builds GoogleAdsSavon::SOAP::Request instances.
7
+ # The RequestBuilder is configured by the client that instantiates it.
8
+ # It uses the options set by the client to build an appropriate request.
9
+ class RequestBuilder
10
+
11
+ # Initialize a new +RequestBuilder+ with the given SOAP operation.
12
+ # The operation may be specified using a symbol or a string.
13
+ def initialize(operation, options = {})
14
+ @operation = operation
15
+ assign_options(options)
16
+ end
17
+
18
+ # Writer for the <tt>HTTPI::Request</tt> object.
19
+ attr_writer :http
20
+
21
+ # Writer for the <tt>GoogleAdsSavon::SOAP::XML</tt> object.
22
+ attr_writer :soap
23
+
24
+ # Writer for the <tt>Akami::WSSE</tt> object.
25
+ attr_writer :wsse
26
+
27
+ # Writer for the <tt>Wasabi::Document</tt> object.
28
+ attr_writer :wsdl
29
+
30
+ # Writer for the <tt>GoogleAdsSavon::Config</tt> object.
31
+ attr_writer :config
32
+
33
+ # Writer for the attributes of the SOAP input tag. Accepts a Hash.
34
+ attr_writer :attributes
35
+
36
+ # Writer for the namespace identifer of the <tt>GoogleAdsSavon::SOAP::XML</tt>
37
+ # object.
38
+ attr_writer :namespace_identifier
39
+
40
+ # Writer for the SOAP action of the <tt>GoogleAdsSavon::SOAP::XML</tt> object.
41
+ attr_writer :soap_action
42
+
43
+ # Reader for the operation of the request being built by the request builder.
44
+ attr_reader :operation
45
+
46
+ # Builds and returns a <tt>GoogleAdsSavon::SOAP::Request</tt> object. You may optionally
47
+ # pass a block to the method that will be run after the initial configuration of
48
+ # the dependencies. +self+ will be yielded to the block if the block accepts an
49
+ # argument.
50
+ def request(&post_configuration_block)
51
+ configure_dependencies
52
+
53
+ if post_configuration_block
54
+ # Only yield self to the block if our block takes an argument
55
+ args = [] and (args << self if post_configuration_block.arity == 1)
56
+ post_configuration_block.call(*args)
57
+ end
58
+
59
+ Request.new(config, http, soap)
60
+ end
61
+
62
+ # Returns the identifier for the default namespace. If an operation namespace
63
+ # identifier is defined for the current operation in the WSDL document, this
64
+ # namespace identifier is used. Otherwise, the +@namespace_identifier+ instance
65
+ # variable is used.
66
+ def namespace_identifier
67
+ if operation_namespace_defined_in_wsdl?
68
+ wsdl.operations[operation][:namespace_identifier].to_sym
69
+ else
70
+ @namespace_identifier
71
+ end
72
+ end
73
+
74
+ # Returns the namespace identifier to be used for the the SOAP input tag.
75
+ # If +@namespace_identifier+ is not +nil+, it will be returned. Otherwise, the
76
+ # default namespace identifier as returned by +namespace_identifier+ will be
77
+ # returned.
78
+ def input_namespace_identifier
79
+ @namespace_identifier || namespace_identifier
80
+ end
81
+
82
+ # Returns the default namespace to be used for the SOAP request. If a namespace
83
+ # is defined for the operation in the WSDL document, this namespace will be
84
+ # returned. Otherwise, the default WSDL document namespace will be returned.
85
+ def namespace
86
+ if operation_namespace_defined_in_wsdl?
87
+ wsdl.parser.namespaces[namespace_identifier.to_s]
88
+ else
89
+ wsdl.namespace
90
+ end
91
+ end
92
+
93
+ # Returns true if the operation's namespace is defined within the WSDL
94
+ # document.
95
+ def operation_namespace_defined_in_wsdl?
96
+ return false unless wsdl.document?
97
+ (operation = wsdl.operations[self.operation]) && operation[:namespace_identifier]
98
+ end
99
+
100
+ # Returns the SOAP action. If +@soap_action+ has been defined, this will
101
+ # be returned. Otherwise, if there is a WSDL document defined, the SOAP
102
+ # action corresponding to the operation will be returned. Failing this,
103
+ # the operation name will be used to form the SOAP action.
104
+ def soap_action
105
+ return @soap_action if @soap_action
106
+
107
+ if wsdl.document?
108
+ wsdl.soap_action(operation.to_sym)
109
+ else
110
+ Gyoku::XMLKey.create(operation).to_sym
111
+ end
112
+ end
113
+
114
+ # Returns the SOAP operation input tag. If there is a WSDL document defined,
115
+ # and the operation's input tag is defined in the document, this will be
116
+ # returned. Otherwise, the operation name will be used to form the input tag.
117
+ def soap_input_tag
118
+ if wsdl.document? && (input = wsdl.soap_input(operation.to_sym))
119
+ input
120
+ else
121
+ Gyoku::XMLKey.create(operation)
122
+ end
123
+ end
124
+
125
+ # Changes the body of the SOAP request to +body+.
126
+ def body=(body)
127
+ soap.body = body
128
+ end
129
+
130
+ # Returns the body of the SOAP request.
131
+ def body
132
+ soap.body
133
+ end
134
+
135
+ # Returns the attributes of the SOAP input tag. Defaults to
136
+ # an empty Hash.
137
+ def attributes
138
+ @attributes ||= {}
139
+ end
140
+
141
+ # Returns the <tt>GoogleAdsSavon::Config</tt> object for the request. Defaults
142
+ # to a clone of <tt>GoogleAdsSavon.config</tt>.
143
+ def config
144
+ @config ||= GoogleAdsSavon.config.clone
145
+ end
146
+
147
+ # Returns the <tt>HTTPI::Request</tt> object.
148
+ def http
149
+ @http ||= HTTPI::Request.new
150
+ end
151
+
152
+ # Returns the <tt>SOAP::XML</tt> object.
153
+ def soap
154
+ @soap ||= XML.new(config)
155
+ end
156
+
157
+ # Returns the <tt>Wasabi::Document</tt> object.
158
+ def wsdl
159
+ @wsdl ||= Wasabi::Document.new
160
+ end
161
+
162
+ # Returns the <tt>Akami::WSSE</tt> object.
163
+ def wsse
164
+ @wsse ||= Akami.wsse
165
+ end
166
+
167
+ private
168
+
169
+ def configure_dependencies
170
+ soap.endpoint = wsdl.endpoint
171
+ soap.element_form_default = wsdl.element_form_default
172
+ soap.wsse = wsse
173
+
174
+ soap.namespace = namespace
175
+ soap.namespace_identifier = namespace_identifier
176
+
177
+ add_wsdl_namespaces_to_soap
178
+ add_wsdl_types_to_soap
179
+
180
+ soap.input = [input_namespace_identifier, soap_input_tag.to_sym, attributes]
181
+
182
+ http.headers["SOAPAction"] = %{"#{soap_action}"}
183
+ end
184
+
185
+ def add_wsdl_namespaces_to_soap
186
+ wsdl.type_namespaces.each do |path, uri|
187
+ soap.use_namespace(path, uri)
188
+ end
189
+ end
190
+
191
+ def add_wsdl_types_to_soap
192
+ wsdl.type_definitions.each do |path, type|
193
+ soap.types[path] = type
194
+ end
195
+ end
196
+
197
+ def assign_options(options)
198
+ options.each do |option, value|
199
+ send(:"#{option}=", value) if value
200
+ end
201
+ end
202
+
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,130 @@
1
+ require "ads_savon/soap/xml"
2
+ require "ads_savon/soap/fault"
3
+ require "ads_savon/soap/invalid_response_error"
4
+ require "ads_savon/http/error"
5
+
6
+ module GoogleAdsSavon
7
+ module SOAP
8
+
9
+ # = GoogleAdsSavon::SOAP::Response
10
+ #
11
+ # Represents the SOAP response and contains the HTTP response.
12
+ class Response
13
+
14
+ # Expects an <tt>HTTPI::Response</tt> and handles errors.
15
+ def initialize(config, response)
16
+ self.config = config
17
+ self.http = response
18
+ raise_errors if config.raise_errors
19
+ end
20
+
21
+ attr_accessor :http, :config
22
+
23
+ # Returns whether the request was successful.
24
+ def success?
25
+ !soap_fault? && !http_error?
26
+ end
27
+
28
+ # Returns whether there was a SOAP fault.
29
+ def soap_fault?
30
+ soap_fault.present?
31
+ end
32
+
33
+ # Returns the <tt>GoogleAdsSavon::SOAP::Fault</tt>.
34
+ def soap_fault
35
+ @soap_fault ||= Fault.new http
36
+ end
37
+
38
+ # Returns whether there was an HTTP error.
39
+ def http_error?
40
+ http_error.present?
41
+ end
42
+
43
+ # Returns the <tt>GoogleAdsSavon::HTTP::Error</tt>.
44
+ def http_error
45
+ @http_error ||= HTTP::Error.new http
46
+ end
47
+
48
+ # Shortcut accessor for the SOAP response body Hash.
49
+ def [](key)
50
+ body[key]
51
+ end
52
+
53
+ # Returns the SOAP response header as a Hash.
54
+ def header
55
+ if !hash.has_key? :envelope
56
+ raise GoogleAdsSavon::SOAP::InvalidResponseError, "Unable to parse response body '#{to_xml}'"
57
+ end
58
+ hash[:envelope][:header]
59
+ end
60
+
61
+ # Returns the SOAP response body as a Hash.
62
+ def body
63
+ if !hash.has_key? :envelope
64
+ raise GoogleAdsSavon::SOAP::InvalidResponseError, "Unable to parse response body '#{to_xml}'"
65
+ end
66
+ hash[:envelope][:body]
67
+ end
68
+
69
+ alias to_hash body
70
+
71
+ # Traverses the SOAP response body Hash for a given +path+ of Hash keys and returns
72
+ # the value as an Array. Defaults to return an empty Array in case the path does not
73
+ # exist or returns nil.
74
+ def to_array(*path)
75
+ result = path.inject body do |memo, key|
76
+ return [] unless memo[key]
77
+ memo[key]
78
+ end
79
+
80
+ result.kind_of?(Array) ? result.compact : [result].compact
81
+ end
82
+
83
+ # Returns the complete SOAP response XML without normalization.
84
+ def hash
85
+ @hash ||= nori.parse(to_xml)
86
+ end
87
+
88
+ # Returns the SOAP response XML.
89
+ def to_xml
90
+ http.body
91
+ end
92
+
93
+ # Returns a <tt>Nokogiri::XML::Document</tt> for the SOAP response XML.
94
+ def doc
95
+ @doc ||= Nokogiri::XML(to_xml)
96
+ end
97
+
98
+ # Returns an Array of <tt>Nokogiri::XML::Node</tt> objects retrieved with the given +path+.
99
+ # Automatically adds all of the document's namespaces unless a +namespaces+ hash is provided.
100
+ def xpath(path, namespaces = nil)
101
+ doc.xpath(path, namespaces || xml_namespaces)
102
+ end
103
+
104
+ private
105
+
106
+ def raise_errors
107
+ raise soap_fault if soap_fault?
108
+ raise http_error if http_error?
109
+ end
110
+
111
+ def xml_namespaces
112
+ @xml_namespaces ||= doc.collect_namespaces
113
+ end
114
+
115
+ def nori
116
+ return @nori if @nori
117
+
118
+ nori_options = {
119
+ :strip_namespaces => true,
120
+ :convert_tags_to => lambda { |tag| tag.snakecase.to_sym },
121
+ :advanced_typecasting => false
122
+ }
123
+
124
+ non_nil_nori_options = nori_options.reject { |_, value| value.nil? }
125
+ @nori = Nori.new(non_nil_nori_options)
126
+ end
127
+
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,252 @@
1
+ require "builder"
2
+ require "gyoku"
3
+ require "rexml/document"
4
+ require "nori"
5
+
6
+ require "ads_savon/soap"
7
+
8
+ module GoogleAdsSavon
9
+ module SOAP
10
+
11
+ # = GoogleAdsSavon::SOAP::XML
12
+ #
13
+ # Represents the SOAP request XML. Contains various global and per request/instance settings
14
+ # like the SOAP version, header, body and namespaces.
15
+ class XML
16
+
17
+ # XML Schema Type namespaces.
18
+ SCHEMA_TYPES = {
19
+ "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
20
+ "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance"
21
+ }
22
+
23
+ # Expects a +config+ object.
24
+ def initialize(config)
25
+ self.config = config
26
+ end
27
+
28
+ attr_accessor :config
29
+
30
+ # Accessor for the SOAP +input+ tag.
31
+ attr_accessor :input
32
+
33
+ # Accessor for the SOAP +endpoint+.
34
+ attr_accessor :endpoint
35
+
36
+ # Sets the SOAP +version+.
37
+ def version=(version)
38
+ raise ArgumentError, "Invalid SOAP version: #{version}" unless SOAP::VERSIONS.include? version
39
+ @version = version
40
+ end
41
+
42
+ # Returns the SOAP +version+. Defaults to <tt>GoogleAdsSavon.config.soap_version</tt>.
43
+ def version
44
+ @version ||= config.soap_version
45
+ end
46
+
47
+ # Sets the SOAP +header+ Hash.
48
+ attr_writer :header
49
+
50
+ # Returns the SOAP +header+. Defaults to an empty Hash.
51
+ def header
52
+ @header ||= config.soap_header.nil? ? {} : config.soap_header
53
+ end
54
+
55
+ # Sets the SOAP envelope namespace.
56
+ attr_writer :env_namespace
57
+
58
+ # Returns the SOAP envelope namespace. Uses the global namespace if set Defaults to :env.
59
+ def env_namespace
60
+ @env_namespace ||= config.env_namespace.nil? ? :env : config.env_namespace
61
+ end
62
+
63
+ # Sets the +namespaces+ Hash.
64
+ attr_writer :namespaces
65
+
66
+ # Returns the +namespaces+. Defaults to a Hash containing the SOAP envelope namespace.
67
+ def namespaces
68
+ @namespaces ||= begin
69
+ key = ["xmlns"]
70
+ key << env_namespace if env_namespace && env_namespace != ""
71
+ { key.join(":") => SOAP::NAMESPACE[version] }
72
+ end
73
+ end
74
+
75
+ def namespace_by_uri(uri)
76
+ namespaces.each do |candidate_identifier, candidate_uri|
77
+ return candidate_identifier.gsub(/^xmlns:/, '') if candidate_uri == uri
78
+ end
79
+ nil
80
+ end
81
+
82
+ def used_namespaces
83
+ @used_namespaces ||= {}
84
+ end
85
+
86
+ def use_namespace(path, uri)
87
+ @internal_namespace_count ||= 0
88
+
89
+ unless identifier = namespace_by_uri(uri)
90
+ identifier = "ins#{@internal_namespace_count}"
91
+ namespaces["xmlns:#{identifier}"] = uri
92
+ @internal_namespace_count += 1
93
+ end
94
+
95
+ used_namespaces[path] = identifier
96
+ end
97
+
98
+ def types
99
+ @types ||= {}
100
+ end
101
+
102
+ # Sets the default namespace identifier.
103
+ attr_writer :namespace_identifier
104
+
105
+ # Returns the default namespace identifier.
106
+ def namespace_identifier
107
+ @namespace_identifier ||= :wsdl
108
+ end
109
+
110
+ # Accessor for whether all local elements should be namespaced.
111
+ attr_accessor :element_form_default
112
+
113
+ # Accessor for the default namespace URI.
114
+ attr_accessor :namespace
115
+
116
+ # Accessor for the <tt>GoogleAdsSavon::WSSE</tt> object.
117
+ attr_accessor :wsse
118
+
119
+ def signature?
120
+ wsse.respond_to?(:signature?) && wsse.signature?
121
+ end
122
+
123
+ # Returns the SOAP request encoding. Defaults to "UTF-8".
124
+ def encoding
125
+ @encoding ||= "UTF-8"
126
+ end
127
+
128
+ # Sets the SOAP request encoding.
129
+ attr_writer :encoding
130
+
131
+ # Accepts a +block+ and yields a <tt>Builder::XmlMarkup</tt> object to let you create
132
+ # custom body XML.
133
+ def body
134
+ @body = yield builder(nil) if block_given?
135
+ @body
136
+ end
137
+
138
+ # Sets the SOAP +body+. Expected to be a Hash that can be translated to XML via `Gyoku.xml`
139
+ # or any other Object responding to to_s.
140
+ attr_writer :body
141
+
142
+ # Accepts a +block+ and yields a <tt>Builder::XmlMarkup</tt> object to let you create
143
+ # a completely custom XML.
144
+ def xml(directive_tag = :xml, attrs = {})
145
+ @xml = yield builder(directive_tag, attrs) if block_given?
146
+ end
147
+
148
+ # Accepts an XML String and lets you specify a completely custom request body.
149
+ attr_writer :xml
150
+
151
+ # Returns the XML for a SOAP request.
152
+ def to_xml(clear_cache = false)
153
+ if clear_cache
154
+ @xml = nil
155
+ @header_for_xml = nil
156
+ end
157
+
158
+ @xml ||= tag(builder, :Envelope, complete_namespaces) do |xml|
159
+ tag(xml, :Header) { xml << header_for_xml } unless header_for_xml.empty?
160
+
161
+ # TODO: Maybe there should be some sort of plugin architecture where
162
+ # classes like WSSE::Signature can hook into this process.
163
+ body_attributes = (signature? ? wsse.signature.body_attributes : {})
164
+
165
+ if input.nil?
166
+ tag(xml, :Body, body_attributes)
167
+ else
168
+ tag(xml, :Body, body_attributes) { xml.tag!(*add_namespace_to_input) { xml << body_to_xml } }
169
+ end
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ # Returns a new <tt>Builder::XmlMarkup</tt> object.
176
+ def builder(directive_tag = :xml, attrs = { :encoding => encoding })
177
+ builder = Builder::XmlMarkup.new
178
+ builder.instruct!(directive_tag, attrs) if directive_tag
179
+ builder
180
+ end
181
+
182
+ # Expects a builder +xml+ instance, a tag +name+ and accepts optional +namespaces+
183
+ # and a block to create an XML tag.
184
+ def tag(xml, name, namespaces = {}, &block)
185
+ if env_namespace && env_namespace != ""
186
+ xml.tag! env_namespace, name, namespaces, &block
187
+ else
188
+ xml.tag! name, namespaces, &block
189
+ end
190
+ end
191
+
192
+ # Returns the complete Hash of namespaces.
193
+ def complete_namespaces
194
+ defaults = SCHEMA_TYPES.dup
195
+ defaults["xmlns:#{namespace_identifier}"] = namespace if namespace
196
+ defaults.merge namespaces
197
+ end
198
+
199
+ # Returns the SOAP header as an XML String.
200
+ def header_for_xml
201
+ @header_for_xml ||= (Hash === header ? Gyoku.xml(header) : header) + wsse_header
202
+ end
203
+
204
+ # Returns the WSSE header or an empty String in case WSSE was not set.
205
+ def wsse_header
206
+ wsse.respond_to?(:to_xml) ? wsse.to_xml : ""
207
+ end
208
+
209
+ # Returns the SOAP body as an XML String.
210
+ def body_to_xml
211
+ return body.to_s unless body.kind_of? Hash
212
+ body_to_xml = element_form_default == :qualified ? add_namespaces_to_body(body) : body
213
+ Gyoku.xml body_to_xml, :element_form_default => element_form_default, :namespace => namespace_identifier
214
+ end
215
+
216
+ def add_namespaces_to_body(hash, path = [input[1].to_s])
217
+ return unless hash
218
+ return hash.map { |value| add_namespaces_to_body(value, path) } if hash.kind_of?(Array)
219
+ return hash.to_s unless hash.kind_of? Hash
220
+
221
+ hash.inject({}) do |newhash, (key, value)|
222
+ camelcased_key = Gyoku::XMLKey.create(key)
223
+ newpath = path + [camelcased_key]
224
+
225
+ if used_namespaces[newpath]
226
+ newhash.merge(
227
+ "#{used_namespaces[newpath]}:#{camelcased_key}" =>
228
+ add_namespaces_to_body(value, types[newpath] ? [types[newpath]] : newpath)
229
+ )
230
+ else
231
+ add_namespaces_to_values(value, path) if key == :order!
232
+ newhash.merge(key => value)
233
+ end
234
+ end
235
+ end
236
+
237
+ def add_namespace_to_input
238
+ return input.compact unless used_namespaces[[input[1].to_s]]
239
+ [used_namespaces[[input[1].to_s]], input[1], input[2]]
240
+ end
241
+
242
+ def add_namespaces_to_values(values, path)
243
+ values.collect! { |value|
244
+ camelcased_value = Gyoku::XMLKey.create(value)
245
+ namespace_path = path + [camelcased_value.to_s]
246
+ namespace = used_namespaces[namespace_path]
247
+ "#{namespace.blank? ? '' : namespace + ":"}#{camelcased_value}"
248
+ }
249
+ end
250
+ end
251
+ end
252
+ end