digitaria-handsoap 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,227 @@
1
+ Handsoap
2
+ ===
3
+
4
+ What
5
+ ---
6
+ Handsoap is a library for creating SOAP clients in Ruby.
7
+
8
+ [Watch a tutorial](http://www.vimeo.com/4813848), showing how to use Handsoap. The final application can be found at: [http://github.com/troelskn/handsoap-example/tree/master](http://github.com/troelskn/handsoap-example/tree/master)
9
+
10
+ ![Handsoap](http://ny-image0.etsy.com/il_430xN.68558416.jpg)
11
+
12
+ Why
13
+ ---
14
+
15
+ Ruby already has a SOAP-client library, [soap4r](http://dev.ctor.org/soap4r), so why create another one?
16
+
17
+ > Let me summarize SOAP4R: it smells like Java code built on a Monday morning by an EJB coder.
18
+ >
19
+ > -- [Ruby In Practice: REST, SOAP, WebSphere MQ and SalesForce](http://blog.labnotes.org/2008/01/28/ruby-in-practice-rest-soap-websphere-mq-and-salesforce/)
20
+
21
+ OK, not entirely fair, but soap4r has problems. It's incomplete and buggy. If you try to use it for any real-world services, you quickly run into compatibility issues. You can get around some of them, if you have control over the service, but you may not always be that lucky. In the end, even if you get it working, it has a bulky un-Rubyish feel to it.
22
+
23
+ Handsoap tries to do better by taking a minimalistic approach. Instead of a full abstraction layer, it is more like a toolbox with which you can write SOAP bindings. You could think of it as a [ffi](http://c2.com/cgi/wiki?ForeignFunctionInterface) targeting SOAP.
24
+
25
+ This means that you generally need to do more manual labor in the cases where soap4r would have automated the mapping. It also means that you need to get your hands dirty with wsdl, xsd and other heavyweight specifications. However, it does give you some tools to help you stay sane.
26
+
27
+ There are several benefits of using Handsoap:
28
+
29
+ * It supports the entire SOAP specification, all versions (because you have to implement it your self).
30
+ * You actually get a sporting chance to debug and fix protocol level bugs.
31
+ * It's much faster than soap4r, because it uses fast low-level libraries for xml-parsing and http-communication.
32
+
33
+ To summarise, soap4r takes an optimistic approach, where Handsoap expects things to fail. If soap4r works for you today, it's probably the better choice. If you find your self strugling with it, Handsoap will offer a more smooth ride. It won't magically fix things for you though.
34
+
35
+ Handsoap vs. soap4r benchmark
36
+ ---
37
+
38
+ Benchmarks are always unfair, but my experiments has placed Handsoap at being approximately double as fast as soap4r. I'd love any suggestions for a more precise measure.
39
+
40
+ $ ruby tests/benchmark_test.rb 1000
41
+ Benchmarking 1000 calls ...
42
+ user system total real
43
+ handsoap 0.750000 0.090000 0.840000 ( 1.992437)
44
+ soap4r 2.240000 0.140000 2.380000 ( 3.605836)
45
+ ---------------
46
+ Legend:
47
+ The user CPU time, system CPU time, the sum of the user and system CPU times,
48
+ and the elapsed real time. The unit of time is seconds.
49
+
50
+ SOAP basics
51
+ ---
52
+
53
+ SOAP is a protocol that is tunneled through XML over HTTP. Apart from using the technology for transportation, it doesn't have much to do with HTTP. Some times, it hasn't even got much to do with XML either.
54
+
55
+ A SOAP client basically consists of three parts:
56
+
57
+ * A http-connectivity layer,
58
+ * a mechanism for marshalling native data types to XML,
59
+ * and a mechanism for unmarshalling XML to native data types.
60
+
61
+ The protocol also contains a large and unwieldy specification of how to do the (un)marshalling, which can be used as the basis for automatically mapping to a rich type model. This makes the protocol fitting for .net/Java, but is a huge overhead for a very dynamically typed language such as Ruby. Much of the complexity of clients such as soap4r, is in the parts that tries to use this specification. Handsoap expects you to manually write the code that marshals/unmarshals, thereby bypassing this complexity (or rather - pass it to the programmer)
62
+
63
+ Handsoap only supports RPC-style SOAP. This seems to be the most common style. It's probably possible to add support for Document-style with little effort, but until I see the need I'm not going there.
64
+
65
+ The toolbox
66
+ ---
67
+
68
+ The Handsoap toolbox consists of the following components.
69
+
70
+ Handsoap can use either [curb](http://curb.rubyforge.org/) or [httpclient](http://dev.ctor.org/http-access2) for HTTP-connectivity. The former is recommended, and default, but for portability you might choose the latter. You usually don't need to interact at the HTTP-level, but if you do (for example, if you have to use SSL), you can.
71
+
72
+ For parsing XML, Handsoap uses [Nokogiri](http://github.com/tenderlove/nokogiri/tree/master). While this may become optional in the future, the dependency is a bit tighter. The XML-parser is used internally in Handsoap, as well as by the code that maps from SOAP to Ruby (The code you're writing). Nokogiri is very fast (being based om libxml) and has a polished and stable api.
73
+
74
+ There is also a library for generating XML, which you'll use when mapping from Ruby to SOAP. It's quite similar to [Builder](http://builder.rubyforge.org/), but is tailored towards being used for writing SOAP-messages. The name of this library is `XmlMason` and it is included/part of Handsoap.
75
+
76
+ Recommendations
77
+ ---
78
+
79
+ ###Workflow
80
+
81
+ 1. Find the wsdl for the service you want to consume.
82
+
83
+ 2. Figure out the url for the endpoint, as well as the protocol version. Put this in a config file.
84
+ * To find the endpoint, look inside the wsdl, for `<soap:address location="..">`
85
+
86
+ 3. Create a service class. Add endpoints and protocol. Alias needed namespace(s).
87
+ * To find the namespace(s), look in the samples from soapUI. It will be imported as `v1`
88
+
89
+ 4. Open the wsdl in [soapUI](http://www.soapui.org/).
90
+
91
+ 5. In soapUI, find a sample request for the method you want to use. Copy+paste the body-part.
92
+
93
+ 6. Create a method in your service class (Use ruby naming convention)
94
+
95
+ 7. Write Ruby-code (using XmlMason) to generate a request that is similar to the example from soapUI. (In your copy+paste buffer)
96
+
97
+ 8. Write Ruby-code to parse the response (a Nokogiri XML-document) into Ruby data types.
98
+
99
+ 9. Write an integration test to verify that your method works as expected. You can use soapUI to [generate a mock-service](http://www.soapui.org/userguide/mock/getting_started.html).
100
+
101
+ Repeat point 5..9 for each method that you need to use.
102
+ Between each iteration, you should refactor shared code into helper functions.
103
+
104
+ ###Configuration
105
+
106
+ If you use Rails, you should put the endpoint in a constant in the environment file. That way, you can have different endpoints for test/development/production/etc.
107
+
108
+ If you don't use Rails, it's still a good idea to move this information to a config file.
109
+
110
+ The configuration could look like this:
111
+
112
+ # wsdl: http://example.org/ws/service?WSDL
113
+ EXAMPLE_SERVICE_ENDPOINT = {
114
+ :uri => 'http://example.org/ws/service',
115
+ :version => 2
116
+ }
117
+
118
+ If you use Rails, you will need to load the gem from the `config/environment.rb` file, using:
119
+
120
+ config.gem 'troelskn-handsoap', :lib => 'handsoap', :source => "http://gems.github.com"
121
+
122
+ ###Service class
123
+
124
+ Put your service in a file under `app/models`. You should extend `Handsoap::Service`.
125
+
126
+ You need to provide the endpoint and the SOAP version (1.1 or 1.2). If in doubt, use version 2.
127
+
128
+ A service usually has a namespace for describing the message-body ([RPC/Litteral style](http://www.ibm.com/developerworks/webservices/library/ws-whichwsdl/#N1011F)). You should set this in the `on_create_document` handler.
129
+
130
+ A typical service looks like the following:
131
+
132
+ # -*- coding: utf-8 -*-
133
+ require 'handsoap'
134
+
135
+ class Example::FooService < Handsoap::Service
136
+ endpoint EXAMPLE_SERVICE_ENDPOINT
137
+ on_create_document do |doc|
138
+ doc.alias 'wsdl', "http://example.org/ws/spec"
139
+ end
140
+ # public methods
141
+ # todo
142
+
143
+ private
144
+ # helpers
145
+ # todo
146
+ end
147
+
148
+ The above would go in the file `app/models/example/foo_service.rb`
149
+
150
+ ###Integration tests
151
+
152
+ Since you're writing mappings manually, it's a good idea to write tests that verify that the service works. If you use standard Rails with `Test::Unit`, you should put these in an integration-test.
153
+
154
+ For the sample service above, you would create a file in `test/integration/example/foo_service.rb`, with the following content:
155
+
156
+ # -*- coding: utf-8 -*-
157
+ require 'test_helper'
158
+
159
+ # Example::FooService.logger = $stdout
160
+
161
+ class Example::FooServiceTest < Test::Unit::TestCase
162
+ def test_update_icon
163
+ icon = { :href => 'http://www.example.com/icon.jpg', :type => 'image/jpeg' }
164
+ result = Example::FooService.update_icon!(icon)
165
+ assert_equal icon.type, result.type
166
+ end
167
+ end
168
+
169
+ Note the commented-out line. If you set a logger on the service-class, you can see exactly which XML goes forth and back, which is very useful for debugging.
170
+
171
+ ###Methods
172
+
173
+ You should use Ruby naming-conventions for methods names. If the method has side-effects, you should postfix it with an exclamation.
174
+ Repeat code inside the invoke-block, should be refactored out to *builders*, and the response should be parsed with a *parser*.
175
+
176
+ def update_icon!(icon)
177
+ response = invoke("wsdl:UpdateIcon") do |message|
178
+ build_icon!(message, icon)
179
+ end
180
+ parse_icon(response.document.xpath('//icon').first)
181
+ end
182
+
183
+
184
+ ###Helpers
185
+
186
+ You'll end up with two kinds of helpers; Ruby->XML transformers (aka. *builders*) and XML->Ruby transformers (aka. *parsers*).
187
+ It's recommended that you stick to the following style/naming scheme:
188
+
189
+ # icon -> xml
190
+ def build_icon!(message, icon)
191
+ message.add "icon" do |i|
192
+ i.set_attr "href", icon[:href]
193
+ i.set_attr "type", icon[:type]
194
+ end
195
+ end
196
+
197
+ # xml -> icon
198
+ def parse_icon(node)
199
+ { :href => node['href'], :type => node['type'] }
200
+ end
201
+
202
+ or, if you prefer, you can use a class to represent entities:
203
+
204
+ # icon -> xml
205
+ def build_icon!(message, icon)
206
+ message.add "icon" do |i|
207
+ i.set_attr "href", icon.href
208
+ i.set_attr "type", icon.type
209
+ end
210
+ end
211
+
212
+ # xml -> icon
213
+ def parse_icon(node)
214
+ Icon.new :href => node['href'],
215
+ :type => node['type']
216
+ end
217
+
218
+ License
219
+ ---
220
+
221
+ Copyright: [Unwire A/S](http://www.unwire.dk), 2009
222
+
223
+ License: [Creative Commons Attribution 2.5 Denmark License](http://creativecommons.org/licenses/by/2.5/dk/)
224
+
225
+ ___
226
+
227
+ troelskn@gmail.com - April, 2009
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ # -*- coding: utf-8 -*-
2
+ begin
3
+ require 'jeweler'
4
+ Jeweler::Tasks.new do |gemspec|
5
+ gemspec.name = "handsoap"
6
+ gemspec.summary = "Handsoap is a library for creating SOAP clients in Ruby"
7
+ gemspec.email = "troelskn@gmail.com"
8
+ gemspec.homepage = "http://github.com/troelskn/handsoap"
9
+ gemspec.description = gemspec.summary
10
+ gemspec.authors = ["Troels Knak-Nielsen"]
11
+ gemspec.add_dependency "nokogiri", ">= 1.2.3"
12
+ gemspec.add_dependency "curb", ">= 0.3.2"
13
+ # gemspec.add_dependency "httpclient", ">= 2.1.2"
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 3
3
+ :major: 0
4
+ :minor: 1
File without changes
@@ -0,0 +1,314 @@
1
+ require 'open-uri'
2
+ require 'uri'
3
+ require 'cgi'
4
+ require 'nokogiri'
5
+
6
+ # TODO: inline builders, if they are only ever used in one place
7
+ # TODO: http://www.crossedconnections.org/w/?p=51 -- The 'typens' namespace is magical ...
8
+
9
+ class Builders
10
+ def initialize(xsd)
11
+ @xsd = xsd
12
+ @builders = {}
13
+ end
14
+ def add(type)
15
+ @builders[type] = false unless @builders[type]
16
+ end
17
+ def each
18
+ results = []
19
+ while builder = @builders.find { |builder,is_rendered| !is_rendered }
20
+ results << yield(@xsd.get_complex_type(builder[0]))
21
+ @builders[builder[0]] = true
22
+ end
23
+ results.join("")
24
+ end
25
+ end
26
+
27
+ class HandsoapGenerator < Rails::Generator::NamedBase
28
+ attr_reader :wsdl
29
+ def initialize(runtime_args, runtime_options = {})
30
+ super
31
+ # Wsdl argument is required.
32
+ usage if @args.empty?
33
+ @wsdl_uri = @args.shift
34
+ end
35
+
36
+ def banner
37
+ "WARNING: This generator is rather incomplete and buggy. Use at your own risk." +
38
+ "\n" + "Usage: #{$0} #{spec.name} name URI [options]" +
39
+ "\n" + " name Basename of the service class" +
40
+ "\n" + " URI URI of the WSDL to generate from"
41
+ end
42
+
43
+ def manifest
44
+ record do |m|
45
+ @wsdl = Handsoap::Wsdl.new(@wsdl_uri)
46
+ @wsdl.parse!
47
+ @xsd = Handsoap::XsdSpider.new(@wsdl_uri)
48
+ @xsd.process!
49
+ m.directory "app"
50
+ m.directory "app/models"
51
+ @builders = Builders.new(@xsd)
52
+ m.template "gateway.rbt", "app/models/#{file_name}_service.rb"
53
+ end
54
+ end
55
+
56
+ def builders
57
+ @builders
58
+ end
59
+
60
+ def render_build(context_name, message_type, varname = nil, indentation = ' ')
61
+ if varname.nil?
62
+ ruby_name = message_type.ruby_name
63
+ else
64
+ ruby_name = "#{varname}[:#{message_type.ruby_name}]"
65
+ end
66
+ # message_type.namespaces
67
+ if message_type.attribute?
68
+ "#{context_name}.set_attr " + '"' + message_type.name + '", ' + ruby_name
69
+ elsif message_type.boolean?
70
+ "#{context_name}.add " + '"' + message_type.name + '", bool_to_str(' + ruby_name + ')'
71
+ elsif message_type.primitive?
72
+ "#{context_name}.add " + '"' + message_type.name + '", ' + ruby_name
73
+ elsif message_type.list?
74
+ list_type = @xsd.get_complex_type(message_type.type)
75
+ builders.add(list_type.type)
76
+ # TODO: a naming conflict waiting to happen hereabout
77
+ # TODO: indentation
78
+ "#{varname}.each do |#{message_type.ruby_name}|" + "\n" + indentation +
79
+ " build_#{list_type.ruby_type}!(#{context_name}, #{message_type.ruby_name})" + "\n" + indentation +
80
+ "end"
81
+ else
82
+ builders.add(message_type.type)
83
+ "build_#{message_type.ruby_type}!(#{context_name}, " + ruby_name + ")"
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+ module Handsoap
90
+
91
+ class Wsdl
92
+ attr_reader :uri, :soap_actions, :soap_ports, :target_namespace
93
+ def initialize(uri)
94
+ @uri = uri
95
+ end
96
+
97
+ def parse!
98
+ wsdl = Nokogiri.XML(Kernel.open(@uri).read)
99
+ @target_namespace = wsdl.namespaces['xmlns:tns'] || wsdl.namespaces['xmlns']
100
+ @soap_actions = []
101
+ @soap_ports = []
102
+ messages = {}
103
+
104
+ wsdl.xpath('//wsdl:message').each do |message|
105
+ message_name = message['name']
106
+ messages[message_name] = message.xpath('wsdl:part').map { |part| MessageType::Part.new(part['type'] || 'xs:element', part['name']) }
107
+ end
108
+
109
+ wsdl.xpath('//*[name()="soap:operation"]').each do |operation|
110
+ operation_name = operation.parent['name']
111
+ operation_spec = wsdl.xpath('//wsdl:operation[@name="' + operation_name + '"]').first
112
+ raise RuntimeError, "Couldn't find wsdl:operation node for #{operation_name}" if operation_spec.nil?
113
+ msg_type_in = operation_spec.xpath('./wsdl:input').first["message"]
114
+ raise RuntimeError, "Couldn't find wsdl:input node for #{operation_name}" if msg_type_in.nil?
115
+ raise RuntimeError, "Invalid message type #{msg_type_in} for #{operation_name}" if messages[msg_type_in].nil?
116
+ msg_type_out = operation_spec.xpath('./wsdl:output').first["message"]
117
+ raise RuntimeError, "Couldn't find wsdl:output node for #{operation_name}" if msg_type_out.nil?
118
+ raise RuntimeError, "Invalid message type #{msg_type_out} for #{operation_name}" if messages[msg_type_out].nil?
119
+ @soap_actions << SoapAction.new(operation, messages[msg_type_in], messages[msg_type_out])
120
+ end
121
+ raise RuntimeError, "Could not parse WSDL" if soap_actions.empty?
122
+
123
+ wsdl.xpath('//wsdl:port', {"xmlns:wsdl" => 'http://schemas.xmlsoap.org/wsdl/'}).each do |port|
124
+ name = port['name'].underscore
125
+ location = port.xpath('./*[@location]').first['location']
126
+ @soap_ports << { :name => name, :soap_name => port['name'], :location => location }
127
+ end
128
+ end
129
+ end
130
+
131
+ class SoapAction
132
+ attr_reader :input_type, :output_type
133
+ def initialize(xml_node, input_type, output_type)
134
+ @xml_node = xml_node
135
+ @input_type = input_type
136
+ @output_type = output_type
137
+ end
138
+ def name
139
+ @xml_node.parent['name'].underscore
140
+ end
141
+ def soap_name
142
+ @xml_node.parent['name']
143
+ end
144
+ def href
145
+ @xml_node['soapAction']
146
+ end
147
+ end
148
+
149
+ module MessageType
150
+
151
+ # complex-type is a spec (class), not an element ... (object)
152
+ # <xs:complexType name="User">
153
+ # <xs:annotation>
154
+ # <xs:documentation>The element specifies a user</xs:documentation>
155
+ # </xs:annotation>
156
+ # <xs:attribute name="dn" type="xs:string" use="required"/>
157
+ # </xs:complexType>
158
+ class ComplexType
159
+ def initialize(xml_node)
160
+ @xml_node = xml_node
161
+ end
162
+ def type
163
+ @xml_node['name']
164
+ end
165
+ def ruby_type
166
+ type.gsub(/^.*:/, "").underscore.gsub(/-/, '_')
167
+ end
168
+ def elements
169
+ @xml_node.xpath('./xs:attribute|./xs:all/xs:element|./xs:sequence').map do |node|
170
+ case
171
+ when node.node_name == 'attribute'
172
+ Attribute.new(node['type'], node['name'])
173
+ when node.node_name == 'element'
174
+ Element.new(node['type'], node['name'], []) # TODO: elements.elements
175
+ when node.node_name == 'sequence'
176
+ choice_node = node.xpath('./xs:choice').first
177
+ if choice_node
178
+ # TODO
179
+ Attribute.new('xs:choice', 'todo')
180
+ else
181
+ entity_node = node.xpath('./xs:element').first
182
+ Sequence.new(entity_node['type'], entity_node['name'])
183
+ end
184
+ else
185
+ puts node
186
+ raise "Unknown type #{node.node_name}"
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ class Base
193
+ attr_reader :type, :name
194
+ def initialize(type, name)
195
+ raise "'type' can't be nil" if type.nil?
196
+ raise "'name' can't be nil" if name.nil?
197
+ @type = type
198
+ @name = name
199
+ end
200
+ def ruby_type
201
+ type.gsub(/^.*:/, "").underscore.gsub(/-/, '_')
202
+ end
203
+ def ruby_name
204
+ name.underscore.gsub(/-/, '_')
205
+ end
206
+ def attribute?
207
+ false
208
+ end
209
+ def primitive?
210
+ /^xs:/.match type
211
+ end
212
+ def boolean?
213
+ type == "xs:boolean"
214
+ end
215
+ def list?
216
+ false
217
+ end
218
+ end
219
+
220
+ # Parts are shallow elements
221
+ # <wsdl:part name="widget-instance-id" type="xs:int" />
222
+ class Part < Base
223
+ end
224
+
225
+ # <wsdl:part name="widget-instance-id" type="xs:int" />
226
+ # <xs:element maxOccurs="1" minOccurs="0" name="description" type="xs:string"/>
227
+ class Element < Base
228
+ attr_reader :elements
229
+ def initialize(type, name, elements = [])
230
+ super(type, name)
231
+ @elements = elements
232
+ end
233
+ end
234
+
235
+ # <xs:attribute name="id" type="xs:int" use="required"/>
236
+ class Attribute < Base
237
+ def primitive?
238
+ true
239
+ end
240
+ def attribute?
241
+ true
242
+ end
243
+ end
244
+
245
+ # <xs:sequence>
246
+ # <xs:element maxOccurs="unbounded" minOccurs="0" name="widget-area" type="WidgetArea"/>
247
+ # </xs:sequence>
248
+ class Sequence < Base
249
+ def list?
250
+ true
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+ module Handsoap
257
+
258
+ class XsdSpider
259
+ def initialize(uri)
260
+ @queue = []
261
+ @wsdl_uri = uri
262
+ end
263
+
264
+ def results
265
+ @queue.map { |element| element[:data] }
266
+ end
267
+
268
+ def get_complex_type(name)
269
+ # TODO namespace
270
+ short_name = name.gsub(/^.*:/, "")
271
+ results.each do |data|
272
+ search = data[:document].xpath('//xs:complexType[@name="' + short_name + '"]')
273
+ if search.any?
274
+ return MessageType::ComplexType.new(search.first)
275
+ end
276
+ end
277
+ raise "Didn't find '#{name}' (short name #{short_name})"
278
+ end
279
+
280
+ def process!
281
+ spider_href(@wsdl_uri, nil)
282
+ while process_next do end
283
+ end
284
+
285
+ private
286
+
287
+ def add_href(href, namespace)
288
+ unless @queue.find { |element| element[:href] == href }
289
+ @queue << { :href => href, :namespace => namespace, :state => :new, :data => {} }
290
+ end
291
+ end
292
+
293
+ def process_next
294
+ next_element = @queue.find { |element| element[:state] == :new }
295
+ if next_element
296
+ next_element[:data] = spider_href(next_element[:href], next_element[:namespace])
297
+ next_element[:state] = :done
298
+ return true
299
+ end
300
+ return false
301
+ end
302
+
303
+ def spider_href(href, namespace)
304
+ raise "'href' must be a String" if href.nil?
305
+ xsd = Nokogiri.XML(Kernel.open(href).read)
306
+ # <xs:include schemaLocation="...xsd"/>
307
+ # <xs:import namespace="" schemaLocation="...xsd"/>
308
+ xsd.xpath('//*[@schemaLocation]').each do |inc|
309
+ add_href(inc['schemaLocation'], inc['namespace'] || namespace)
310
+ end
311
+ { :document => xsd, :namespace => namespace }
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,27 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'handsoap'
3
+
4
+ class <%= class_name %>Service < Handsoap::Service
5
+ endpoint <%= file_name.upcase %>_SERVICE_ENDPOINT
6
+ on_create_document do |doc|
7
+ doc.alias 'wsdl', "<%= wsdl.target_namespace %>"
8
+ end
9
+ <% wsdl.soap_actions.each do |action| %>
10
+ def <%= action.name %>(<%= action.input_type.map { |message_type| message_type.ruby_name }.join(", ") %>)
11
+ response = invoke("wsdl:<%= action.soap_name %>") do |context|<% action.input_type.each do |message_type| %>
12
+ <%= render_build('context', message_type) %><% end %>
13
+ end
14
+ response.document.xpath('//*').map { |node| raise "TODO" }
15
+ end
16
+ <% end %>
17
+ private
18
+ # builders
19
+ <% builders.each do |type| %>
20
+ # <%= type.type %> ruby -> xml
21
+ def build_<%= type.ruby_type %>!(context, <%= type.ruby_type %>)<% type.elements.each do |element| %>
22
+ <%= render_build('context', element, type.ruby_type) %><% end %>
23
+ end
24
+ <% end %>
25
+ # parsers
26
+ # TODO
27
+ end
data/lib/handsoap.rb ADDED
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'handsoap/xml_mason'
3
+ require 'handsoap/service'
4
+
5
+
@@ -0,0 +1,287 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'rubygems'
3
+ require 'httpclient'
4
+ require 'nokogiri'
5
+ require 'curb'
6
+ require 'handsoap/xml_mason'
7
+ require 'time'
8
+
9
+ module Handsoap
10
+
11
+ def self.http_driver
12
+ @http_driver || :curb
13
+ end
14
+
15
+ def self.http_driver=(driver)
16
+ @http_driver = driver
17
+ end
18
+
19
+ SOAP_NAMESPACE = { 1 => 'http://schemas.xmlsoap.org/soap/envelope/', 2 => 'http://www.w3.org/2001/12/soap-encoding' }
20
+
21
+ class Response
22
+ def initialize(http_body, soap_namespace)
23
+ @http_body = http_body
24
+ @soap_namespace = soap_namespace
25
+ @document = :lazy
26
+ @fault = :lazy
27
+ end
28
+ def document?
29
+ !! document
30
+ end
31
+ def document
32
+ if @document == :lazy
33
+ doc = Nokogiri::XML(@http_body)
34
+ @document = (doc && doc.root && doc.errors.empty?) ? doc : nil
35
+ end
36
+ return @document
37
+ end
38
+ def fault?
39
+ !! fault
40
+ end
41
+ def fault
42
+ if @fault == :lazy
43
+ nodes = document? ? document.xpath('/env:Envelope/env:Body/descendant-or-self::env:Fault', { 'env' => @soap_namespace }) : []
44
+ @fault = nodes.any? ? Fault.from_xml(nodes.first, :namespace => @soap_namespace) : nil
45
+ end
46
+ return @fault
47
+ end
48
+ end
49
+
50
+ class Fault < Exception
51
+ attr_reader :code, :reason, :details
52
+ def initialize(code, reason, details)
53
+ @code = code
54
+ @reason = reason
55
+ @details = details
56
+ end
57
+ def to_s
58
+ "Handsoap::Fault { :code => '#{@code}', :reason => '#{@reason}' }"
59
+ end
60
+ def self.from_xml(node, options = { :namespace => nil })
61
+ if not options[:namespace]
62
+ raise "Missing option :namespace"
63
+ end
64
+ ns = { 'env' => options[:namespace] }
65
+ fault_code = node.xpath('./env:Code/env:Value/text()', ns).to_s
66
+ if fault_code == ""
67
+ fault_code = node.xpath('./faultcode/text()', ns).to_s
68
+ end
69
+ reason = node.xpath('./env:Reason/env:Text[1]/text()', ns).to_s
70
+ if reason == ""
71
+ reason = node.xpath('./faultstring/text()', ns).to_s
72
+ end
73
+ details = node.xpath('./detail/*', ns)
74
+ self.new(fault_code, reason, details)
75
+ end
76
+ end
77
+
78
+ class Service
79
+ @@logger = nil
80
+ def self.logger=(io)
81
+ @@logger = io
82
+ end
83
+ def self.endpoint(args = {})
84
+ @protocol_version = args[:version] || raise("Missing option :version")
85
+ @uri = args[:uri] || raise("Missing option :uri")
86
+ end
87
+ def self.envelope_namespace
88
+ if SOAP_NAMESPACE[@protocol_version].nil?
89
+ raise "Unknown protocol version '#{@protocol_version.inspect}'"
90
+ end
91
+ SOAP_NAMESPACE[@protocol_version]
92
+ end
93
+ def self.request_content_type
94
+ @protocol_version == 1 ? "text/xml" : "application/soap+xml"
95
+ end
96
+ def self.map_method(mapping)
97
+ if @mapping.nil?
98
+ @mapping = {}
99
+ end
100
+ @mapping.merge! mapping
101
+ end
102
+ def self.on_create_document(&block)
103
+ @create_document_callback = block
104
+ end
105
+ def self.fire_on_create_document(doc)
106
+ if @create_document_callback
107
+ @create_document_callback.call doc
108
+ end
109
+ end
110
+ def self.uri
111
+ @uri
112
+ end
113
+ def self.get_mapping(name)
114
+ @mapping[name] if @mapping
115
+ end
116
+ @@instance = {}
117
+ def self.instance
118
+ @@instance[self.to_s] ||= self.new
119
+ end
120
+ def self.method_missing(method, *args)
121
+ if instance.respond_to?(method)
122
+ instance.__send__ method, *args
123
+ else
124
+ super
125
+ end
126
+ end
127
+ def method_missing(method, *args)
128
+ action = self.class.get_mapping(method)
129
+ if action
130
+ invoke(action, *args)
131
+ else
132
+ super
133
+ end
134
+ end
135
+ def invoke(action, options = { :soap_action => :auto }, &block)
136
+ if action
137
+ if options.kind_of? String
138
+ options = { :soap_action => options }
139
+ end
140
+ if options[:soap_action] == :auto
141
+ options[:soap_action] = action.gsub(/^.+:/, "")
142
+ elsif options[:soap_action] == :none
143
+ options[:soap_action] = nil
144
+ end
145
+ doc = make_envelope do |body|
146
+ body.add action
147
+ end
148
+ if block_given?
149
+ yield doc.find(action)
150
+ end
151
+ dispatch(doc, options[:soap_action])
152
+ end
153
+ end
154
+ def on_before_dispatch
155
+ end
156
+ def on_fault(fault)
157
+ raise fault
158
+ end
159
+ private
160
+ # Helper to serialize a node into a ruby string
161
+ def xml_to_str(node, xquery = nil)
162
+ begin
163
+ n = xquery ? node.xpath(xquery, ns).first : node
164
+ n.serialize('UTF-8')
165
+ rescue Exception => ex
166
+ nil
167
+ end
168
+ end
169
+ # Helper to serialize a node into a ruby integer
170
+ def xml_to_int(node, xquery = nil)
171
+ begin
172
+ n = xquery ? node.xpath(xquery, ns).first : node
173
+ n.to_s.to_i
174
+ rescue Exception => ex
175
+ nil
176
+ end
177
+ end
178
+ # Helper to serialize a node into a ruby float
179
+ def xml_to_float(node, xquery = nil)
180
+ begin
181
+ n = xquery ? node.xpath(xquery, ns).first : node
182
+ n.to_s.to_f
183
+ rescue Exception => ex
184
+ nil
185
+ end
186
+ end
187
+ # Helper to serialize a node into a ruby boolean
188
+ def xml_to_bool(node, xquery = nil)
189
+ begin
190
+ n = xquery ? node.xpath(xquery, ns).first : node
191
+ n.to_s == "true"
192
+ rescue Exception => ex
193
+ nil
194
+ end
195
+ end
196
+ # Helper to serialize a node into a ruby Time object
197
+ def xml_to_date(node, xquery = nil)
198
+ begin
199
+ n = xquery ? node.xpath(xquery, ns).first : node
200
+ Time.iso8601(n.to_s)
201
+ rescue Exception => ex
202
+ nil
203
+ end
204
+ end
205
+ def debug(message = nil)
206
+ if @@logger
207
+ if message
208
+ @@logger.puts(message)
209
+ end
210
+ if block_given?
211
+ yield @@logger
212
+ end
213
+ end
214
+ end
215
+ def dispatch(doc, action)
216
+ on_before_dispatch()
217
+ headers = {
218
+ "Content-Type" => "#{self.class.request_content_type};charset=UTF-8"
219
+ }
220
+ headers["SOAPAction"] = action unless action.nil?
221
+ body = doc.to_s
222
+ debug do |logger|
223
+ logger.puts "==============="
224
+ logger.puts "--- Request ---"
225
+ logger.puts "URI: %s" % [self.class.uri]
226
+ logger.puts headers.map { |key,value| key + ": " + value }.join("\n")
227
+ logger.puts "---"
228
+ logger.puts body
229
+ end
230
+ if Handsoap.http_driver == :curb
231
+ http_client = Curl::Easy.new(self.class.uri)
232
+ http_client.headers = headers
233
+ http_client.http_post body
234
+ debug do |logger|
235
+ logger.puts "--- Response ---"
236
+ logger.puts "HTTP Status: %s" % [http_client.response_code]
237
+ logger.puts "Content-Type: %s" % [http_client.content_type]
238
+ logger.puts "---"
239
+ logger.puts Handsoap.pretty_format_envelope(http_client.body_str)
240
+ end
241
+ soap_response = Response.new(http_client.body_str, self.class.envelope_namespace)
242
+ else
243
+ response = HTTPClient.new.post(self.class.uri, body, headers)
244
+ debug do |logger|
245
+ logger.puts "--- Response ---"
246
+ logger.puts "HTTP Status: %s" % [response.status]
247
+ logger.puts "Content-Type: %s" % [response.contenttype]
248
+ logger.puts "---"
249
+ logger.puts Handsoap.pretty_format_envelope(response.content)
250
+ end
251
+ soap_response = Response.new(response.content, self.class.envelope_namespace)
252
+ end
253
+ if soap_response.fault?
254
+ return self.on_fault(soap_response.fault)
255
+ end
256
+ return soap_response
257
+ end
258
+ def make_envelope
259
+ doc = XmlMason::Document.new do |doc|
260
+ doc.alias 'env', self.class.envelope_namespace
261
+ doc.add "env:Envelope" do |env|
262
+ env.add "*:Header"
263
+ env.add "*:Body"
264
+ end
265
+ end
266
+ self.class.fire_on_create_document doc
267
+ if block_given?
268
+ yield doc.find("Body")
269
+ end
270
+ return doc
271
+ end
272
+ end
273
+
274
+ def self.pretty_format_envelope(xml_string)
275
+ if /^<.*:Envelope/.match(xml_string)
276
+ begin
277
+ doc = Nokogiri::XML(xml_string)
278
+ rescue Exception => ex
279
+ return "Formatting failed: " + ex.to_s
280
+ end
281
+ return doc.to_s
282
+ # return "\n\e[1;33m" + doc.to_s + "\e[0m"
283
+ end
284
+ return xml_string
285
+ end
286
+
287
+ end
@@ -0,0 +1,175 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Handsoap
4
+
5
+ module XmlMason
6
+
7
+ HTML_ESCAPE = { '&' => '&amp;', '"' => '&quot;', '>' => '&gt;', '<' => '&lt;' }
8
+
9
+ def self.html_escape(s)
10
+ s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }
11
+ end
12
+
13
+ class Node
14
+ def initialize
15
+ @namespaces = {}
16
+ end
17
+ def add(node_name, value = nil)
18
+ prefix, name = parse_ns(node_name)
19
+ node = append_child Element.new(self, prefix, name, value)
20
+ if block_given?
21
+ yield node
22
+ end
23
+ end
24
+ def alias(prefix, namespaces)
25
+ @namespaces[prefix] = namespaces
26
+ end
27
+ def parse_ns(name)
28
+ matches = name.match /^([^:]+):(.*)$/
29
+ if matches
30
+ [matches[1] == '*' ? @prefix : matches[1], matches[2]]
31
+ else
32
+ [nil, name]
33
+ end
34
+ end
35
+ private :parse_ns
36
+ end
37
+
38
+ class Document < Node
39
+ def initialize
40
+ super
41
+ @document_element = nil
42
+ if block_given?
43
+ yield self
44
+ end
45
+ end
46
+ def append_child(node)
47
+ if not @document_element.nil?
48
+ raise "There can only be one element at the top level."
49
+ end
50
+ @document_element = node
51
+ end
52
+ def find(name)
53
+ @document_element.find(name)
54
+ end
55
+ def find_all(name)
56
+ @document_element.find_all(name)
57
+ end
58
+ def get_namespace(prefix)
59
+ @namespaces[prefix] || raise("No alias registered for prefix '#{prefix}'")
60
+ end
61
+ def defines_namespace?(prefix)
62
+ false
63
+ end
64
+ def to_s
65
+ if @document_element.nil?
66
+ raise "No document element added."
67
+ end
68
+ "<?xml version='1.0' ?>" + "\n" + @document_element.to_s
69
+ end
70
+ end
71
+
72
+ class TextNode
73
+ def initialize(text)
74
+ @text = text
75
+ end
76
+ def to_s(indentation = '')
77
+ XmlMason.html_escape(@text)
78
+ end
79
+ end
80
+
81
+ class Element < Node
82
+ def initialize(parent, prefix, node_name, value = nil)
83
+ super()
84
+ # if prefix.to_s == ""
85
+ # raise "missing prefix"
86
+ # end
87
+ @parent = parent
88
+ @prefix = prefix
89
+ @node_name = node_name
90
+ @children = []
91
+ @attributes = {}
92
+ if not value.nil?
93
+ set_value value.to_s
94
+ end
95
+ if block_given?
96
+ yield self
97
+ end
98
+ end
99
+ def full_name
100
+ @prefix.nil? ? @node_name : (@prefix + ":" + @node_name)
101
+ end
102
+ def append_child(node)
103
+ if value_node?
104
+ raise "Element already has a text value. Can't add nodes"
105
+ end
106
+ @children << node
107
+ return node
108
+ end
109
+ def set_value(value)
110
+ if @children.length > 0
111
+ raise "Element already has children. Can't set value"
112
+ end
113
+ @children = [TextNode.new(value)]
114
+ end
115
+ def set_attr(name, value)
116
+ full_name = parse_ns(name).join(":")
117
+ @attributes[name] = value
118
+ end
119
+ def find(name)
120
+ if @node_name == name || full_name == name
121
+ return self
122
+ end
123
+ @children.each do |node|
124
+ if node.respond_to? :find
125
+ tmp = node.find(name)
126
+ if tmp
127
+ return tmp
128
+ end
129
+ end
130
+ end
131
+ return nil
132
+ end
133
+ def find_all(name)
134
+ result = []
135
+ if @node_name == name || full_name == name
136
+ result << self
137
+ end
138
+ @children.each do |node|
139
+ if node.respond_to? :find
140
+ result = result.concat(node.find_all(name))
141
+ end
142
+ end
143
+ return result
144
+ end
145
+ def value_node?
146
+ @children.length == 1 && @children[0].kind_of?(TextNode)
147
+ end
148
+ def get_namespace(prefix)
149
+ @namespaces[prefix] || @parent.get_namespace(prefix)
150
+ end
151
+ def defines_namespace?(prefix)
152
+ @attributes.keys.include?("xmlns:#{prefix}") || @parent.defines_namespace?(prefix)
153
+ end
154
+ def to_s(indentation = '')
155
+ # todo resolve attribute prefixes aswell
156
+ if @prefix && (not defines_namespace?(@prefix))
157
+ set_attr "xmlns:#{@prefix}", get_namespace(@prefix)
158
+ end
159
+ name = XmlMason.html_escape(full_name)
160
+ attr = (@attributes.any? ? (" " + @attributes.map { |key, value| XmlMason.html_escape(key) + '="' + XmlMason.html_escape(value) + '"' }.join(" ")) : "")
161
+ if @children.any?
162
+ if value_node?
163
+ children = @children[0].to_s(indentation + " ")
164
+ else
165
+ children = @children.map { |node| "\n" + node.to_s(indentation + " ") }.join("") + "\n" + indentation
166
+ end
167
+ indentation + "<" + name + attr + ">" + children + "</" + name + ">"
168
+ else
169
+ indentation + "<" + name + attr + " />"
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: digitaria-handsoap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Troels Knak-Nielsen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-29 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: nokogiri
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.2.3
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: curb
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.3.2
34
+ version:
35
+ description: Handsoap is a library for creating SOAP clients in Ruby
36
+ email: troelskn@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README.markdown
43
+ files:
44
+ - README.markdown
45
+ - Rakefile
46
+ - VERSION.yml
47
+ - generators/handsoap/USAGE
48
+ - generators/handsoap/handsoap_generator.rb
49
+ - generators/handsoap/templates/gateway.rbt
50
+ - lib/handsoap.rb
51
+ - lib/handsoap/service.rb
52
+ - lib/handsoap/xml_mason.rb
53
+ has_rdoc: true
54
+ homepage: http://github.com/troelskn/handsoap
55
+ post_install_message:
56
+ rdoc_options:
57
+ - --charset=UTF-8
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.2.0
76
+ signing_key:
77
+ specification_version: 2
78
+ summary: Handsoap is a library for creating SOAP clients in Ruby
79
+ test_files: []
80
+