tina4ruby 3.11.13 → 3.11.15
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 +4 -4
- data/CHANGELOG.md +80 -80
- data/LICENSE.txt +21 -21
- data/README.md +137 -137
- data/exe/tina4ruby +5 -5
- data/lib/tina4/ai.rb +696 -696
- data/lib/tina4/api.rb +189 -189
- data/lib/tina4/auth.rb +305 -305
- data/lib/tina4/auto_crud.rb +244 -244
- data/lib/tina4/cache.rb +154 -154
- data/lib/tina4/cli.rb +1449 -1449
- data/lib/tina4/constants.rb +46 -46
- data/lib/tina4/container.rb +74 -74
- data/lib/tina4/cors.rb +74 -74
- data/lib/tina4/crud.rb +692 -692
- data/lib/tina4/database/sqlite3_adapter.rb +165 -165
- data/lib/tina4/database.rb +625 -625
- data/lib/tina4/database_result.rb +208 -208
- data/lib/tina4/debug.rb +8 -8
- data/lib/tina4/dev.rb +14 -14
- data/lib/tina4/dev_admin.rb +935 -935
- data/lib/tina4/dev_mailbox.rb +191 -191
- data/lib/tina4/drivers/firebird_driver.rb +124 -110
- data/lib/tina4/drivers/mongodb_driver.rb +561 -561
- data/lib/tina4/drivers/mssql_driver.rb +112 -112
- data/lib/tina4/drivers/mysql_driver.rb +90 -90
- data/lib/tina4/drivers/odbc_driver.rb +191 -191
- data/lib/tina4/drivers/postgres_driver.rb +116 -106
- data/lib/tina4/drivers/sqlite_driver.rb +122 -122
- data/lib/tina4/env.rb +95 -95
- data/lib/tina4/error_overlay.rb +252 -252
- data/lib/tina4/events.rb +109 -109
- data/lib/tina4/field_types.rb +154 -154
- data/lib/tina4/frond.rb +2025 -2025
- data/lib/tina4/gallery/auth/meta.json +1 -1
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
- data/lib/tina4/gallery/database/meta.json +1 -1
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
- data/lib/tina4/gallery/error-overlay/meta.json +1 -1
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
- data/lib/tina4/gallery/orm/meta.json +1 -1
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
- data/lib/tina4/gallery/rest-api/meta.json +1 -1
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
- data/lib/tina4/gallery/templates/meta.json +1 -1
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
- data/lib/tina4/graphql.rb +966 -966
- data/lib/tina4/health.rb +39 -39
- data/lib/tina4/html_element.rb +170 -170
- data/lib/tina4/job.rb +80 -80
- data/lib/tina4/localization.rb +168 -168
- data/lib/tina4/log.rb +203 -203
- data/lib/tina4/mcp.rb +696 -696
- data/lib/tina4/messenger.rb +587 -587
- data/lib/tina4/metrics.rb +793 -793
- data/lib/tina4/middleware.rb +445 -445
- data/lib/tina4/migration.rb +451 -451
- data/lib/tina4/orm.rb +790 -790
- data/lib/tina4/public/css/tina4.css +2463 -2463
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/public/images/logo.svg +5 -5
- data/lib/tina4/public/js/frond.min.js +2 -2
- data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
- data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
- data/lib/tina4/public/js/tina4.min.js +92 -92
- data/lib/tina4/public/js/tina4js.min.js +48 -48
- data/lib/tina4/public/swagger/index.html +90 -90
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
- data/lib/tina4/query_builder.rb +380 -380
- data/lib/tina4/queue.rb +366 -366
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
- data/lib/tina4/queue_backends/lite_backend.rb +298 -298
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
- data/lib/tina4/rack_app.rb +817 -817
- data/lib/tina4/rate_limiter.rb +130 -130
- data/lib/tina4/request.rb +268 -255
- data/lib/tina4/response.rb +346 -346
- data/lib/tina4/response_cache.rb +551 -551
- data/lib/tina4/router.rb +406 -406
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
- data/lib/tina4/scss/tina4css/_badges.scss +22 -22
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
- data/lib/tina4/scss/tina4css/_cards.scss +49 -49
- data/lib/tina4/scss/tina4css/_forms.scss +156 -156
- data/lib/tina4/scss/tina4css/_grid.scss +81 -81
- data/lib/tina4/scss/tina4css/_modals.scss +84 -84
- data/lib/tina4/scss/tina4css/_nav.scss +149 -149
- data/lib/tina4/scss/tina4css/_reset.scss +94 -94
- data/lib/tina4/scss/tina4css/_tables.scss +54 -54
- data/lib/tina4/scss/tina4css/_typography.scss +55 -55
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
- data/lib/tina4/scss/tina4css/_variables.scss +117 -117
- data/lib/tina4/scss/tina4css/base.scss +1 -1
- data/lib/tina4/scss/tina4css/colors.scss +48 -48
- data/lib/tina4/scss/tina4css/tina4.scss +17 -17
- data/lib/tina4/scss_compiler.rb +178 -178
- data/lib/tina4/seeder.rb +567 -567
- data/lib/tina4/service_runner.rb +303 -303
- data/lib/tina4/session.rb +297 -297
- data/lib/tina4/session_handlers/database_handler.rb +72 -72
- data/lib/tina4/session_handlers/file_handler.rb +67 -67
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
- data/lib/tina4/session_handlers/redis_handler.rb +43 -43
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
- data/lib/tina4/shutdown.rb +84 -84
- data/lib/tina4/sql_translation.rb +158 -158
- data/lib/tina4/swagger.rb +124 -124
- data/lib/tina4/template.rb +894 -894
- data/lib/tina4/templates/base.twig +26 -26
- data/lib/tina4/templates/errors/302.twig +14 -14
- data/lib/tina4/templates/errors/401.twig +9 -9
- data/lib/tina4/templates/errors/403.twig +29 -29
- data/lib/tina4/templates/errors/404.twig +29 -29
- data/lib/tina4/templates/errors/500.twig +38 -38
- data/lib/tina4/templates/errors/502.twig +9 -9
- data/lib/tina4/templates/errors/503.twig +12 -12
- data/lib/tina4/templates/errors/base.twig +37 -37
- data/lib/tina4/test_client.rb +159 -159
- data/lib/tina4/testing.rb +340 -340
- data/lib/tina4/validator.rb +174 -174
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +312 -312
- data/lib/tina4/websocket.rb +343 -343
- data/lib/tina4/websocket_backplane.rb +190 -190
- data/lib/tina4/wsdl.rb +564 -564
- data/lib/tina4.rb +458 -458
- data/lib/tina4ruby.rb +4 -4
- metadata +3 -3
data/lib/tina4/wsdl.rb
CHANGED
|
@@ -1,564 +1,564 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "rexml/document"
|
|
4
|
-
|
|
5
|
-
module Tina4
|
|
6
|
-
# SOAP 1.1 / WSDL server — zero-dependency, mirrors tina4-python's wsdl module.
|
|
7
|
-
#
|
|
8
|
-
# Usage (class-based):
|
|
9
|
-
#
|
|
10
|
-
# class Calculator < Tina4::WSDL
|
|
11
|
-
# wsdl_operation output: { Result: :int }
|
|
12
|
-
# def add(a, b)
|
|
13
|
-
# { Result: a.to_i + b.to_i }
|
|
14
|
-
# end
|
|
15
|
-
# end
|
|
16
|
-
#
|
|
17
|
-
# # In a route handler:
|
|
18
|
-
# service = Calculator.new(request)
|
|
19
|
-
# response.call(service.handle)
|
|
20
|
-
#
|
|
21
|
-
# Supported:
|
|
22
|
-
# - WSDL 1.1 generation from Ruby type declarations
|
|
23
|
-
# - SOAP 1.1 request/response handling via REXML
|
|
24
|
-
# - Lifecycle hooks (on_request, on_result)
|
|
25
|
-
# - Auto type mapping (Integer -> int, String -> string, Float -> double, etc.)
|
|
26
|
-
# - XML escaping on all response values
|
|
27
|
-
# - SOAP fault responses on errors
|
|
28
|
-
#
|
|
29
|
-
class WSDL
|
|
30
|
-
NS_SOAP = "http://schemas.xmlsoap.org/wsdl/soap/"
|
|
31
|
-
NS_WSDL = "http://schemas.xmlsoap.org/wsdl/"
|
|
32
|
-
NS_XSD = "http://www.w3.org/2001/XMLSchema"
|
|
33
|
-
NS_SOAP_ENV = "http://schemas.xmlsoap.org/soap/envelope/"
|
|
34
|
-
|
|
35
|
-
RUBY_TO_XSD = {
|
|
36
|
-
:int => "xsd:int",
|
|
37
|
-
:integer => "xsd:int",
|
|
38
|
-
:string => "xsd:string",
|
|
39
|
-
:float => "xsd:double",
|
|
40
|
-
:double => "xsd:double",
|
|
41
|
-
:boolean => "xsd:boolean",
|
|
42
|
-
:bool => "xsd:boolean",
|
|
43
|
-
:date => "xsd:date",
|
|
44
|
-
:datetime => "xsd:dateTime",
|
|
45
|
-
:base64 => "xsd:base64Binary",
|
|
46
|
-
Integer => "xsd:int",
|
|
47
|
-
String => "xsd:string",
|
|
48
|
-
Float => "xsd:double",
|
|
49
|
-
TrueClass => "xsd:boolean",
|
|
50
|
-
FalseClass => "xsd:boolean"
|
|
51
|
-
}.freeze
|
|
52
|
-
|
|
53
|
-
# ── Class-level DSL ──────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
class << self
|
|
56
|
-
# Registry of operations declared via wsdl_operation + def.
|
|
57
|
-
# Each entry: { input: { name => type, ... }, output: { name => type, ... } }
|
|
58
|
-
def wsdl_operations
|
|
59
|
-
@wsdl_operations ||= {}
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Pending output hash waiting for the next method definition.
|
|
63
|
-
def pending_wsdl_output
|
|
64
|
-
@pending_wsdl_output
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Mark the next defined method as a WSDL operation.
|
|
68
|
-
#
|
|
69
|
-
# wsdl_operation output: { Result: :int }
|
|
70
|
-
# def add(a, b) ...
|
|
71
|
-
#
|
|
72
|
-
# Input parameters are inferred from the method signature.
|
|
73
|
-
# The +output+ hash maps response element names to XSD type symbols.
|
|
74
|
-
def wsdl_operation(output: {})
|
|
75
|
-
@pending_wsdl_output = output
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Hook into method definition to capture the pending operation.
|
|
79
|
-
def method_added(method_name)
|
|
80
|
-
super
|
|
81
|
-
return unless @pending_wsdl_output
|
|
82
|
-
|
|
83
|
-
output = @pending_wsdl_output
|
|
84
|
-
@pending_wsdl_output = nil
|
|
85
|
-
|
|
86
|
-
# Infer input parameter names from the method signature.
|
|
87
|
-
params = instance_method(method_name).parameters
|
|
88
|
-
input = {}
|
|
89
|
-
params.each do |_kind, name|
|
|
90
|
-
next if name.nil?
|
|
91
|
-
input[name] = :string # default; callers can rely on type coercion
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
wsdl_operations[method_name.to_s] = { input: input, output: output }
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Ensure subclasses get their own copy of the operations registry.
|
|
98
|
-
def inherited(subclass)
|
|
99
|
-
super
|
|
100
|
-
subclass.instance_variable_set(:@wsdl_operations, wsdl_operations.dup)
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# ── Instance ─────────────────────────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
attr_reader :request, :service_url
|
|
107
|
-
|
|
108
|
-
def initialize(request = nil, service_url: "")
|
|
109
|
-
@request = request
|
|
110
|
-
@service_url = service_url.empty? ? infer_url : service_url
|
|
111
|
-
@operations = discover_operations
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Main entry point. Returns WSDL XML on GET/?wsdl, or processes a SOAP
|
|
115
|
-
# request on POST.
|
|
116
|
-
def handle
|
|
117
|
-
return generate_wsdl if @request.nil?
|
|
118
|
-
|
|
119
|
-
method = if @request.respond_to?(:method)
|
|
120
|
-
@request.method.to_s.upcase
|
|
121
|
-
elsif @request.respond_to?(:body) && @request.body && !@request.body.to_s.empty?
|
|
122
|
-
"POST"
|
|
123
|
-
else
|
|
124
|
-
"GET"
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
params = (@request.respond_to?(:params) ? @request.params : nil) || {}
|
|
128
|
-
url = (@request.respond_to?(:url) ? @request.url : nil) || ""
|
|
129
|
-
|
|
130
|
-
if method == "GET" || params.key?("wsdl") || params.key?(:wsdl) || url.end_with?("?wsdl")
|
|
131
|
-
return generate_wsdl
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
body = if @request.respond_to?(:body)
|
|
135
|
-
@request.body.is_a?(String) ? @request.body : @request.body.to_s
|
|
136
|
-
else
|
|
137
|
-
""
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
process_soap(body)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# ── Lifecycle hooks (override in subclass) ───────────────────────────
|
|
144
|
-
|
|
145
|
-
# Called before operation invocation. Override to validate/log.
|
|
146
|
-
def on_request(request)
|
|
147
|
-
# no-op
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Called after operation returns. Override to transform/audit.
|
|
151
|
-
# Must return the (possibly modified) result.
|
|
152
|
-
def on_result(result)
|
|
153
|
-
result
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# ── WSDL generation ──────────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
def generate_wsdl(endpoint_url = "")
|
|
159
|
-
@service_url = endpoint_url unless endpoint_url.empty?
|
|
160
|
-
service_name = self.class.name ? self.class.name.split("::").last : "AnonymousService"
|
|
161
|
-
tns = "urn:#{service_name}"
|
|
162
|
-
|
|
163
|
-
parts = []
|
|
164
|
-
parts << '<?xml version="1.0" encoding="UTF-8"?>'
|
|
165
|
-
parts << "<definitions name=\"#{service_name}\""
|
|
166
|
-
parts << " targetNamespace=\"#{tns}\""
|
|
167
|
-
parts << " xmlns:tns=\"#{tns}\""
|
|
168
|
-
parts << " xmlns:soap=\"#{NS_SOAP}\""
|
|
169
|
-
parts << " xmlns:xsd=\"#{NS_XSD}\""
|
|
170
|
-
parts << " xmlns=\"#{NS_WSDL}\">"
|
|
171
|
-
parts << ""
|
|
172
|
-
|
|
173
|
-
# Types
|
|
174
|
-
parts << " <types>"
|
|
175
|
-
parts << " <xsd:schema targetNamespace=\"#{tns}\">"
|
|
176
|
-
|
|
177
|
-
@operations.each do |op_name, meta|
|
|
178
|
-
# Request element
|
|
179
|
-
parts << " <xsd:element name=\"#{op_name}\">"
|
|
180
|
-
parts << " <xsd:complexType>"
|
|
181
|
-
parts << " <xsd:sequence>"
|
|
182
|
-
meta[:input].each do |pname, ptype|
|
|
183
|
-
xsd = xsd_type(ptype)
|
|
184
|
-
parts << " <xsd:element name=\"#{pname}\" type=\"#{xsd}\"/>"
|
|
185
|
-
end
|
|
186
|
-
parts << " </xsd:sequence>"
|
|
187
|
-
parts << " </xsd:complexType>"
|
|
188
|
-
parts << " </xsd:element>"
|
|
189
|
-
|
|
190
|
-
# Response element
|
|
191
|
-
parts << " <xsd:element name=\"#{op_name}Response\">"
|
|
192
|
-
parts << " <xsd:complexType>"
|
|
193
|
-
parts << " <xsd:sequence>"
|
|
194
|
-
meta[:output].each do |rname, rtype|
|
|
195
|
-
xsd = xsd_type(rtype)
|
|
196
|
-
parts << " <xsd:element name=\"#{rname}\" type=\"#{xsd}\"/>"
|
|
197
|
-
end
|
|
198
|
-
parts << " </xsd:sequence>"
|
|
199
|
-
parts << " </xsd:complexType>"
|
|
200
|
-
parts << " </xsd:element>"
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
parts << " </xsd:schema>"
|
|
204
|
-
parts << " </types>"
|
|
205
|
-
parts << ""
|
|
206
|
-
|
|
207
|
-
# Messages
|
|
208
|
-
@operations.each_key do |op_name|
|
|
209
|
-
parts << " <message name=\"#{op_name}Input\">"
|
|
210
|
-
parts << " <part name=\"parameters\" element=\"tns:#{op_name}\"/>"
|
|
211
|
-
parts << " </message>"
|
|
212
|
-
parts << " <message name=\"#{op_name}Output\">"
|
|
213
|
-
parts << " <part name=\"parameters\" element=\"tns:#{op_name}Response\"/>"
|
|
214
|
-
parts << " </message>"
|
|
215
|
-
end
|
|
216
|
-
parts << ""
|
|
217
|
-
|
|
218
|
-
# PortType
|
|
219
|
-
parts << " <portType name=\"#{service_name}PortType\">"
|
|
220
|
-
@operations.each_key do |op_name|
|
|
221
|
-
parts << " <operation name=\"#{op_name}\">"
|
|
222
|
-
parts << " <input message=\"tns:#{op_name}Input\"/>"
|
|
223
|
-
parts << " <output message=\"tns:#{op_name}Output\"/>"
|
|
224
|
-
parts << " </operation>"
|
|
225
|
-
end
|
|
226
|
-
parts << " </portType>"
|
|
227
|
-
parts << ""
|
|
228
|
-
|
|
229
|
-
# Binding
|
|
230
|
-
parts << " <binding name=\"#{service_name}Binding\" type=\"tns:#{service_name}PortType\">"
|
|
231
|
-
parts << " <soap:binding style=\"document\" transport=\"http://schemas.xmlsoap.org/soap/http\"/>"
|
|
232
|
-
@operations.each_key do |op_name|
|
|
233
|
-
parts << " <operation name=\"#{op_name}\">"
|
|
234
|
-
parts << " <soap:operation soapAction=\"#{tns}/#{op_name}\"/>"
|
|
235
|
-
parts << ' <input><soap:body use="literal"/></input>'
|
|
236
|
-
parts << ' <output><soap:body use="literal"/></output>'
|
|
237
|
-
parts << " </operation>"
|
|
238
|
-
end
|
|
239
|
-
parts << " </binding>"
|
|
240
|
-
parts << ""
|
|
241
|
-
|
|
242
|
-
# Service
|
|
243
|
-
parts << " <service name=\"#{service_name}\">"
|
|
244
|
-
parts << " <port name=\"#{service_name}Port\" binding=\"tns:#{service_name}Binding\">"
|
|
245
|
-
parts << " <soap:address location=\"#{@service_url}\"/>"
|
|
246
|
-
parts << " </port>"
|
|
247
|
-
parts << " </service>"
|
|
248
|
-
|
|
249
|
-
parts << "</definitions>"
|
|
250
|
-
parts.join("\n")
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
private
|
|
254
|
-
|
|
255
|
-
# ── Auto-discovery ───────────────────────────────────────────────────
|
|
256
|
-
|
|
257
|
-
def discover_operations
|
|
258
|
-
self.class.wsdl_operations.dup
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
def infer_url
|
|
262
|
-
return @request.url if @request && @request.respond_to?(:url)
|
|
263
|
-
"/"
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# ── SOAP request processing ──────────────────────────────────────────
|
|
267
|
-
|
|
268
|
-
def process_soap(xml_body)
|
|
269
|
-
on_request(@request)
|
|
270
|
-
|
|
271
|
-
begin
|
|
272
|
-
doc = REXML::Document.new(xml_body)
|
|
273
|
-
rescue REXML::ParseException
|
|
274
|
-
return soap_fault("Client", "Malformed XML")
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
# Find the SOAP Body element (namespace-agnostic)
|
|
278
|
-
body_el = find_child(doc.root, "Body")
|
|
279
|
-
return soap_fault("Client", "Missing SOAP Body") unless body_el
|
|
280
|
-
|
|
281
|
-
# First child of Body is the operation element
|
|
282
|
-
op_el = body_el.elements.first
|
|
283
|
-
return soap_fault("Client", "Empty SOAP Body") unless op_el
|
|
284
|
-
|
|
285
|
-
op_name = local_name(op_el)
|
|
286
|
-
|
|
287
|
-
unless @operations.key?(op_name)
|
|
288
|
-
return soap_fault("Client", "Unknown operation: #{op_name}")
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
meta = @operations[op_name]
|
|
292
|
-
|
|
293
|
-
# Extract parameters from the operation element
|
|
294
|
-
params = {}
|
|
295
|
-
meta[:input].each do |param_name, param_type|
|
|
296
|
-
child = find_child(op_el, param_name.to_s)
|
|
297
|
-
if child
|
|
298
|
-
value = child.text || ""
|
|
299
|
-
params[param_name.to_s] = convert_value(value, param_type)
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
begin
|
|
304
|
-
result = send(op_name.to_sym, *meta[:input].keys.map { |k| params[k.to_s] })
|
|
305
|
-
result = on_result(result)
|
|
306
|
-
rescue StandardError => e
|
|
307
|
-
return soap_fault("Server", e.message)
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
soap_response(op_name, result)
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
# ── XML helpers (REXML) ──────────────────────────────────────────────
|
|
314
|
-
|
|
315
|
-
def find_child(parent, local)
|
|
316
|
-
parent.each_element do |el|
|
|
317
|
-
return el if local_name(el) == local
|
|
318
|
-
end
|
|
319
|
-
nil
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def local_name(element)
|
|
323
|
-
element.name # REXML already strips the prefix for .name
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
# ── Type conversion ──────────────────────────────────────────────────
|
|
327
|
-
|
|
328
|
-
def convert_value(value, target_type)
|
|
329
|
-
case target_type.to_s.downcase.to_sym
|
|
330
|
-
when :int, :integer
|
|
331
|
-
value.to_i
|
|
332
|
-
when :float, :double
|
|
333
|
-
value.to_f
|
|
334
|
-
when :boolean, :bool
|
|
335
|
-
%w[true 1 yes].include?(value.downcase)
|
|
336
|
-
else
|
|
337
|
-
value
|
|
338
|
-
end
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
# ── Response builders ────────────────────────────────────────────────
|
|
342
|
-
|
|
343
|
-
def soap_response(op_name, result)
|
|
344
|
-
parts = []
|
|
345
|
-
parts << '<?xml version="1.0" encoding="UTF-8"?>'
|
|
346
|
-
parts << "<soap:Envelope xmlns:soap=\"#{NS_SOAP_ENV}\">"
|
|
347
|
-
parts << "<soap:Body>"
|
|
348
|
-
parts << "<#{op_name}Response>"
|
|
349
|
-
|
|
350
|
-
if result.is_a?(Hash)
|
|
351
|
-
result.each do |k, v|
|
|
352
|
-
if v.nil?
|
|
353
|
-
parts << "<#{k} xsi:nil=\"true\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"/>"
|
|
354
|
-
elsif v.is_a?(Array)
|
|
355
|
-
v.each { |item| parts << "<#{k}>#{escape_xml(item.to_s)}</#{k}>" }
|
|
356
|
-
else
|
|
357
|
-
parts << "<#{k}>#{escape_xml(v.to_s)}</#{k}>"
|
|
358
|
-
end
|
|
359
|
-
end
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
parts << "</#{op_name}Response>"
|
|
363
|
-
parts << "</soap:Body>"
|
|
364
|
-
parts << "</soap:Envelope>"
|
|
365
|
-
parts.join("\n")
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
def soap_fault(code, message)
|
|
369
|
-
'<?xml version="1.0" encoding="UTF-8"?>' \
|
|
370
|
-
"<soap:Envelope xmlns:soap=\"#{NS_SOAP_ENV}\">" \
|
|
371
|
-
"<soap:Body>" \
|
|
372
|
-
"<soap:Fault>" \
|
|
373
|
-
"<faultcode>#{code}</faultcode>" \
|
|
374
|
-
"<faultstring>#{escape_xml(message)}</faultstring>" \
|
|
375
|
-
"</soap:Fault>" \
|
|
376
|
-
"</soap:Body>" \
|
|
377
|
-
"</soap:Envelope>"
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
def escape_xml(s)
|
|
381
|
-
s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
def xsd_type(ruby_type)
|
|
385
|
-
return RUBY_TO_XSD[ruby_type] if RUBY_TO_XSD.key?(ruby_type)
|
|
386
|
-
|
|
387
|
-
sym = ruby_type.to_s.downcase.to_sym
|
|
388
|
-
RUBY_TO_XSD.fetch(sym, "xsd:string")
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
# ── Legacy wrapper ───────────────────────────────────────────────────
|
|
392
|
-
# Keeps backward compatibility with the old Tina4::WSDL::Service API
|
|
393
|
-
# used in demos and existing code.
|
|
394
|
-
|
|
395
|
-
class Service
|
|
396
|
-
attr_reader :name, :namespace, :operations
|
|
397
|
-
|
|
398
|
-
def initialize(name:, namespace: "http://tina4.com/wsdl")
|
|
399
|
-
@name = name
|
|
400
|
-
@namespace = namespace
|
|
401
|
-
@operations = {}
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
def add_operation(name, input_params: {}, output_params: {}, &handler)
|
|
405
|
-
@operations[name.to_s] = {
|
|
406
|
-
input: input_params,
|
|
407
|
-
output: output_params,
|
|
408
|
-
handler: handler
|
|
409
|
-
}
|
|
410
|
-
end
|
|
411
|
-
|
|
412
|
-
def generate_wsdl(endpoint_url)
|
|
413
|
-
xml = '<?xml version="1.0" encoding="UTF-8"?>'
|
|
414
|
-
xml += "\n<definitions xmlns=\"http://schemas.xmlsoap.org/wsdl/\""
|
|
415
|
-
xml += " xmlns:soap=\"http://schemas.xmlsoap.org/wsdl/soap/\""
|
|
416
|
-
xml += " xmlns:tns=\"#{@namespace}\""
|
|
417
|
-
xml += " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\""
|
|
418
|
-
xml += " name=\"#{@name}\" targetNamespace=\"#{@namespace}\">\n"
|
|
419
|
-
|
|
420
|
-
# Types
|
|
421
|
-
xml += " <types>\n <xsd:schema targetNamespace=\"#{@namespace}\">\n"
|
|
422
|
-
@operations.each do |op_name, op|
|
|
423
|
-
xml += _generate_elements(op_name, op[:input], "Request")
|
|
424
|
-
xml += _generate_elements(op_name, op[:output], "Response")
|
|
425
|
-
end
|
|
426
|
-
xml += " </xsd:schema>\n </types>\n"
|
|
427
|
-
|
|
428
|
-
# Messages
|
|
429
|
-
@operations.each_key do |op_name|
|
|
430
|
-
xml += " <message name=\"#{op_name}Request\">\n"
|
|
431
|
-
xml += " <part name=\"parameters\" element=\"tns:#{op_name}Request\"/>\n"
|
|
432
|
-
xml += " </message>\n"
|
|
433
|
-
xml += " <message name=\"#{op_name}Response\">\n"
|
|
434
|
-
xml += " <part name=\"parameters\" element=\"tns:#{op_name}Response\"/>\n"
|
|
435
|
-
xml += " </message>\n"
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
# PortType
|
|
439
|
-
xml += " <portType name=\"#{@name}PortType\">\n"
|
|
440
|
-
@operations.each_key do |op_name|
|
|
441
|
-
xml += " <operation name=\"#{op_name}\">\n"
|
|
442
|
-
xml += " <input message=\"tns:#{op_name}Request\"/>\n"
|
|
443
|
-
xml += " <output message=\"tns:#{op_name}Response\"/>\n"
|
|
444
|
-
xml += " </operation>\n"
|
|
445
|
-
end
|
|
446
|
-
xml += " </portType>\n"
|
|
447
|
-
|
|
448
|
-
# Binding
|
|
449
|
-
xml += " <binding name=\"#{@name}Binding\" type=\"tns:#{@name}PortType\">\n"
|
|
450
|
-
xml += " <soap:binding style=\"document\" transport=\"http://schemas.xmlsoap.org/soap/http\"/>\n"
|
|
451
|
-
@operations.each_key do |op_name|
|
|
452
|
-
xml += " <operation name=\"#{op_name}\">\n"
|
|
453
|
-
xml += " <soap:operation soapAction=\"#{@namespace}/#{op_name}\"/>\n"
|
|
454
|
-
xml += " <input><soap:body use=\"literal\"/></input>\n"
|
|
455
|
-
xml += " <output><soap:body use=\"literal\"/></output>\n"
|
|
456
|
-
xml += " </operation>\n"
|
|
457
|
-
end
|
|
458
|
-
xml += " </binding>\n"
|
|
459
|
-
|
|
460
|
-
# Service
|
|
461
|
-
xml += " <service name=\"#{@name}\">\n"
|
|
462
|
-
xml += " <port name=\"#{@name}Port\" binding=\"tns:#{@name}Binding\">\n"
|
|
463
|
-
xml += " <soap:address location=\"#{endpoint_url}\"/>\n"
|
|
464
|
-
xml += " </port>\n"
|
|
465
|
-
xml += " </service>\n"
|
|
466
|
-
xml += "</definitions>"
|
|
467
|
-
xml
|
|
468
|
-
end
|
|
469
|
-
|
|
470
|
-
def handle_soap_request(xml_body)
|
|
471
|
-
doc = REXML::Document.new(xml_body)
|
|
472
|
-
|
|
473
|
-
# Find Body element (namespace-agnostic)
|
|
474
|
-
body_el = _find_child(doc.root, "Body")
|
|
475
|
-
return _soap_fault("Unknown operation") unless body_el
|
|
476
|
-
|
|
477
|
-
op_el = body_el.elements.first
|
|
478
|
-
return _soap_fault("Unknown operation") unless op_el
|
|
479
|
-
|
|
480
|
-
op_name = op_el.name
|
|
481
|
-
return _soap_fault("Unknown operation") unless @operations.key?(op_name)
|
|
482
|
-
|
|
483
|
-
operation = @operations[op_name]
|
|
484
|
-
|
|
485
|
-
# Extract parameters
|
|
486
|
-
params = {}
|
|
487
|
-
operation[:input].each_key do |param_name|
|
|
488
|
-
child = _find_child(op_el, param_name.to_s)
|
|
489
|
-
params[param_name.to_s] = child.text if child
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
# Execute handler
|
|
493
|
-
result = operation[:handler].call(params)
|
|
494
|
-
|
|
495
|
-
# Build SOAP response
|
|
496
|
-
_build_soap_response(op_name, result)
|
|
497
|
-
rescue StandardError => e
|
|
498
|
-
_soap_fault(e.message)
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
private
|
|
502
|
-
|
|
503
|
-
def _find_child(parent, local)
|
|
504
|
-
parent.each_element do |el|
|
|
505
|
-
return el if el.name == local
|
|
506
|
-
end
|
|
507
|
-
nil
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
def _generate_elements(op_name, params, suffix)
|
|
511
|
-
xml = " <xsd:element name=\"#{op_name}#{suffix}\">\n"
|
|
512
|
-
xml += " <xsd:complexType><xsd:sequence>\n"
|
|
513
|
-
params.each do |name, type|
|
|
514
|
-
xsd_type = _ruby_to_xsd_type(type)
|
|
515
|
-
xml += " <xsd:element name=\"#{name}\" type=\"xsd:#{xsd_type}\"/>\n"
|
|
516
|
-
end
|
|
517
|
-
xml += " </xsd:sequence></xsd:complexType>\n"
|
|
518
|
-
xml += " </xsd:element>\n"
|
|
519
|
-
xml
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
def _ruby_to_xsd_type(type)
|
|
523
|
-
case type.to_s.downcase
|
|
524
|
-
when "string" then "string"
|
|
525
|
-
when "integer", "int" then "int"
|
|
526
|
-
when "float", "double" then "double"
|
|
527
|
-
when "boolean", "bool" then "boolean"
|
|
528
|
-
when "date" then "date"
|
|
529
|
-
when "datetime" then "dateTime"
|
|
530
|
-
else "string"
|
|
531
|
-
end
|
|
532
|
-
end
|
|
533
|
-
|
|
534
|
-
def _build_soap_response(op_name, result)
|
|
535
|
-
xml = '<?xml version="1.0" encoding="UTF-8"?>'
|
|
536
|
-
xml += "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\""
|
|
537
|
-
xml += " xmlns:tns=\"#{@namespace}\">"
|
|
538
|
-
xml += "<soap:Body>"
|
|
539
|
-
xml += "<tns:#{op_name}Response>"
|
|
540
|
-
if result.is_a?(Hash)
|
|
541
|
-
result.each { |k, v| xml += "<#{k}>#{_escape_xml(v.to_s)}</#{k}>" }
|
|
542
|
-
else
|
|
543
|
-
xml += "<result>#{_escape_xml(result.to_s)}</result>"
|
|
544
|
-
end
|
|
545
|
-
xml += "</tns:#{op_name}Response>"
|
|
546
|
-
xml += "</soap:Body></soap:Envelope>"
|
|
547
|
-
xml
|
|
548
|
-
end
|
|
549
|
-
|
|
550
|
-
def _soap_fault(message)
|
|
551
|
-
'<?xml version="1.0" encoding="UTF-8"?>' \
|
|
552
|
-
'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' \
|
|
553
|
-
"<soap:Body><soap:Fault>" \
|
|
554
|
-
"<faultcode>soap:Server</faultcode>" \
|
|
555
|
-
"<faultstring>#{_escape_xml(message)}</faultstring>" \
|
|
556
|
-
"</soap:Fault></soap:Body></soap:Envelope>"
|
|
557
|
-
end
|
|
558
|
-
|
|
559
|
-
def _escape_xml(s)
|
|
560
|
-
s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
561
|
-
end
|
|
562
|
-
end
|
|
563
|
-
end
|
|
564
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rexml/document"
|
|
4
|
+
|
|
5
|
+
module Tina4
|
|
6
|
+
# SOAP 1.1 / WSDL server — zero-dependency, mirrors tina4-python's wsdl module.
|
|
7
|
+
#
|
|
8
|
+
# Usage (class-based):
|
|
9
|
+
#
|
|
10
|
+
# class Calculator < Tina4::WSDL
|
|
11
|
+
# wsdl_operation output: { Result: :int }
|
|
12
|
+
# def add(a, b)
|
|
13
|
+
# { Result: a.to_i + b.to_i }
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # In a route handler:
|
|
18
|
+
# service = Calculator.new(request)
|
|
19
|
+
# response.call(service.handle)
|
|
20
|
+
#
|
|
21
|
+
# Supported:
|
|
22
|
+
# - WSDL 1.1 generation from Ruby type declarations
|
|
23
|
+
# - SOAP 1.1 request/response handling via REXML
|
|
24
|
+
# - Lifecycle hooks (on_request, on_result)
|
|
25
|
+
# - Auto type mapping (Integer -> int, String -> string, Float -> double, etc.)
|
|
26
|
+
# - XML escaping on all response values
|
|
27
|
+
# - SOAP fault responses on errors
|
|
28
|
+
#
|
|
29
|
+
class WSDL
|
|
30
|
+
NS_SOAP = "http://schemas.xmlsoap.org/wsdl/soap/"
|
|
31
|
+
NS_WSDL = "http://schemas.xmlsoap.org/wsdl/"
|
|
32
|
+
NS_XSD = "http://www.w3.org/2001/XMLSchema"
|
|
33
|
+
NS_SOAP_ENV = "http://schemas.xmlsoap.org/soap/envelope/"
|
|
34
|
+
|
|
35
|
+
RUBY_TO_XSD = {
|
|
36
|
+
:int => "xsd:int",
|
|
37
|
+
:integer => "xsd:int",
|
|
38
|
+
:string => "xsd:string",
|
|
39
|
+
:float => "xsd:double",
|
|
40
|
+
:double => "xsd:double",
|
|
41
|
+
:boolean => "xsd:boolean",
|
|
42
|
+
:bool => "xsd:boolean",
|
|
43
|
+
:date => "xsd:date",
|
|
44
|
+
:datetime => "xsd:dateTime",
|
|
45
|
+
:base64 => "xsd:base64Binary",
|
|
46
|
+
Integer => "xsd:int",
|
|
47
|
+
String => "xsd:string",
|
|
48
|
+
Float => "xsd:double",
|
|
49
|
+
TrueClass => "xsd:boolean",
|
|
50
|
+
FalseClass => "xsd:boolean"
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
# ── Class-level DSL ──────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
class << self
|
|
56
|
+
# Registry of operations declared via wsdl_operation + def.
|
|
57
|
+
# Each entry: { input: { name => type, ... }, output: { name => type, ... } }
|
|
58
|
+
def wsdl_operations
|
|
59
|
+
@wsdl_operations ||= {}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Pending output hash waiting for the next method definition.
|
|
63
|
+
def pending_wsdl_output
|
|
64
|
+
@pending_wsdl_output
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Mark the next defined method as a WSDL operation.
|
|
68
|
+
#
|
|
69
|
+
# wsdl_operation output: { Result: :int }
|
|
70
|
+
# def add(a, b) ...
|
|
71
|
+
#
|
|
72
|
+
# Input parameters are inferred from the method signature.
|
|
73
|
+
# The +output+ hash maps response element names to XSD type symbols.
|
|
74
|
+
def wsdl_operation(output: {})
|
|
75
|
+
@pending_wsdl_output = output
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Hook into method definition to capture the pending operation.
|
|
79
|
+
def method_added(method_name)
|
|
80
|
+
super
|
|
81
|
+
return unless @pending_wsdl_output
|
|
82
|
+
|
|
83
|
+
output = @pending_wsdl_output
|
|
84
|
+
@pending_wsdl_output = nil
|
|
85
|
+
|
|
86
|
+
# Infer input parameter names from the method signature.
|
|
87
|
+
params = instance_method(method_name).parameters
|
|
88
|
+
input = {}
|
|
89
|
+
params.each do |_kind, name|
|
|
90
|
+
next if name.nil?
|
|
91
|
+
input[name] = :string # default; callers can rely on type coercion
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
wsdl_operations[method_name.to_s] = { input: input, output: output }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Ensure subclasses get their own copy of the operations registry.
|
|
98
|
+
def inherited(subclass)
|
|
99
|
+
super
|
|
100
|
+
subclass.instance_variable_set(:@wsdl_operations, wsdl_operations.dup)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# ── Instance ─────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
attr_reader :request, :service_url
|
|
107
|
+
|
|
108
|
+
def initialize(request = nil, service_url: "")
|
|
109
|
+
@request = request
|
|
110
|
+
@service_url = service_url.empty? ? infer_url : service_url
|
|
111
|
+
@operations = discover_operations
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Main entry point. Returns WSDL XML on GET/?wsdl, or processes a SOAP
|
|
115
|
+
# request on POST.
|
|
116
|
+
def handle
|
|
117
|
+
return generate_wsdl if @request.nil?
|
|
118
|
+
|
|
119
|
+
method = if @request.respond_to?(:method)
|
|
120
|
+
@request.method.to_s.upcase
|
|
121
|
+
elsif @request.respond_to?(:body) && @request.body && !@request.body.to_s.empty?
|
|
122
|
+
"POST"
|
|
123
|
+
else
|
|
124
|
+
"GET"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
params = (@request.respond_to?(:params) ? @request.params : nil) || {}
|
|
128
|
+
url = (@request.respond_to?(:url) ? @request.url : nil) || ""
|
|
129
|
+
|
|
130
|
+
if method == "GET" || params.key?("wsdl") || params.key?(:wsdl) || url.end_with?("?wsdl")
|
|
131
|
+
return generate_wsdl
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
body = if @request.respond_to?(:body)
|
|
135
|
+
@request.body.is_a?(String) ? @request.body : @request.body.to_s
|
|
136
|
+
else
|
|
137
|
+
""
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
process_soap(body)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# ── Lifecycle hooks (override in subclass) ───────────────────────────
|
|
144
|
+
|
|
145
|
+
# Called before operation invocation. Override to validate/log.
|
|
146
|
+
def on_request(request)
|
|
147
|
+
# no-op
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Called after operation returns. Override to transform/audit.
|
|
151
|
+
# Must return the (possibly modified) result.
|
|
152
|
+
def on_result(result)
|
|
153
|
+
result
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# ── WSDL generation ──────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
def generate_wsdl(endpoint_url = "")
|
|
159
|
+
@service_url = endpoint_url unless endpoint_url.empty?
|
|
160
|
+
service_name = self.class.name ? self.class.name.split("::").last : "AnonymousService"
|
|
161
|
+
tns = "urn:#{service_name}"
|
|
162
|
+
|
|
163
|
+
parts = []
|
|
164
|
+
parts << '<?xml version="1.0" encoding="UTF-8"?>'
|
|
165
|
+
parts << "<definitions name=\"#{service_name}\""
|
|
166
|
+
parts << " targetNamespace=\"#{tns}\""
|
|
167
|
+
parts << " xmlns:tns=\"#{tns}\""
|
|
168
|
+
parts << " xmlns:soap=\"#{NS_SOAP}\""
|
|
169
|
+
parts << " xmlns:xsd=\"#{NS_XSD}\""
|
|
170
|
+
parts << " xmlns=\"#{NS_WSDL}\">"
|
|
171
|
+
parts << ""
|
|
172
|
+
|
|
173
|
+
# Types
|
|
174
|
+
parts << " <types>"
|
|
175
|
+
parts << " <xsd:schema targetNamespace=\"#{tns}\">"
|
|
176
|
+
|
|
177
|
+
@operations.each do |op_name, meta|
|
|
178
|
+
# Request element
|
|
179
|
+
parts << " <xsd:element name=\"#{op_name}\">"
|
|
180
|
+
parts << " <xsd:complexType>"
|
|
181
|
+
parts << " <xsd:sequence>"
|
|
182
|
+
meta[:input].each do |pname, ptype|
|
|
183
|
+
xsd = xsd_type(ptype)
|
|
184
|
+
parts << " <xsd:element name=\"#{pname}\" type=\"#{xsd}\"/>"
|
|
185
|
+
end
|
|
186
|
+
parts << " </xsd:sequence>"
|
|
187
|
+
parts << " </xsd:complexType>"
|
|
188
|
+
parts << " </xsd:element>"
|
|
189
|
+
|
|
190
|
+
# Response element
|
|
191
|
+
parts << " <xsd:element name=\"#{op_name}Response\">"
|
|
192
|
+
parts << " <xsd:complexType>"
|
|
193
|
+
parts << " <xsd:sequence>"
|
|
194
|
+
meta[:output].each do |rname, rtype|
|
|
195
|
+
xsd = xsd_type(rtype)
|
|
196
|
+
parts << " <xsd:element name=\"#{rname}\" type=\"#{xsd}\"/>"
|
|
197
|
+
end
|
|
198
|
+
parts << " </xsd:sequence>"
|
|
199
|
+
parts << " </xsd:complexType>"
|
|
200
|
+
parts << " </xsd:element>"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
parts << " </xsd:schema>"
|
|
204
|
+
parts << " </types>"
|
|
205
|
+
parts << ""
|
|
206
|
+
|
|
207
|
+
# Messages
|
|
208
|
+
@operations.each_key do |op_name|
|
|
209
|
+
parts << " <message name=\"#{op_name}Input\">"
|
|
210
|
+
parts << " <part name=\"parameters\" element=\"tns:#{op_name}\"/>"
|
|
211
|
+
parts << " </message>"
|
|
212
|
+
parts << " <message name=\"#{op_name}Output\">"
|
|
213
|
+
parts << " <part name=\"parameters\" element=\"tns:#{op_name}Response\"/>"
|
|
214
|
+
parts << " </message>"
|
|
215
|
+
end
|
|
216
|
+
parts << ""
|
|
217
|
+
|
|
218
|
+
# PortType
|
|
219
|
+
parts << " <portType name=\"#{service_name}PortType\">"
|
|
220
|
+
@operations.each_key do |op_name|
|
|
221
|
+
parts << " <operation name=\"#{op_name}\">"
|
|
222
|
+
parts << " <input message=\"tns:#{op_name}Input\"/>"
|
|
223
|
+
parts << " <output message=\"tns:#{op_name}Output\"/>"
|
|
224
|
+
parts << " </operation>"
|
|
225
|
+
end
|
|
226
|
+
parts << " </portType>"
|
|
227
|
+
parts << ""
|
|
228
|
+
|
|
229
|
+
# Binding
|
|
230
|
+
parts << " <binding name=\"#{service_name}Binding\" type=\"tns:#{service_name}PortType\">"
|
|
231
|
+
parts << " <soap:binding style=\"document\" transport=\"http://schemas.xmlsoap.org/soap/http\"/>"
|
|
232
|
+
@operations.each_key do |op_name|
|
|
233
|
+
parts << " <operation name=\"#{op_name}\">"
|
|
234
|
+
parts << " <soap:operation soapAction=\"#{tns}/#{op_name}\"/>"
|
|
235
|
+
parts << ' <input><soap:body use="literal"/></input>'
|
|
236
|
+
parts << ' <output><soap:body use="literal"/></output>'
|
|
237
|
+
parts << " </operation>"
|
|
238
|
+
end
|
|
239
|
+
parts << " </binding>"
|
|
240
|
+
parts << ""
|
|
241
|
+
|
|
242
|
+
# Service
|
|
243
|
+
parts << " <service name=\"#{service_name}\">"
|
|
244
|
+
parts << " <port name=\"#{service_name}Port\" binding=\"tns:#{service_name}Binding\">"
|
|
245
|
+
parts << " <soap:address location=\"#{@service_url}\"/>"
|
|
246
|
+
parts << " </port>"
|
|
247
|
+
parts << " </service>"
|
|
248
|
+
|
|
249
|
+
parts << "</definitions>"
|
|
250
|
+
parts.join("\n")
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
# ── Auto-discovery ───────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
def discover_operations
|
|
258
|
+
self.class.wsdl_operations.dup
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def infer_url
|
|
262
|
+
return @request.url if @request && @request.respond_to?(:url)
|
|
263
|
+
"/"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# ── SOAP request processing ──────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
def process_soap(xml_body)
|
|
269
|
+
on_request(@request)
|
|
270
|
+
|
|
271
|
+
begin
|
|
272
|
+
doc = REXML::Document.new(xml_body)
|
|
273
|
+
rescue REXML::ParseException
|
|
274
|
+
return soap_fault("Client", "Malformed XML")
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Find the SOAP Body element (namespace-agnostic)
|
|
278
|
+
body_el = find_child(doc.root, "Body")
|
|
279
|
+
return soap_fault("Client", "Missing SOAP Body") unless body_el
|
|
280
|
+
|
|
281
|
+
# First child of Body is the operation element
|
|
282
|
+
op_el = body_el.elements.first
|
|
283
|
+
return soap_fault("Client", "Empty SOAP Body") unless op_el
|
|
284
|
+
|
|
285
|
+
op_name = local_name(op_el)
|
|
286
|
+
|
|
287
|
+
unless @operations.key?(op_name)
|
|
288
|
+
return soap_fault("Client", "Unknown operation: #{op_name}")
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
meta = @operations[op_name]
|
|
292
|
+
|
|
293
|
+
# Extract parameters from the operation element
|
|
294
|
+
params = {}
|
|
295
|
+
meta[:input].each do |param_name, param_type|
|
|
296
|
+
child = find_child(op_el, param_name.to_s)
|
|
297
|
+
if child
|
|
298
|
+
value = child.text || ""
|
|
299
|
+
params[param_name.to_s] = convert_value(value, param_type)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
begin
|
|
304
|
+
result = send(op_name.to_sym, *meta[:input].keys.map { |k| params[k.to_s] })
|
|
305
|
+
result = on_result(result)
|
|
306
|
+
rescue StandardError => e
|
|
307
|
+
return soap_fault("Server", e.message)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
soap_response(op_name, result)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# ── XML helpers (REXML) ──────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
def find_child(parent, local)
|
|
316
|
+
parent.each_element do |el|
|
|
317
|
+
return el if local_name(el) == local
|
|
318
|
+
end
|
|
319
|
+
nil
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def local_name(element)
|
|
323
|
+
element.name # REXML already strips the prefix for .name
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# ── Type conversion ──────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
def convert_value(value, target_type)
|
|
329
|
+
case target_type.to_s.downcase.to_sym
|
|
330
|
+
when :int, :integer
|
|
331
|
+
value.to_i
|
|
332
|
+
when :float, :double
|
|
333
|
+
value.to_f
|
|
334
|
+
when :boolean, :bool
|
|
335
|
+
%w[true 1 yes].include?(value.downcase)
|
|
336
|
+
else
|
|
337
|
+
value
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# ── Response builders ────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
def soap_response(op_name, result)
|
|
344
|
+
parts = []
|
|
345
|
+
parts << '<?xml version="1.0" encoding="UTF-8"?>'
|
|
346
|
+
parts << "<soap:Envelope xmlns:soap=\"#{NS_SOAP_ENV}\">"
|
|
347
|
+
parts << "<soap:Body>"
|
|
348
|
+
parts << "<#{op_name}Response>"
|
|
349
|
+
|
|
350
|
+
if result.is_a?(Hash)
|
|
351
|
+
result.each do |k, v|
|
|
352
|
+
if v.nil?
|
|
353
|
+
parts << "<#{k} xsi:nil=\"true\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"/>"
|
|
354
|
+
elsif v.is_a?(Array)
|
|
355
|
+
v.each { |item| parts << "<#{k}>#{escape_xml(item.to_s)}</#{k}>" }
|
|
356
|
+
else
|
|
357
|
+
parts << "<#{k}>#{escape_xml(v.to_s)}</#{k}>"
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
parts << "</#{op_name}Response>"
|
|
363
|
+
parts << "</soap:Body>"
|
|
364
|
+
parts << "</soap:Envelope>"
|
|
365
|
+
parts.join("\n")
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def soap_fault(code, message)
|
|
369
|
+
'<?xml version="1.0" encoding="UTF-8"?>' \
|
|
370
|
+
"<soap:Envelope xmlns:soap=\"#{NS_SOAP_ENV}\">" \
|
|
371
|
+
"<soap:Body>" \
|
|
372
|
+
"<soap:Fault>" \
|
|
373
|
+
"<faultcode>#{code}</faultcode>" \
|
|
374
|
+
"<faultstring>#{escape_xml(message)}</faultstring>" \
|
|
375
|
+
"</soap:Fault>" \
|
|
376
|
+
"</soap:Body>" \
|
|
377
|
+
"</soap:Envelope>"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def escape_xml(s)
|
|
381
|
+
s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def xsd_type(ruby_type)
|
|
385
|
+
return RUBY_TO_XSD[ruby_type] if RUBY_TO_XSD.key?(ruby_type)
|
|
386
|
+
|
|
387
|
+
sym = ruby_type.to_s.downcase.to_sym
|
|
388
|
+
RUBY_TO_XSD.fetch(sym, "xsd:string")
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# ── Legacy wrapper ───────────────────────────────────────────────────
|
|
392
|
+
# Keeps backward compatibility with the old Tina4::WSDL::Service API
|
|
393
|
+
# used in demos and existing code.
|
|
394
|
+
|
|
395
|
+
class Service
|
|
396
|
+
attr_reader :name, :namespace, :operations
|
|
397
|
+
|
|
398
|
+
def initialize(name:, namespace: "http://tina4.com/wsdl")
|
|
399
|
+
@name = name
|
|
400
|
+
@namespace = namespace
|
|
401
|
+
@operations = {}
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def add_operation(name, input_params: {}, output_params: {}, &handler)
|
|
405
|
+
@operations[name.to_s] = {
|
|
406
|
+
input: input_params,
|
|
407
|
+
output: output_params,
|
|
408
|
+
handler: handler
|
|
409
|
+
}
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def generate_wsdl(endpoint_url)
|
|
413
|
+
xml = '<?xml version="1.0" encoding="UTF-8"?>'
|
|
414
|
+
xml += "\n<definitions xmlns=\"http://schemas.xmlsoap.org/wsdl/\""
|
|
415
|
+
xml += " xmlns:soap=\"http://schemas.xmlsoap.org/wsdl/soap/\""
|
|
416
|
+
xml += " xmlns:tns=\"#{@namespace}\""
|
|
417
|
+
xml += " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\""
|
|
418
|
+
xml += " name=\"#{@name}\" targetNamespace=\"#{@namespace}\">\n"
|
|
419
|
+
|
|
420
|
+
# Types
|
|
421
|
+
xml += " <types>\n <xsd:schema targetNamespace=\"#{@namespace}\">\n"
|
|
422
|
+
@operations.each do |op_name, op|
|
|
423
|
+
xml += _generate_elements(op_name, op[:input], "Request")
|
|
424
|
+
xml += _generate_elements(op_name, op[:output], "Response")
|
|
425
|
+
end
|
|
426
|
+
xml += " </xsd:schema>\n </types>\n"
|
|
427
|
+
|
|
428
|
+
# Messages
|
|
429
|
+
@operations.each_key do |op_name|
|
|
430
|
+
xml += " <message name=\"#{op_name}Request\">\n"
|
|
431
|
+
xml += " <part name=\"parameters\" element=\"tns:#{op_name}Request\"/>\n"
|
|
432
|
+
xml += " </message>\n"
|
|
433
|
+
xml += " <message name=\"#{op_name}Response\">\n"
|
|
434
|
+
xml += " <part name=\"parameters\" element=\"tns:#{op_name}Response\"/>\n"
|
|
435
|
+
xml += " </message>\n"
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# PortType
|
|
439
|
+
xml += " <portType name=\"#{@name}PortType\">\n"
|
|
440
|
+
@operations.each_key do |op_name|
|
|
441
|
+
xml += " <operation name=\"#{op_name}\">\n"
|
|
442
|
+
xml += " <input message=\"tns:#{op_name}Request\"/>\n"
|
|
443
|
+
xml += " <output message=\"tns:#{op_name}Response\"/>\n"
|
|
444
|
+
xml += " </operation>\n"
|
|
445
|
+
end
|
|
446
|
+
xml += " </portType>\n"
|
|
447
|
+
|
|
448
|
+
# Binding
|
|
449
|
+
xml += " <binding name=\"#{@name}Binding\" type=\"tns:#{@name}PortType\">\n"
|
|
450
|
+
xml += " <soap:binding style=\"document\" transport=\"http://schemas.xmlsoap.org/soap/http\"/>\n"
|
|
451
|
+
@operations.each_key do |op_name|
|
|
452
|
+
xml += " <operation name=\"#{op_name}\">\n"
|
|
453
|
+
xml += " <soap:operation soapAction=\"#{@namespace}/#{op_name}\"/>\n"
|
|
454
|
+
xml += " <input><soap:body use=\"literal\"/></input>\n"
|
|
455
|
+
xml += " <output><soap:body use=\"literal\"/></output>\n"
|
|
456
|
+
xml += " </operation>\n"
|
|
457
|
+
end
|
|
458
|
+
xml += " </binding>\n"
|
|
459
|
+
|
|
460
|
+
# Service
|
|
461
|
+
xml += " <service name=\"#{@name}\">\n"
|
|
462
|
+
xml += " <port name=\"#{@name}Port\" binding=\"tns:#{@name}Binding\">\n"
|
|
463
|
+
xml += " <soap:address location=\"#{endpoint_url}\"/>\n"
|
|
464
|
+
xml += " </port>\n"
|
|
465
|
+
xml += " </service>\n"
|
|
466
|
+
xml += "</definitions>"
|
|
467
|
+
xml
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def handle_soap_request(xml_body)
|
|
471
|
+
doc = REXML::Document.new(xml_body)
|
|
472
|
+
|
|
473
|
+
# Find Body element (namespace-agnostic)
|
|
474
|
+
body_el = _find_child(doc.root, "Body")
|
|
475
|
+
return _soap_fault("Unknown operation") unless body_el
|
|
476
|
+
|
|
477
|
+
op_el = body_el.elements.first
|
|
478
|
+
return _soap_fault("Unknown operation") unless op_el
|
|
479
|
+
|
|
480
|
+
op_name = op_el.name
|
|
481
|
+
return _soap_fault("Unknown operation") unless @operations.key?(op_name)
|
|
482
|
+
|
|
483
|
+
operation = @operations[op_name]
|
|
484
|
+
|
|
485
|
+
# Extract parameters
|
|
486
|
+
params = {}
|
|
487
|
+
operation[:input].each_key do |param_name|
|
|
488
|
+
child = _find_child(op_el, param_name.to_s)
|
|
489
|
+
params[param_name.to_s] = child.text if child
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Execute handler
|
|
493
|
+
result = operation[:handler].call(params)
|
|
494
|
+
|
|
495
|
+
# Build SOAP response
|
|
496
|
+
_build_soap_response(op_name, result)
|
|
497
|
+
rescue StandardError => e
|
|
498
|
+
_soap_fault(e.message)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
private
|
|
502
|
+
|
|
503
|
+
def _find_child(parent, local)
|
|
504
|
+
parent.each_element do |el|
|
|
505
|
+
return el if el.name == local
|
|
506
|
+
end
|
|
507
|
+
nil
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def _generate_elements(op_name, params, suffix)
|
|
511
|
+
xml = " <xsd:element name=\"#{op_name}#{suffix}\">\n"
|
|
512
|
+
xml += " <xsd:complexType><xsd:sequence>\n"
|
|
513
|
+
params.each do |name, type|
|
|
514
|
+
xsd_type = _ruby_to_xsd_type(type)
|
|
515
|
+
xml += " <xsd:element name=\"#{name}\" type=\"xsd:#{xsd_type}\"/>\n"
|
|
516
|
+
end
|
|
517
|
+
xml += " </xsd:sequence></xsd:complexType>\n"
|
|
518
|
+
xml += " </xsd:element>\n"
|
|
519
|
+
xml
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def _ruby_to_xsd_type(type)
|
|
523
|
+
case type.to_s.downcase
|
|
524
|
+
when "string" then "string"
|
|
525
|
+
when "integer", "int" then "int"
|
|
526
|
+
when "float", "double" then "double"
|
|
527
|
+
when "boolean", "bool" then "boolean"
|
|
528
|
+
when "date" then "date"
|
|
529
|
+
when "datetime" then "dateTime"
|
|
530
|
+
else "string"
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def _build_soap_response(op_name, result)
|
|
535
|
+
xml = '<?xml version="1.0" encoding="UTF-8"?>'
|
|
536
|
+
xml += "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\""
|
|
537
|
+
xml += " xmlns:tns=\"#{@namespace}\">"
|
|
538
|
+
xml += "<soap:Body>"
|
|
539
|
+
xml += "<tns:#{op_name}Response>"
|
|
540
|
+
if result.is_a?(Hash)
|
|
541
|
+
result.each { |k, v| xml += "<#{k}>#{_escape_xml(v.to_s)}</#{k}>" }
|
|
542
|
+
else
|
|
543
|
+
xml += "<result>#{_escape_xml(result.to_s)}</result>"
|
|
544
|
+
end
|
|
545
|
+
xml += "</tns:#{op_name}Response>"
|
|
546
|
+
xml += "</soap:Body></soap:Envelope>"
|
|
547
|
+
xml
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def _soap_fault(message)
|
|
551
|
+
'<?xml version="1.0" encoding="UTF-8"?>' \
|
|
552
|
+
'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' \
|
|
553
|
+
"<soap:Body><soap:Fault>" \
|
|
554
|
+
"<faultcode>soap:Server</faultcode>" \
|
|
555
|
+
"<faultstring>#{_escape_xml(message)}</faultstring>" \
|
|
556
|
+
"</soap:Fault></soap:Body></soap:Envelope>"
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def _escape_xml(s)
|
|
560
|
+
s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
end
|