tina4ruby 3.10.5 → 3.10.10
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/lib/tina4/migration.rb +46 -14
- data/lib/tina4/template.rb +26 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/wsdl.rb +430 -31
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3001c964ea4a136f0dcc9af6766c2de0f98f323f8b9ff93deaab922477967c28
|
|
4
|
+
data.tar.gz: 2abc2dfa785de1cb2a503902cf009501df413af82e9054fc6d044cb4d689525a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d86f3ba17e99a972bdeee8d25a8afec34f12a89ab68ae9be21a5f1ed9bae6e3fa58b69083a8db25ee1ce17bfd1dc47790439f54196f848f6e95df0332d343eb1
|
|
7
|
+
data.tar.gz: '08ded077a54c8ed7c044878ccce8ba7aa099c855be82a3dcc93568ee7ab7eb32f429ae2877872e97754ead753cc2cb4ccc8a990b94703e77787163e20b30e4db'
|
data/lib/tina4/migration.rb
CHANGED
|
@@ -103,16 +103,36 @@ module Tina4
|
|
|
103
103
|
|
|
104
104
|
def ensure_tracking_table
|
|
105
105
|
unless @db.table_exists?(TRACKING_TABLE)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
|
|
106
|
+
if firebird?
|
|
107
|
+
# Firebird: no AUTOINCREMENT, no TEXT type, use generator for IDs
|
|
108
|
+
begin
|
|
109
|
+
@db.execute("CREATE GENERATOR GEN_TINA4_MIGRATION_ID")
|
|
110
|
+
@db.execute("COMMIT") rescue nil
|
|
111
|
+
rescue
|
|
112
|
+
# Generator may already exist
|
|
113
|
+
end
|
|
114
|
+
@db.execute(<<~SQL)
|
|
115
|
+
CREATE TABLE #{TRACKING_TABLE} (
|
|
116
|
+
id INTEGER NOT NULL PRIMARY KEY,
|
|
117
|
+
migration_name VARCHAR(500) NOT NULL,
|
|
118
|
+
description VARCHAR(500) DEFAULT '',
|
|
119
|
+
batch INTEGER NOT NULL DEFAULT 1,
|
|
120
|
+
executed_at VARCHAR(50) DEFAULT CURRENT_TIMESTAMP,
|
|
121
|
+
passed INTEGER NOT NULL DEFAULT 1
|
|
122
|
+
)
|
|
123
|
+
SQL
|
|
124
|
+
else
|
|
125
|
+
@db.execute(<<~SQL)
|
|
126
|
+
CREATE TABLE #{TRACKING_TABLE} (
|
|
127
|
+
id INTEGER PRIMARY KEY,
|
|
128
|
+
migration_name VARCHAR(255) NOT NULL,
|
|
129
|
+
description VARCHAR(255) DEFAULT '',
|
|
130
|
+
batch INTEGER NOT NULL DEFAULT 1,
|
|
131
|
+
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
132
|
+
passed INTEGER NOT NULL DEFAULT 1
|
|
133
|
+
)
|
|
134
|
+
SQL
|
|
135
|
+
end
|
|
116
136
|
Tina4::Log.info("Created migrations tracking table")
|
|
117
137
|
end
|
|
118
138
|
end
|
|
@@ -309,10 +329,22 @@ module Tina4
|
|
|
309
329
|
# Extract description from filename (strip numeric prefix and extension)
|
|
310
330
|
stem = File.basename(name, File.extname(name))
|
|
311
331
|
desc = stem.sub(/\A\d+_/, "").tr("_", " ")
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
332
|
+
if firebird?
|
|
333
|
+
# Firebird: generate ID from sequence
|
|
334
|
+
row = @db.fetch_one(
|
|
335
|
+
"SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS NEXT_ID FROM RDB\$DATABASE"
|
|
336
|
+
)
|
|
337
|
+
next_id = row ? (row[:NEXT_ID] || row[:next_id] || 1).to_i : 1
|
|
338
|
+
@db.execute(
|
|
339
|
+
"INSERT INTO #{TRACKING_TABLE} (id, migration_name, description, batch, passed) VALUES (?, ?, ?, ?, ?)",
|
|
340
|
+
[next_id, name, desc, batch, passed]
|
|
341
|
+
)
|
|
342
|
+
else
|
|
343
|
+
@db.execute(
|
|
344
|
+
"INSERT INTO #{TRACKING_TABLE} (migration_name, description, batch, passed) VALUES (?, ?, ?, ?)",
|
|
345
|
+
[name, desc, batch, passed]
|
|
346
|
+
)
|
|
347
|
+
end
|
|
316
348
|
end
|
|
317
349
|
|
|
318
350
|
def remove_migration_record(name)
|
data/lib/tina4/template.rb
CHANGED
|
@@ -488,6 +488,32 @@ module Tina4
|
|
|
488
488
|
right = evaluate_expression(Regexp.last_match(3))
|
|
489
489
|
return apply_math(left, op, right)
|
|
490
490
|
end
|
|
491
|
+
|
|
492
|
+
# Function call with dotted name: obj.method(args)
|
|
493
|
+
if expr =~ /\A([\w.]+)\s*\((.*)\)\z/m
|
|
494
|
+
func_name = Regexp.last_match(1)
|
|
495
|
+
args_str = Regexp.last_match(2)
|
|
496
|
+
if func_name.include?(".")
|
|
497
|
+
last_dot = func_name.rindex(".")
|
|
498
|
+
obj_path = func_name[0...last_dot]
|
|
499
|
+
method_name = func_name[(last_dot + 1)..]
|
|
500
|
+
obj = resolve_variable(obj_path)
|
|
501
|
+
if obj.respond_to?(:call)
|
|
502
|
+
# obj itself is callable — unlikely but handle
|
|
503
|
+
elsif obj.is_a?(Hash)
|
|
504
|
+
callable = obj[method_name] || obj[method_name.to_sym] || obj[method_name.to_s]
|
|
505
|
+
if callable.respond_to?(:call)
|
|
506
|
+
args = args_str && !args_str.strip.empty? ? parse_filter_args(args_str) : []
|
|
507
|
+
return callable.call(*args)
|
|
508
|
+
end
|
|
509
|
+
elsif obj.respond_to?(method_name.to_sym)
|
|
510
|
+
args = args_str && !args_str.strip.empty? ? parse_filter_args(args_str) : []
|
|
511
|
+
return obj.send(method_name.to_sym, *args)
|
|
512
|
+
end
|
|
513
|
+
return nil
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
491
517
|
resolve_variable(expr)
|
|
492
518
|
end
|
|
493
519
|
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4/wsdl.rb
CHANGED
|
@@ -1,13 +1,402 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rexml/document"
|
|
4
|
+
|
|
3
5
|
module Tina4
|
|
4
|
-
|
|
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
|
|
159
|
+
service_name = self.class.name ? self.class.name.split("::").last : "AnonymousService"
|
|
160
|
+
tns = "urn:#{service_name}"
|
|
161
|
+
|
|
162
|
+
parts = []
|
|
163
|
+
parts << '<?xml version="1.0" encoding="UTF-8"?>'
|
|
164
|
+
parts << "<definitions name=\"#{service_name}\""
|
|
165
|
+
parts << " targetNamespace=\"#{tns}\""
|
|
166
|
+
parts << " xmlns:tns=\"#{tns}\""
|
|
167
|
+
parts << " xmlns:soap=\"#{NS_SOAP}\""
|
|
168
|
+
parts << " xmlns:xsd=\"#{NS_XSD}\""
|
|
169
|
+
parts << " xmlns=\"#{NS_WSDL}\">"
|
|
170
|
+
parts << ""
|
|
171
|
+
|
|
172
|
+
# Types
|
|
173
|
+
parts << " <types>"
|
|
174
|
+
parts << " <xsd:schema targetNamespace=\"#{tns}\">"
|
|
175
|
+
|
|
176
|
+
@operations.each do |op_name, meta|
|
|
177
|
+
# Request element
|
|
178
|
+
parts << " <xsd:element name=\"#{op_name}\">"
|
|
179
|
+
parts << " <xsd:complexType>"
|
|
180
|
+
parts << " <xsd:sequence>"
|
|
181
|
+
meta[:input].each do |pname, ptype|
|
|
182
|
+
xsd = xsd_type(ptype)
|
|
183
|
+
parts << " <xsd:element name=\"#{pname}\" type=\"#{xsd}\"/>"
|
|
184
|
+
end
|
|
185
|
+
parts << " </xsd:sequence>"
|
|
186
|
+
parts << " </xsd:complexType>"
|
|
187
|
+
parts << " </xsd:element>"
|
|
188
|
+
|
|
189
|
+
# Response element
|
|
190
|
+
parts << " <xsd:element name=\"#{op_name}Response\">"
|
|
191
|
+
parts << " <xsd:complexType>"
|
|
192
|
+
parts << " <xsd:sequence>"
|
|
193
|
+
meta[:output].each do |rname, rtype|
|
|
194
|
+
xsd = xsd_type(rtype)
|
|
195
|
+
parts << " <xsd:element name=\"#{rname}\" type=\"#{xsd}\"/>"
|
|
196
|
+
end
|
|
197
|
+
parts << " </xsd:sequence>"
|
|
198
|
+
parts << " </xsd:complexType>"
|
|
199
|
+
parts << " </xsd:element>"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
parts << " </xsd:schema>"
|
|
203
|
+
parts << " </types>"
|
|
204
|
+
parts << ""
|
|
205
|
+
|
|
206
|
+
# Messages
|
|
207
|
+
@operations.each_key do |op_name|
|
|
208
|
+
parts << " <message name=\"#{op_name}Input\">"
|
|
209
|
+
parts << " <part name=\"parameters\" element=\"tns:#{op_name}\"/>"
|
|
210
|
+
parts << " </message>"
|
|
211
|
+
parts << " <message name=\"#{op_name}Output\">"
|
|
212
|
+
parts << " <part name=\"parameters\" element=\"tns:#{op_name}Response\"/>"
|
|
213
|
+
parts << " </message>"
|
|
214
|
+
end
|
|
215
|
+
parts << ""
|
|
216
|
+
|
|
217
|
+
# PortType
|
|
218
|
+
parts << " <portType name=\"#{service_name}PortType\">"
|
|
219
|
+
@operations.each_key do |op_name|
|
|
220
|
+
parts << " <operation name=\"#{op_name}\">"
|
|
221
|
+
parts << " <input message=\"tns:#{op_name}Input\"/>"
|
|
222
|
+
parts << " <output message=\"tns:#{op_name}Output\"/>"
|
|
223
|
+
parts << " </operation>"
|
|
224
|
+
end
|
|
225
|
+
parts << " </portType>"
|
|
226
|
+
parts << ""
|
|
227
|
+
|
|
228
|
+
# Binding
|
|
229
|
+
parts << " <binding name=\"#{service_name}Binding\" type=\"tns:#{service_name}PortType\">"
|
|
230
|
+
parts << " <soap:binding style=\"document\" transport=\"http://schemas.xmlsoap.org/soap/http\"/>"
|
|
231
|
+
@operations.each_key do |op_name|
|
|
232
|
+
parts << " <operation name=\"#{op_name}\">"
|
|
233
|
+
parts << " <soap:operation soapAction=\"#{tns}/#{op_name}\"/>"
|
|
234
|
+
parts << ' <input><soap:body use="literal"/></input>'
|
|
235
|
+
parts << ' <output><soap:body use="literal"/></output>'
|
|
236
|
+
parts << " </operation>"
|
|
237
|
+
end
|
|
238
|
+
parts << " </binding>"
|
|
239
|
+
parts << ""
|
|
240
|
+
|
|
241
|
+
# Service
|
|
242
|
+
parts << " <service name=\"#{service_name}\">"
|
|
243
|
+
parts << " <port name=\"#{service_name}Port\" binding=\"tns:#{service_name}Binding\">"
|
|
244
|
+
parts << " <soap:address location=\"#{@service_url}\"/>"
|
|
245
|
+
parts << " </port>"
|
|
246
|
+
parts << " </service>"
|
|
247
|
+
|
|
248
|
+
parts << "</definitions>"
|
|
249
|
+
parts.join("\n")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
# ── Auto-discovery ───────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
def discover_operations
|
|
257
|
+
self.class.wsdl_operations.dup
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def infer_url
|
|
261
|
+
return @request.url if @request && @request.respond_to?(:url)
|
|
262
|
+
"/"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# ── SOAP request processing ──────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
def process_soap(xml_body)
|
|
268
|
+
on_request(@request)
|
|
269
|
+
|
|
270
|
+
begin
|
|
271
|
+
doc = REXML::Document.new(xml_body)
|
|
272
|
+
rescue REXML::ParseException
|
|
273
|
+
return soap_fault("Client", "Malformed XML")
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Find the SOAP Body element (namespace-agnostic)
|
|
277
|
+
body_el = find_child(doc.root, "Body")
|
|
278
|
+
return soap_fault("Client", "Missing SOAP Body") unless body_el
|
|
279
|
+
|
|
280
|
+
# First child of Body is the operation element
|
|
281
|
+
op_el = body_el.elements.first
|
|
282
|
+
return soap_fault("Client", "Empty SOAP Body") unless op_el
|
|
283
|
+
|
|
284
|
+
op_name = local_name(op_el)
|
|
285
|
+
|
|
286
|
+
unless @operations.key?(op_name)
|
|
287
|
+
return soap_fault("Client", "Unknown operation: #{op_name}")
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
meta = @operations[op_name]
|
|
291
|
+
|
|
292
|
+
# Extract parameters from the operation element
|
|
293
|
+
params = {}
|
|
294
|
+
meta[:input].each do |param_name, param_type|
|
|
295
|
+
child = find_child(op_el, param_name.to_s)
|
|
296
|
+
if child
|
|
297
|
+
value = child.text || ""
|
|
298
|
+
params[param_name.to_s] = convert_value(value, param_type)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
begin
|
|
303
|
+
result = send(op_name.to_sym, *meta[:input].keys.map { |k| params[k.to_s] })
|
|
304
|
+
result = on_result(result)
|
|
305
|
+
rescue StandardError => e
|
|
306
|
+
return soap_fault("Server", e.message)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
soap_response(op_name, result)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# ── XML helpers (REXML) ──────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
def find_child(parent, local)
|
|
315
|
+
parent.each_element do |el|
|
|
316
|
+
return el if local_name(el) == local
|
|
317
|
+
end
|
|
318
|
+
nil
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def local_name(element)
|
|
322
|
+
element.name # REXML already strips the prefix for .name
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# ── Type conversion ──────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
def convert_value(value, target_type)
|
|
328
|
+
case target_type.to_s.downcase.to_sym
|
|
329
|
+
when :int, :integer
|
|
330
|
+
value.to_i
|
|
331
|
+
when :float, :double
|
|
332
|
+
value.to_f
|
|
333
|
+
when :boolean, :bool
|
|
334
|
+
%w[true 1 yes].include?(value.downcase)
|
|
335
|
+
else
|
|
336
|
+
value
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# ── Response builders ────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
def soap_response(op_name, result)
|
|
343
|
+
parts = []
|
|
344
|
+
parts << '<?xml version="1.0" encoding="UTF-8"?>'
|
|
345
|
+
parts << "<soap:Envelope xmlns:soap=\"#{NS_SOAP_ENV}\">"
|
|
346
|
+
parts << "<soap:Body>"
|
|
347
|
+
parts << "<#{op_name}Response>"
|
|
348
|
+
|
|
349
|
+
if result.is_a?(Hash)
|
|
350
|
+
result.each do |k, v|
|
|
351
|
+
if v.nil?
|
|
352
|
+
parts << "<#{k} xsi:nil=\"true\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"/>"
|
|
353
|
+
elsif v.is_a?(Array)
|
|
354
|
+
v.each { |item| parts << "<#{k}>#{escape_xml(item.to_s)}</#{k}>" }
|
|
355
|
+
else
|
|
356
|
+
parts << "<#{k}>#{escape_xml(v.to_s)}</#{k}>"
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
parts << "</#{op_name}Response>"
|
|
362
|
+
parts << "</soap:Body>"
|
|
363
|
+
parts << "</soap:Envelope>"
|
|
364
|
+
parts.join("\n")
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def soap_fault(code, message)
|
|
368
|
+
'<?xml version="1.0" encoding="UTF-8"?>' \
|
|
369
|
+
"<soap:Envelope xmlns:soap=\"#{NS_SOAP_ENV}\">" \
|
|
370
|
+
"<soap:Body>" \
|
|
371
|
+
"<soap:Fault>" \
|
|
372
|
+
"<faultcode>#{code}</faultcode>" \
|
|
373
|
+
"<faultstring>#{escape_xml(message)}</faultstring>" \
|
|
374
|
+
"</soap:Fault>" \
|
|
375
|
+
"</soap:Body>" \
|
|
376
|
+
"</soap:Envelope>"
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def escape_xml(s)
|
|
380
|
+
s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def xsd_type(ruby_type)
|
|
384
|
+
return RUBY_TO_XSD[ruby_type] if RUBY_TO_XSD.key?(ruby_type)
|
|
385
|
+
|
|
386
|
+
sym = ruby_type.to_s.downcase.to_sym
|
|
387
|
+
RUBY_TO_XSD.fetch(sym, "xsd:string")
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# ── Legacy wrapper ───────────────────────────────────────────────────
|
|
391
|
+
# Keeps backward compatibility with the old Tina4::WSDL::Service API
|
|
392
|
+
# used in demos and existing code.
|
|
393
|
+
|
|
5
394
|
class Service
|
|
6
395
|
attr_reader :name, :namespace, :operations
|
|
7
396
|
|
|
8
397
|
def initialize(name:, namespace: "http://tina4.com/wsdl")
|
|
9
|
-
@name
|
|
10
|
-
@namespace
|
|
398
|
+
@name = name
|
|
399
|
+
@namespace = namespace
|
|
11
400
|
@operations = {}
|
|
12
401
|
end
|
|
13
402
|
|
|
@@ -30,8 +419,8 @@ module Tina4
|
|
|
30
419
|
# Types
|
|
31
420
|
xml += " <types>\n <xsd:schema targetNamespace=\"#{@namespace}\">\n"
|
|
32
421
|
@operations.each do |op_name, op|
|
|
33
|
-
xml +=
|
|
34
|
-
xml +=
|
|
422
|
+
xml += _generate_elements(op_name, op[:input], "Request")
|
|
423
|
+
xml += _generate_elements(op_name, op[:output], "Response")
|
|
35
424
|
end
|
|
36
425
|
xml += " </xsd:schema>\n </types>\n"
|
|
37
426
|
|
|
@@ -78,44 +467,50 @@ module Tina4
|
|
|
78
467
|
end
|
|
79
468
|
|
|
80
469
|
def handle_soap_request(xml_body)
|
|
81
|
-
|
|
82
|
-
op_name = nil
|
|
83
|
-
params = {}
|
|
470
|
+
doc = REXML::Document.new(xml_body)
|
|
84
471
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
472
|
+
# Find Body element (namespace-agnostic)
|
|
473
|
+
body_el = _find_child(doc.root, "Body")
|
|
474
|
+
return _soap_fault("Unknown operation") unless body_el
|
|
475
|
+
|
|
476
|
+
op_el = body_el.elements.first
|
|
477
|
+
return _soap_fault("Unknown operation") unless op_el
|
|
91
478
|
|
|
92
|
-
|
|
479
|
+
op_name = op_el.name
|
|
480
|
+
return _soap_fault("Unknown operation") unless @operations.key?(op_name)
|
|
93
481
|
|
|
94
482
|
operation = @operations[op_name]
|
|
95
483
|
|
|
96
|
-
# Extract parameters
|
|
484
|
+
# Extract parameters
|
|
485
|
+
params = {}
|
|
97
486
|
operation[:input].each_key do |param_name|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
end
|
|
487
|
+
child = _find_child(op_el, param_name.to_s)
|
|
488
|
+
params[param_name.to_s] = child.text if child
|
|
101
489
|
end
|
|
102
490
|
|
|
103
491
|
# Execute handler
|
|
104
492
|
result = operation[:handler].call(params)
|
|
105
493
|
|
|
106
494
|
# Build SOAP response
|
|
107
|
-
|
|
108
|
-
rescue => e
|
|
109
|
-
|
|
495
|
+
_build_soap_response(op_name, result)
|
|
496
|
+
rescue StandardError => e
|
|
497
|
+
_soap_fault(e.message)
|
|
110
498
|
end
|
|
111
499
|
|
|
112
500
|
private
|
|
113
501
|
|
|
114
|
-
def
|
|
502
|
+
def _find_child(parent, local)
|
|
503
|
+
parent.each_element do |el|
|
|
504
|
+
return el if el.name == local
|
|
505
|
+
end
|
|
506
|
+
nil
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def _generate_elements(op_name, params, suffix)
|
|
115
510
|
xml = " <xsd:element name=\"#{op_name}#{suffix}\">\n"
|
|
116
511
|
xml += " <xsd:complexType><xsd:sequence>\n"
|
|
117
512
|
params.each do |name, type|
|
|
118
|
-
xsd_type =
|
|
513
|
+
xsd_type = _ruby_to_xsd_type(type)
|
|
119
514
|
xml += " <xsd:element name=\"#{name}\" type=\"xsd:#{xsd_type}\"/>\n"
|
|
120
515
|
end
|
|
121
516
|
xml += " </xsd:sequence></xsd:complexType>\n"
|
|
@@ -123,7 +518,7 @@ module Tina4
|
|
|
123
518
|
xml
|
|
124
519
|
end
|
|
125
520
|
|
|
126
|
-
def
|
|
521
|
+
def _ruby_to_xsd_type(type)
|
|
127
522
|
case type.to_s.downcase
|
|
128
523
|
when "string" then "string"
|
|
129
524
|
when "integer", "int" then "int"
|
|
@@ -135,30 +530,34 @@ module Tina4
|
|
|
135
530
|
end
|
|
136
531
|
end
|
|
137
532
|
|
|
138
|
-
def
|
|
533
|
+
def _build_soap_response(op_name, result)
|
|
139
534
|
xml = '<?xml version="1.0" encoding="UTF-8"?>'
|
|
140
|
-
xml +=
|
|
535
|
+
xml += "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\""
|
|
141
536
|
xml += " xmlns:tns=\"#{@namespace}\">"
|
|
142
537
|
xml += "<soap:Body>"
|
|
143
538
|
xml += "<tns:#{op_name}Response>"
|
|
144
539
|
if result.is_a?(Hash)
|
|
145
|
-
result.each { |k, v| xml += "<#{k}>#{v}</#{k}>" }
|
|
540
|
+
result.each { |k, v| xml += "<#{k}>#{_escape_xml(v.to_s)}</#{k}>" }
|
|
146
541
|
else
|
|
147
|
-
xml += "<result>#{result}</result>"
|
|
542
|
+
xml += "<result>#{_escape_xml(result.to_s)}</result>"
|
|
148
543
|
end
|
|
149
544
|
xml += "</tns:#{op_name}Response>"
|
|
150
545
|
xml += "</soap:Body></soap:Envelope>"
|
|
151
546
|
xml
|
|
152
547
|
end
|
|
153
548
|
|
|
154
|
-
def
|
|
549
|
+
def _soap_fault(message)
|
|
155
550
|
'<?xml version="1.0" encoding="UTF-8"?>' \
|
|
156
551
|
'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' \
|
|
157
552
|
"<soap:Body><soap:Fault>" \
|
|
158
553
|
"<faultcode>soap:Server</faultcode>" \
|
|
159
|
-
"<faultstring>#{message}</faultstring>" \
|
|
554
|
+
"<faultstring>#{_escape_xml(message)}</faultstring>" \
|
|
160
555
|
"</soap:Fault></soap:Body></soap:Envelope>"
|
|
161
556
|
end
|
|
557
|
+
|
|
558
|
+
def _escape_xml(s)
|
|
559
|
+
s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
560
|
+
end
|
|
162
561
|
end
|
|
163
562
|
end
|
|
164
563
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.10.
|
|
4
|
+
version: 3.10.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
@@ -150,6 +150,20 @@ dependencies:
|
|
|
150
150
|
- - "~>"
|
|
151
151
|
- !ruby/object:Gem::Version
|
|
152
152
|
version: '3.16'
|
|
153
|
+
- !ruby/object:Gem::Dependency
|
|
154
|
+
name: rexml
|
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
|
156
|
+
requirements:
|
|
157
|
+
- - "~>"
|
|
158
|
+
- !ruby/object:Gem::Version
|
|
159
|
+
version: '3.2'
|
|
160
|
+
type: :runtime
|
|
161
|
+
prerelease: false
|
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
163
|
+
requirements:
|
|
164
|
+
- - "~>"
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: '3.2'
|
|
153
167
|
- !ruby/object:Gem::Dependency
|
|
154
168
|
name: webrick
|
|
155
169
|
requirement: !ruby/object:Gem::Requirement
|