tina4ruby 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +768 -0
  5. data/exe/tina4 +4 -0
  6. data/lib/tina4/api.rb +152 -0
  7. data/lib/tina4/auth.rb +139 -0
  8. data/lib/tina4/cli.rb +349 -0
  9. data/lib/tina4/crud.rb +124 -0
  10. data/lib/tina4/database.rb +135 -0
  11. data/lib/tina4/database_result.rb +89 -0
  12. data/lib/tina4/debug.rb +83 -0
  13. data/lib/tina4/dev.rb +15 -0
  14. data/lib/tina4/dev_reload.rb +68 -0
  15. data/lib/tina4/drivers/firebird_driver.rb +94 -0
  16. data/lib/tina4/drivers/mssql_driver.rb +112 -0
  17. data/lib/tina4/drivers/mysql_driver.rb +90 -0
  18. data/lib/tina4/drivers/postgres_driver.rb +99 -0
  19. data/lib/tina4/drivers/sqlite_driver.rb +85 -0
  20. data/lib/tina4/env.rb +55 -0
  21. data/lib/tina4/field_types.rb +84 -0
  22. data/lib/tina4/graphql.rb +837 -0
  23. data/lib/tina4/localization.rb +100 -0
  24. data/lib/tina4/middleware.rb +59 -0
  25. data/lib/tina4/migration.rb +124 -0
  26. data/lib/tina4/orm.rb +168 -0
  27. data/lib/tina4/public/css/tina4.css +2286 -0
  28. data/lib/tina4/public/css/tina4.min.css +2 -0
  29. data/lib/tina4/public/js/tina4.js +134 -0
  30. data/lib/tina4/public/js/tina4helper.js +387 -0
  31. data/lib/tina4/queue.rb +117 -0
  32. data/lib/tina4/queue_backends/kafka_backend.rb +80 -0
  33. data/lib/tina4/queue_backends/lite_backend.rb +79 -0
  34. data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -0
  35. data/lib/tina4/rack_app.rb +150 -0
  36. data/lib/tina4/request.rb +158 -0
  37. data/lib/tina4/response.rb +172 -0
  38. data/lib/tina4/router.rb +148 -0
  39. data/lib/tina4/scss/tina4css/_alerts.scss +34 -0
  40. data/lib/tina4/scss/tina4css/_badges.scss +22 -0
  41. data/lib/tina4/scss/tina4css/_buttons.scss +69 -0
  42. data/lib/tina4/scss/tina4css/_cards.scss +49 -0
  43. data/lib/tina4/scss/tina4css/_forms.scss +156 -0
  44. data/lib/tina4/scss/tina4css/_grid.scss +81 -0
  45. data/lib/tina4/scss/tina4css/_modals.scss +84 -0
  46. data/lib/tina4/scss/tina4css/_nav.scss +149 -0
  47. data/lib/tina4/scss/tina4css/_reset.scss +94 -0
  48. data/lib/tina4/scss/tina4css/_tables.scss +54 -0
  49. data/lib/tina4/scss/tina4css/_typography.scss +55 -0
  50. data/lib/tina4/scss/tina4css/_utilities.scss +197 -0
  51. data/lib/tina4/scss/tina4css/_variables.scss +117 -0
  52. data/lib/tina4/scss/tina4css/base.scss +1 -0
  53. data/lib/tina4/scss/tina4css/colors.scss +48 -0
  54. data/lib/tina4/scss/tina4css/tina4.scss +17 -0
  55. data/lib/tina4/scss_compiler.rb +131 -0
  56. data/lib/tina4/seeder.rb +529 -0
  57. data/lib/tina4/session.rb +145 -0
  58. data/lib/tina4/session_handlers/file_handler.rb +55 -0
  59. data/lib/tina4/session_handlers/mongo_handler.rb +49 -0
  60. data/lib/tina4/session_handlers/redis_handler.rb +43 -0
  61. data/lib/tina4/swagger.rb +123 -0
  62. data/lib/tina4/template.rb +478 -0
  63. data/lib/tina4/templates/base.twig +26 -0
  64. data/lib/tina4/templates/errors/403.twig +22 -0
  65. data/lib/tina4/templates/errors/404.twig +22 -0
  66. data/lib/tina4/templates/errors/500.twig +22 -0
  67. data/lib/tina4/testing.rb +213 -0
  68. data/lib/tina4/version.rb +5 -0
  69. data/lib/tina4/webserver.rb +101 -0
  70. data/lib/tina4/websocket.rb +167 -0
  71. data/lib/tina4/wsdl.rb +164 -0
  72. data/lib/tina4.rb +259 -0
  73. data/lib/tina4ruby.rb +4 -0
  74. metadata +324 -0
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+ require "json"
3
+ require "stringio"
4
+
5
+ module Tina4
6
+ module Testing
7
+ class << self
8
+ def suites
9
+ @suites ||= []
10
+ end
11
+
12
+ def results
13
+ @results ||= { passed: 0, failed: 0, errors: 0, tests: [] }
14
+ end
15
+
16
+ def reset!
17
+ @suites = []
18
+ @results = { passed: 0, failed: 0, errors: 0, tests: [] }
19
+ end
20
+
21
+ def describe(name, &block)
22
+ suite = TestSuite.new(name)
23
+ suite.instance_eval(&block)
24
+ suites << suite
25
+ end
26
+
27
+ def run_all
28
+ reset_results
29
+ suites.each do |suite|
30
+ run_suite(suite)
31
+ end
32
+ print_results
33
+ results
34
+ end
35
+
36
+ private
37
+
38
+ def reset_results
39
+ @results = { passed: 0, failed: 0, errors: 0, tests: [] }
40
+ end
41
+
42
+ def run_suite(suite)
43
+ puts "\n #{suite.name}"
44
+ suite.tests.each do |test|
45
+ run_test(suite, test)
46
+ end
47
+ end
48
+
49
+ def run_test(suite, test)
50
+ suite.run_before_each
51
+ context = TestContext.new
52
+ context.instance_eval(&test[:block])
53
+ results[:passed] += 1
54
+ results[:tests] << { name: test[:name], status: :passed, suite: suite.name }
55
+ puts " \e[32m✓\e[0m #{test[:name]}"
56
+ rescue TestFailure => e
57
+ results[:failed] += 1
58
+ results[:tests] << { name: test[:name], status: :failed, suite: suite.name, message: e.message }
59
+ puts " \e[31m✗\e[0m #{test[:name]}: #{e.message}"
60
+ rescue => e
61
+ results[:errors] += 1
62
+ results[:tests] << { name: test[:name], status: :error, suite: suite.name, message: e.message }
63
+ puts " \e[33m!\e[0m #{test[:name]}: #{e.message}"
64
+ ensure
65
+ suite.run_after_each
66
+ end
67
+
68
+ def print_results
69
+ total = results[:passed] + results[:failed] + results[:errors]
70
+ puts "\n #{total} tests: \e[32m#{results[:passed]} passed\e[0m, " \
71
+ "\e[31m#{results[:failed]} failed\e[0m, " \
72
+ "\e[33m#{results[:errors]} errors\e[0m\n"
73
+ end
74
+ end
75
+
76
+ class TestFailure < StandardError; end
77
+
78
+ class TestSuite
79
+ attr_reader :name, :tests
80
+
81
+ def initialize(name)
82
+ @name = name
83
+ @tests = []
84
+ @before_each = nil
85
+ @after_each = nil
86
+ end
87
+
88
+ def it(description, &block)
89
+ @tests << { name: description, block: block }
90
+ end
91
+
92
+ def before_each(&block)
93
+ @before_each = block
94
+ end
95
+
96
+ def after_each(&block)
97
+ @after_each = block
98
+ end
99
+
100
+ def run_before_each
101
+ @before_each&.call
102
+ end
103
+
104
+ def run_after_each
105
+ @after_each&.call
106
+ end
107
+ end
108
+
109
+ class TestContext
110
+ def assert(condition, message = "Assertion failed")
111
+ raise TestFailure, message unless condition
112
+ end
113
+
114
+ def assert_equal(expected, actual, message = nil)
115
+ msg = message || "Expected #{expected.inspect}, got #{actual.inspect}"
116
+ raise TestFailure, msg unless expected == actual
117
+ end
118
+
119
+ def assert_not_equal(expected, actual, message = nil)
120
+ msg = message || "Expected #{actual.inspect} to not equal #{expected.inspect}"
121
+ raise TestFailure, msg if expected == actual
122
+ end
123
+
124
+ def assert_nil(value, message = nil)
125
+ msg = message || "Expected nil, got #{value.inspect}"
126
+ raise TestFailure, msg unless value.nil?
127
+ end
128
+
129
+ def assert_not_nil(value, message = nil)
130
+ msg = message || "Expected non-nil value"
131
+ raise TestFailure, msg if value.nil?
132
+ end
133
+
134
+ def assert_includes(collection, item, message = nil)
135
+ msg = message || "Expected #{collection.inspect} to include #{item.inspect}"
136
+ raise TestFailure, msg unless collection.include?(item)
137
+ end
138
+
139
+ def assert_raises(exception_class, message = nil)
140
+ yield
141
+ raise TestFailure, message || "Expected #{exception_class} to be raised"
142
+ rescue exception_class
143
+ true
144
+ end
145
+
146
+ def assert_match(pattern, string, message = nil)
147
+ msg = message || "Expected #{string.inspect} to match #{pattern.inspect}"
148
+ raise TestFailure, msg unless pattern.match?(string)
149
+ end
150
+
151
+ def assert_json(response_body)
152
+ JSON.parse(response_body)
153
+ rescue JSON::ParserError => e
154
+ raise TestFailure, "Invalid JSON: #{e.message}"
155
+ end
156
+
157
+ def assert_status(response, expected_status)
158
+ actual = response.is_a?(Array) ? response[0] : response.status
159
+ assert_equal(expected_status, actual, "Expected status #{expected_status}, got #{actual}")
160
+ end
161
+
162
+ # HTTP test helpers
163
+ def simulate_request(method, path, body: nil, headers: {}, params: {})
164
+ env = build_test_env(method, path, body: body, headers: headers, params: params)
165
+ app = Tina4::RackApp.new
166
+ app.call(env)
167
+ end
168
+
169
+ def get(path, headers: {}, params: {})
170
+ simulate_request("GET", path, headers: headers, params: params)
171
+ end
172
+
173
+ def post(path, body: nil, headers: {})
174
+ simulate_request("POST", path, body: body, headers: headers)
175
+ end
176
+
177
+ def put(path, body: nil, headers: {})
178
+ simulate_request("PUT", path, body: body, headers: headers)
179
+ end
180
+
181
+ def delete(path, headers: {})
182
+ simulate_request("DELETE", path, headers: headers)
183
+ end
184
+
185
+ private
186
+
187
+ def build_test_env(method, path, body: nil, headers: {}, params: {})
188
+ query_string = params.empty? ? "" : URI.encode_www_form(params)
189
+ body_str = body.is_a?(Hash) ? JSON.generate(body) : (body || "")
190
+ input = StringIO.new(body_str)
191
+
192
+ env = {
193
+ "REQUEST_METHOD" => method.upcase,
194
+ "PATH_INFO" => path,
195
+ "QUERY_STRING" => query_string,
196
+ "CONTENT_TYPE" => body.is_a?(Hash) ? "application/json" : "text/plain",
197
+ "CONTENT_LENGTH" => body_str.length.to_s,
198
+ "REMOTE_ADDR" => "127.0.0.1",
199
+ "rack.input" => input,
200
+ "rack.errors" => StringIO.new,
201
+ "rack.url_scheme" => "http"
202
+ }
203
+
204
+ headers.each do |key, value|
205
+ env_key = "HTTP_#{key.upcase.gsub('-', '_')}"
206
+ env[env_key] = value
207
+ end
208
+
209
+ env
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ VERSION = "0.4.0"
5
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ class WebServer
5
+ def initialize(app, host: "0.0.0.0", port: 7145)
6
+ @app = app
7
+ @host = host
8
+ @port = port
9
+ end
10
+
11
+ def start
12
+ require "webrick"
13
+ require "stringio"
14
+ Tina4.print_banner
15
+ Tina4::Debug.info("Starting Tina4 WEBrick server on http://#{@host}:#{@port}")
16
+ @server = WEBrick::HTTPServer.new(
17
+ BindAddress: @host,
18
+ Port: @port,
19
+ Logger: WEBrick::Log.new(File::NULL),
20
+ AccessLog: []
21
+ )
22
+
23
+ # Use a custom servlet that passes ALL methods (including OPTIONS) to Rack
24
+ rack_app = @app
25
+ servlet = Class.new(WEBrick::HTTPServlet::AbstractServlet) do
26
+ define_method(:initialize) do |server, app|
27
+ super(server)
28
+ @app = app
29
+ end
30
+
31
+ %w[GET POST PUT DELETE PATCH HEAD OPTIONS].each do |http_method|
32
+ define_method("do_#{http_method}") do |webrick_req, webrick_res|
33
+ handle_request(webrick_req, webrick_res)
34
+ end
35
+ end
36
+
37
+ define_method(:handle_request) do |webrick_req, webrick_res|
38
+ env = build_rack_env(webrick_req)
39
+ status, headers, body = @app.call(env)
40
+
41
+ webrick_res.status = status
42
+ headers.each do |key, value|
43
+ if key.downcase == "set-cookie"
44
+ Array(value.split("\n")).each { |c| webrick_res.cookies << WEBrick::Cookie.parse_set_cookie(c) }
45
+ else
46
+ webrick_res[key] = value
47
+ end
48
+ end
49
+
50
+ response_body = ""
51
+ body.each { |chunk| response_body += chunk }
52
+ webrick_res.body = response_body
53
+ end
54
+
55
+ define_method(:build_rack_env) do |req|
56
+ input = StringIO.new(req.body || "")
57
+ env = {
58
+ "REQUEST_METHOD" => req.request_method,
59
+ "PATH_INFO" => req.path,
60
+ "QUERY_STRING" => req.query_string || "",
61
+ "SERVER_NAME" => webrick_req_host,
62
+ "SERVER_PORT" => webrick_req_port,
63
+ "CONTENT_TYPE" => req.content_type || "",
64
+ "CONTENT_LENGTH" => (req.content_length rescue 0).to_s,
65
+ "REMOTE_ADDR" => req.peeraddr&.last || "127.0.0.1",
66
+ "rack.input" => input,
67
+ "rack.errors" => $stderr,
68
+ "rack.url_scheme" => "http",
69
+ "rack.version" => [1, 3],
70
+ "rack.multithread" => true,
71
+ "rack.multiprocess" => false,
72
+ "rack.run_once" => false
73
+ }
74
+
75
+ req.header.each do |key, values|
76
+ env_key = "HTTP_#{key.upcase.gsub('-', '_')}"
77
+ env[env_key] = values.join(", ")
78
+ end
79
+
80
+ env
81
+ end
82
+ end
83
+
84
+ # Store host/port for the servlet's build_rack_env
85
+ host = @host
86
+ port = @port.to_s
87
+ servlet.define_method(:webrick_req_host) { host }
88
+ servlet.define_method(:webrick_req_port) { port }
89
+
90
+ @server.mount("/", servlet, rack_app)
91
+
92
+ trap("INT") { @server.shutdown }
93
+ trap("TERM") { @server.shutdown }
94
+ @server.start
95
+ end
96
+
97
+ def stop
98
+ @server&.shutdown
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+ require "socket"
3
+ require "digest"
4
+ require "base64"
5
+
6
+ module Tina4
7
+ class WebSocket
8
+ GUID = "258EAFA5-E914-47DA-95CA-5AB5DC11AD37"
9
+
10
+ attr_reader :connections
11
+
12
+ def initialize
13
+ @connections = {}
14
+ @handlers = {
15
+ open: [],
16
+ message: [],
17
+ close: [],
18
+ error: []
19
+ }
20
+ end
21
+
22
+ def on(event, &block)
23
+ @handlers[event.to_sym] << block if @handlers.key?(event.to_sym)
24
+ end
25
+
26
+ def upgrade?(env)
27
+ upgrade = env["HTTP_UPGRADE"] || ""
28
+ upgrade.downcase == "websocket"
29
+ end
30
+
31
+ def handle_upgrade(env, socket)
32
+ key = env["HTTP_SEC_WEBSOCKET_KEY"]
33
+ return unless key
34
+
35
+ accept = Base64.strict_encode64(
36
+ Digest::SHA1.digest("#{key}#{GUID}")
37
+ )
38
+
39
+ response = "HTTP/1.1 101 Switching Protocols\r\n" \
40
+ "Upgrade: websocket\r\n" \
41
+ "Connection: Upgrade\r\n" \
42
+ "Sec-WebSocket-Accept: #{accept}\r\n\r\n"
43
+
44
+ socket.write(response)
45
+
46
+ conn_id = SecureRandom.hex(16)
47
+ connection = WebSocketConnection.new(conn_id, socket)
48
+ @connections[conn_id] = connection
49
+
50
+ emit(:open, connection)
51
+
52
+ Thread.new do
53
+ begin
54
+ loop do
55
+ frame = connection.read_frame
56
+ break unless frame
57
+
58
+ case frame[:opcode]
59
+ when 0x1 # Text
60
+ emit(:message, connection, frame[:data])
61
+ when 0x8 # Close
62
+ break
63
+ when 0x9 # Ping
64
+ connection.send_pong(frame[:data])
65
+ end
66
+ end
67
+ rescue => e
68
+ emit(:error, connection, e)
69
+ ensure
70
+ @connections.delete(conn_id)
71
+ emit(:close, connection)
72
+ socket.close rescue nil
73
+ end
74
+ end
75
+ end
76
+
77
+ def broadcast(message, exclude: nil)
78
+ @connections.each do |id, conn|
79
+ next if exclude && id == exclude
80
+ conn.send_text(message)
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def emit(event, *args)
87
+ @handlers[event]&.each { |h| h.call(*args) }
88
+ end
89
+ end
90
+
91
+ class WebSocketConnection
92
+ attr_reader :id
93
+
94
+ def initialize(id, socket)
95
+ @id = id
96
+ @socket = socket
97
+ end
98
+
99
+ def send_text(message)
100
+ data = message.encode("UTF-8")
101
+ frame = build_frame(0x1, data)
102
+ @socket.write(frame)
103
+ rescue IOError
104
+ # Connection closed
105
+ end
106
+
107
+ def send_pong(data)
108
+ frame = build_frame(0xA, data || "")
109
+ @socket.write(frame)
110
+ rescue IOError
111
+ # Connection closed
112
+ end
113
+
114
+ def close(code: 1000, reason: "")
115
+ payload = [code].pack("n") + reason
116
+ frame = build_frame(0x8, payload)
117
+ @socket.write(frame) rescue nil
118
+ @socket.close rescue nil
119
+ end
120
+
121
+ def read_frame
122
+ first_byte = @socket.getbyte
123
+ return nil unless first_byte
124
+
125
+ opcode = first_byte & 0x0F
126
+ second_byte = @socket.getbyte
127
+ return nil unless second_byte
128
+
129
+ masked = (second_byte & 0x80) != 0
130
+ length = second_byte & 0x7F
131
+
132
+ if length == 126
133
+ length = @socket.read(2).unpack1("n")
134
+ elsif length == 127
135
+ length = @socket.read(8).unpack1("Q>")
136
+ end
137
+
138
+ mask_key = masked ? @socket.read(4).bytes : nil
139
+ data = @socket.read(length) || ""
140
+
141
+ if masked && mask_key
142
+ data = data.bytes.each_with_index.map { |b, i| b ^ mask_key[i % 4] }.pack("C*")
143
+ end
144
+
145
+ { opcode: opcode, data: data }
146
+ rescue IOError, EOFError
147
+ nil
148
+ end
149
+
150
+ private
151
+
152
+ def build_frame(opcode, data)
153
+ frame = [0x80 | opcode].pack("C")
154
+ length = data.bytesize
155
+
156
+ if length < 126
157
+ frame += [length].pack("C")
158
+ elsif length < 65536
159
+ frame += [126, length].pack("Cn")
160
+ else
161
+ frame += [127, length].pack("CQ>")
162
+ end
163
+
164
+ frame + data
165
+ end
166
+ end
167
+ end
data/lib/tina4/wsdl.rb ADDED
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ module WSDL
5
+ class Service
6
+ attr_reader :name, :namespace, :operations
7
+
8
+ def initialize(name:, namespace: "http://tina4.com/wsdl")
9
+ @name = name
10
+ @namespace = namespace
11
+ @operations = {}
12
+ end
13
+
14
+ def add_operation(name, input_params: {}, output_params: {}, &handler)
15
+ @operations[name.to_s] = {
16
+ input: input_params,
17
+ output: output_params,
18
+ handler: handler
19
+ }
20
+ end
21
+
22
+ def generate_wsdl(endpoint_url)
23
+ xml = '<?xml version="1.0" encoding="UTF-8"?>'
24
+ xml += "\n<definitions xmlns=\"http://schemas.xmlsoap.org/wsdl/\""
25
+ xml += " xmlns:soap=\"http://schemas.xmlsoap.org/wsdl/soap/\""
26
+ xml += " xmlns:tns=\"#{@namespace}\""
27
+ xml += " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\""
28
+ xml += " name=\"#{@name}\" targetNamespace=\"#{@namespace}\">\n"
29
+
30
+ # Types
31
+ xml += " <types>\n <xsd:schema targetNamespace=\"#{@namespace}\">\n"
32
+ @operations.each do |op_name, op|
33
+ xml += generate_elements(op_name, op[:input], "Request")
34
+ xml += generate_elements(op_name, op[:output], "Response")
35
+ end
36
+ xml += " </xsd:schema>\n </types>\n"
37
+
38
+ # Messages
39
+ @operations.each_key do |op_name|
40
+ xml += " <message name=\"#{op_name}Request\">\n"
41
+ xml += " <part name=\"parameters\" element=\"tns:#{op_name}Request\"/>\n"
42
+ xml += " </message>\n"
43
+ xml += " <message name=\"#{op_name}Response\">\n"
44
+ xml += " <part name=\"parameters\" element=\"tns:#{op_name}Response\"/>\n"
45
+ xml += " </message>\n"
46
+ end
47
+
48
+ # PortType
49
+ xml += " <portType name=\"#{@name}PortType\">\n"
50
+ @operations.each_key do |op_name|
51
+ xml += " <operation name=\"#{op_name}\">\n"
52
+ xml += " <input message=\"tns:#{op_name}Request\"/>\n"
53
+ xml += " <output message=\"tns:#{op_name}Response\"/>\n"
54
+ xml += " </operation>\n"
55
+ end
56
+ xml += " </portType>\n"
57
+
58
+ # Binding
59
+ xml += " <binding name=\"#{@name}Binding\" type=\"tns:#{@name}PortType\">\n"
60
+ xml += " <soap:binding style=\"document\" transport=\"http://schemas.xmlsoap.org/soap/http\"/>\n"
61
+ @operations.each_key do |op_name|
62
+ xml += " <operation name=\"#{op_name}\">\n"
63
+ xml += " <soap:operation soapAction=\"#{@namespace}/#{op_name}\"/>\n"
64
+ xml += " <input><soap:body use=\"literal\"/></input>\n"
65
+ xml += " <output><soap:body use=\"literal\"/></output>\n"
66
+ xml += " </operation>\n"
67
+ end
68
+ xml += " </binding>\n"
69
+
70
+ # Service
71
+ xml += " <service name=\"#{@name}\">\n"
72
+ xml += " <port name=\"#{@name}Port\" binding=\"tns:#{@name}Binding\">\n"
73
+ xml += " <soap:address location=\"#{endpoint_url}\"/>\n"
74
+ xml += " </port>\n"
75
+ xml += " </service>\n"
76
+ xml += "</definitions>"
77
+ xml
78
+ end
79
+
80
+ def handle_soap_request(xml_body)
81
+ # Simple SOAP envelope parser
82
+ op_name = nil
83
+ params = {}
84
+
85
+ @operations.each_key do |name|
86
+ if xml_body.include?(name)
87
+ op_name = name
88
+ break
89
+ end
90
+ end
91
+
92
+ return soap_fault("Unknown operation") unless op_name
93
+
94
+ operation = @operations[op_name]
95
+
96
+ # Extract parameters from XML
97
+ operation[:input].each_key do |param_name|
98
+ if xml_body =~ /<#{param_name}>(.*?)<\/#{param_name}>/m
99
+ params[param_name.to_s] = Regexp.last_match(1)
100
+ end
101
+ end
102
+
103
+ # Execute handler
104
+ result = operation[:handler].call(params)
105
+
106
+ # Build SOAP response
107
+ build_soap_response(op_name, result)
108
+ rescue => e
109
+ soap_fault(e.message)
110
+ end
111
+
112
+ private
113
+
114
+ def generate_elements(op_name, params, suffix)
115
+ xml = " <xsd:element name=\"#{op_name}#{suffix}\">\n"
116
+ xml += " <xsd:complexType><xsd:sequence>\n"
117
+ params.each do |name, type|
118
+ xsd_type = ruby_to_xsd_type(type)
119
+ xml += " <xsd:element name=\"#{name}\" type=\"xsd:#{xsd_type}\"/>\n"
120
+ end
121
+ xml += " </xsd:sequence></xsd:complexType>\n"
122
+ xml += " </xsd:element>\n"
123
+ xml
124
+ end
125
+
126
+ def ruby_to_xsd_type(type)
127
+ case type.to_s.downcase
128
+ when "string" then "string"
129
+ when "integer", "int" then "int"
130
+ when "float", "double" then "double"
131
+ when "boolean", "bool" then "boolean"
132
+ when "date" then "date"
133
+ when "datetime" then "dateTime"
134
+ else "string"
135
+ end
136
+ end
137
+
138
+ def build_soap_response(op_name, result)
139
+ xml = '<?xml version="1.0" encoding="UTF-8"?>'
140
+ xml += '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"'
141
+ xml += " xmlns:tns=\"#{@namespace}\">"
142
+ xml += "<soap:Body>"
143
+ xml += "<tns:#{op_name}Response>"
144
+ if result.is_a?(Hash)
145
+ result.each { |k, v| xml += "<#{k}>#{v}</#{k}>" }
146
+ else
147
+ xml += "<result>#{result}</result>"
148
+ end
149
+ xml += "</tns:#{op_name}Response>"
150
+ xml += "</soap:Body></soap:Envelope>"
151
+ xml
152
+ end
153
+
154
+ def soap_fault(message)
155
+ '<?xml version="1.0" encoding="UTF-8"?>' \
156
+ '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' \
157
+ "<soap:Body><soap:Fault>" \
158
+ "<faultcode>soap:Server</faultcode>" \
159
+ "<faultstring>#{message}</faultstring>" \
160
+ "</soap:Fault></soap:Body></soap:Envelope>"
161
+ end
162
+ end
163
+ end
164
+ end