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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +80 -0
- data/LICENSE.txt +21 -0
- data/README.md +768 -0
- data/exe/tina4 +4 -0
- data/lib/tina4/api.rb +152 -0
- data/lib/tina4/auth.rb +139 -0
- data/lib/tina4/cli.rb +349 -0
- data/lib/tina4/crud.rb +124 -0
- data/lib/tina4/database.rb +135 -0
- data/lib/tina4/database_result.rb +89 -0
- data/lib/tina4/debug.rb +83 -0
- data/lib/tina4/dev.rb +15 -0
- data/lib/tina4/dev_reload.rb +68 -0
- data/lib/tina4/drivers/firebird_driver.rb +94 -0
- data/lib/tina4/drivers/mssql_driver.rb +112 -0
- data/lib/tina4/drivers/mysql_driver.rb +90 -0
- data/lib/tina4/drivers/postgres_driver.rb +99 -0
- data/lib/tina4/drivers/sqlite_driver.rb +85 -0
- data/lib/tina4/env.rb +55 -0
- data/lib/tina4/field_types.rb +84 -0
- data/lib/tina4/graphql.rb +837 -0
- data/lib/tina4/localization.rb +100 -0
- data/lib/tina4/middleware.rb +59 -0
- data/lib/tina4/migration.rb +124 -0
- data/lib/tina4/orm.rb +168 -0
- data/lib/tina4/public/css/tina4.css +2286 -0
- data/lib/tina4/public/css/tina4.min.css +2 -0
- data/lib/tina4/public/js/tina4.js +134 -0
- data/lib/tina4/public/js/tina4helper.js +387 -0
- data/lib/tina4/queue.rb +117 -0
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -0
- data/lib/tina4/queue_backends/lite_backend.rb +79 -0
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -0
- data/lib/tina4/rack_app.rb +150 -0
- data/lib/tina4/request.rb +158 -0
- data/lib/tina4/response.rb +172 -0
- data/lib/tina4/router.rb +148 -0
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -0
- data/lib/tina4/scss/tina4css/_badges.scss +22 -0
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -0
- data/lib/tina4/scss/tina4css/_cards.scss +49 -0
- data/lib/tina4/scss/tina4css/_forms.scss +156 -0
- data/lib/tina4/scss/tina4css/_grid.scss +81 -0
- data/lib/tina4/scss/tina4css/_modals.scss +84 -0
- data/lib/tina4/scss/tina4css/_nav.scss +149 -0
- data/lib/tina4/scss/tina4css/_reset.scss +94 -0
- data/lib/tina4/scss/tina4css/_tables.scss +54 -0
- data/lib/tina4/scss/tina4css/_typography.scss +55 -0
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -0
- data/lib/tina4/scss/tina4css/_variables.scss +117 -0
- data/lib/tina4/scss/tina4css/base.scss +1 -0
- data/lib/tina4/scss/tina4css/colors.scss +48 -0
- data/lib/tina4/scss/tina4css/tina4.scss +17 -0
- data/lib/tina4/scss_compiler.rb +131 -0
- data/lib/tina4/seeder.rb +529 -0
- data/lib/tina4/session.rb +145 -0
- data/lib/tina4/session_handlers/file_handler.rb +55 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -0
- data/lib/tina4/session_handlers/redis_handler.rb +43 -0
- data/lib/tina4/swagger.rb +123 -0
- data/lib/tina4/template.rb +478 -0
- data/lib/tina4/templates/base.twig +26 -0
- data/lib/tina4/templates/errors/403.twig +22 -0
- data/lib/tina4/templates/errors/404.twig +22 -0
- data/lib/tina4/templates/errors/500.twig +22 -0
- data/lib/tina4/testing.rb +213 -0
- data/lib/tina4/version.rb +5 -0
- data/lib/tina4/webserver.rb +101 -0
- data/lib/tina4/websocket.rb +167 -0
- data/lib/tina4/wsdl.rb +164 -0
- data/lib/tina4.rb +259 -0
- data/lib/tina4ruby.rb +4 -0
- 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,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
|