tina4 0.3.0 → 0.5.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 +4 -4
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4ruby.rb +4 -0
- metadata +14 -258
- data/CHANGELOG.md +0 -73
- data/LICENSE.txt +0 -21
- data/README.md +0 -662
- data/exe/tina4 +0 -4
- data/lib/tina4/api.rb +0 -152
- data/lib/tina4/auth.rb +0 -139
- data/lib/tina4/cli.rb +0 -243
- data/lib/tina4/crud.rb +0 -124
- data/lib/tina4/database.rb +0 -135
- data/lib/tina4/database_result.rb +0 -89
- data/lib/tina4/debug.rb +0 -83
- data/lib/tina4/dev_reload.rb +0 -68
- data/lib/tina4/drivers/firebird_driver.rb +0 -94
- data/lib/tina4/drivers/mssql_driver.rb +0 -112
- data/lib/tina4/drivers/mysql_driver.rb +0 -90
- data/lib/tina4/drivers/postgres_driver.rb +0 -99
- data/lib/tina4/drivers/sqlite_driver.rb +0 -85
- data/lib/tina4/env.rb +0 -55
- data/lib/tina4/field_types.rb +0 -84
- data/lib/tina4/graphql.rb +0 -837
- data/lib/tina4/localization.rb +0 -100
- data/lib/tina4/middleware.rb +0 -59
- data/lib/tina4/migration.rb +0 -124
- data/lib/tina4/orm.rb +0 -168
- data/lib/tina4/queue.rb +0 -117
- data/lib/tina4/queue_backends/kafka_backend.rb +0 -80
- data/lib/tina4/queue_backends/lite_backend.rb +0 -79
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +0 -73
- data/lib/tina4/rack_app.rb +0 -150
- data/lib/tina4/request.rb +0 -158
- data/lib/tina4/response.rb +0 -172
- data/lib/tina4/router.rb +0 -142
- data/lib/tina4/scss_compiler.rb +0 -131
- data/lib/tina4/session.rb +0 -145
- data/lib/tina4/session_handlers/file_handler.rb +0 -55
- data/lib/tina4/session_handlers/mongo_handler.rb +0 -49
- data/lib/tina4/session_handlers/redis_handler.rb +0 -43
- data/lib/tina4/swagger.rb +0 -123
- data/lib/tina4/template.rb +0 -478
- data/lib/tina4/templates/base.twig +0 -25
- data/lib/tina4/templates/errors/403.twig +0 -22
- data/lib/tina4/templates/errors/404.twig +0 -22
- data/lib/tina4/templates/errors/500.twig +0 -22
- data/lib/tina4/testing.rb +0 -213
- data/lib/tina4/webserver.rb +0 -101
- data/lib/tina4/websocket.rb +0 -167
- data/lib/tina4/wsdl.rb +0 -164
- data/lib/tina4.rb +0 -234
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "json"
|
|
3
|
-
require "fileutils"
|
|
4
|
-
|
|
5
|
-
module Tina4
|
|
6
|
-
module QueueBackends
|
|
7
|
-
class LiteBackend
|
|
8
|
-
def initialize(options = {})
|
|
9
|
-
@dir = options[:dir] || File.join(Dir.pwd, ".queue")
|
|
10
|
-
@dead_letter_dir = File.join(@dir, "dead_letter")
|
|
11
|
-
FileUtils.mkdir_p(@dir)
|
|
12
|
-
FileUtils.mkdir_p(@dead_letter_dir)
|
|
13
|
-
@mutex = Mutex.new
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def enqueue(message)
|
|
17
|
-
@mutex.synchronize do
|
|
18
|
-
topic_dir = topic_path(message.topic)
|
|
19
|
-
FileUtils.mkdir_p(topic_dir)
|
|
20
|
-
path = File.join(topic_dir, "#{message.id}.json")
|
|
21
|
-
File.write(path, message.to_json)
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def dequeue(topic)
|
|
26
|
-
@mutex.synchronize do
|
|
27
|
-
dir = topic_path(topic)
|
|
28
|
-
return nil unless Dir.exist?(dir)
|
|
29
|
-
|
|
30
|
-
files = Dir.glob(File.join(dir, "*.json")).sort_by { |f| File.mtime(f) }
|
|
31
|
-
return nil if files.empty?
|
|
32
|
-
|
|
33
|
-
file = files.first
|
|
34
|
-
data = JSON.parse(File.read(file))
|
|
35
|
-
File.delete(file)
|
|
36
|
-
|
|
37
|
-
Tina4::QueueMessage.new(
|
|
38
|
-
topic: data["topic"],
|
|
39
|
-
payload: data["payload"],
|
|
40
|
-
id: data["id"]
|
|
41
|
-
)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def acknowledge(message)
|
|
46
|
-
# File already deleted on dequeue
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def requeue(message)
|
|
50
|
-
enqueue(message)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def dead_letter(message)
|
|
54
|
-
path = File.join(@dead_letter_dir, "#{message.id}.json")
|
|
55
|
-
File.write(path, message.to_json)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def size(topic)
|
|
59
|
-
dir = topic_path(topic)
|
|
60
|
-
return 0 unless Dir.exist?(dir)
|
|
61
|
-
Dir.glob(File.join(dir, "*.json")).length
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def topics
|
|
65
|
-
return [] unless Dir.exist?(@dir)
|
|
66
|
-
Dir.children(@dir)
|
|
67
|
-
.reject { |d| d == "dead_letter" }
|
|
68
|
-
.select { |d| File.directory?(File.join(@dir, d)) }
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
def topic_path(topic)
|
|
74
|
-
safe_topic = topic.to_s.gsub(/[^a-zA-Z0-9_-]/, "_")
|
|
75
|
-
File.join(@dir, safe_topic)
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tina4
|
|
4
|
-
module QueueBackends
|
|
5
|
-
class RabbitmqBackend
|
|
6
|
-
def initialize(options = {})
|
|
7
|
-
require "bunny"
|
|
8
|
-
@connection = Bunny.new(
|
|
9
|
-
host: options[:host] || "localhost",
|
|
10
|
-
port: options[:port] || 5672,
|
|
11
|
-
username: options[:username] || "guest",
|
|
12
|
-
password: options[:password] || "guest",
|
|
13
|
-
vhost: options[:vhost] || "/"
|
|
14
|
-
)
|
|
15
|
-
@connection.start
|
|
16
|
-
@channel = @connection.create_channel
|
|
17
|
-
@queues = {}
|
|
18
|
-
@exchanges = {}
|
|
19
|
-
rescue LoadError
|
|
20
|
-
raise "RabbitMQ backend requires the 'bunny' gem. Install with: gem install bunny"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def enqueue(message)
|
|
24
|
-
queue = get_queue(message.topic)
|
|
25
|
-
queue.publish(message.to_json, persistent: true)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def dequeue(topic)
|
|
29
|
-
queue = get_queue(topic)
|
|
30
|
-
delivery_info, _properties, payload = queue.pop
|
|
31
|
-
return nil unless payload
|
|
32
|
-
|
|
33
|
-
data = JSON.parse(payload)
|
|
34
|
-
msg = Tina4::QueueMessage.new(
|
|
35
|
-
topic: data["topic"],
|
|
36
|
-
payload: data["payload"],
|
|
37
|
-
id: data["id"]
|
|
38
|
-
)
|
|
39
|
-
@last_delivery_tag = delivery_info.delivery_tag
|
|
40
|
-
msg
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def acknowledge(_message)
|
|
44
|
-
@channel.acknowledge(@last_delivery_tag) if @last_delivery_tag
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def requeue(message)
|
|
48
|
-
enqueue(message)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def dead_letter(message)
|
|
52
|
-
dlq = get_queue("#{message.topic}.dead_letter")
|
|
53
|
-
dlq.publish(message.to_json, persistent: true)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def size(topic)
|
|
57
|
-
queue = get_queue(topic)
|
|
58
|
-
queue.message_count
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def close
|
|
62
|
-
@channel&.close
|
|
63
|
-
@connection&.close
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
def get_queue(topic)
|
|
69
|
-
@queues[topic] ||= @channel.queue(topic, durable: true)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
data/lib/tina4/rack_app.rb
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "json"
|
|
3
|
-
|
|
4
|
-
module Tina4
|
|
5
|
-
class RackApp
|
|
6
|
-
STATIC_DIRS = %w[public src/public src/assets assets].freeze
|
|
7
|
-
|
|
8
|
-
# Pre-built frozen responses for zero-allocation fast paths
|
|
9
|
-
CORS_HEADERS = {
|
|
10
|
-
"access-control-allow-origin" => "*",
|
|
11
|
-
"access-control-allow-methods" => "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
12
|
-
"access-control-allow-headers" => "Content-Type, Authorization, Accept",
|
|
13
|
-
"access-control-max-age" => "86400"
|
|
14
|
-
}.freeze
|
|
15
|
-
|
|
16
|
-
OPTIONS_RESPONSE = [204, CORS_HEADERS, [""]].freeze
|
|
17
|
-
|
|
18
|
-
def initialize(root_dir: Dir.pwd)
|
|
19
|
-
@root_dir = root_dir
|
|
20
|
-
# Pre-compute static roots at boot (not per-request)
|
|
21
|
-
@static_roots = STATIC_DIRS.map { |d| File.join(root_dir, d) }
|
|
22
|
-
.select { |d| Dir.exist?(d) }
|
|
23
|
-
.freeze
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def call(env)
|
|
27
|
-
method = env["REQUEST_METHOD"]
|
|
28
|
-
path = env["PATH_INFO"] || "/"
|
|
29
|
-
|
|
30
|
-
# Fast-path: OPTIONS preflight (zero allocation)
|
|
31
|
-
return OPTIONS_RESPONSE if method == "OPTIONS"
|
|
32
|
-
|
|
33
|
-
# Fast-path: API routes skip static file + swagger checks entirely
|
|
34
|
-
unless path.start_with?("/api/")
|
|
35
|
-
# Swagger
|
|
36
|
-
if path == "/swagger" || path == "/swagger/"
|
|
37
|
-
return serve_swagger_ui
|
|
38
|
-
end
|
|
39
|
-
if path == "/swagger/openapi.json"
|
|
40
|
-
return serve_openapi_json
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Static files (only for non-API paths)
|
|
44
|
-
static_response = try_static(path)
|
|
45
|
-
return static_response if static_response
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Route matching
|
|
49
|
-
result = Tina4::Router.find_route(path, method)
|
|
50
|
-
if result
|
|
51
|
-
route, path_params = result
|
|
52
|
-
handle_route(env, route, path_params)
|
|
53
|
-
else
|
|
54
|
-
handle_404(path)
|
|
55
|
-
end
|
|
56
|
-
rescue => e
|
|
57
|
-
handle_500(e)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
def handle_route(env, route, path_params)
|
|
63
|
-
# Auth check
|
|
64
|
-
if route.auth_handler
|
|
65
|
-
auth_result = route.auth_handler.call(env)
|
|
66
|
-
return handle_403 unless auth_result
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
request = Tina4::Request.new(env, path_params)
|
|
70
|
-
response = Tina4::Response.new
|
|
71
|
-
|
|
72
|
-
# Execute handler
|
|
73
|
-
result = route.handler.call(request, response)
|
|
74
|
-
|
|
75
|
-
# Skip auto_detect if handler already returned the response object
|
|
76
|
-
final_response = result.equal?(response) ? result : Tina4::Response.auto_detect(result, response)
|
|
77
|
-
final_response.to_rack
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def try_static(path)
|
|
81
|
-
return nil if path.include?("..")
|
|
82
|
-
|
|
83
|
-
@static_roots.each do |root|
|
|
84
|
-
full_path = File.join(root, path)
|
|
85
|
-
if File.file?(full_path)
|
|
86
|
-
return serve_static_file(full_path)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Only try index.html for directory-like paths
|
|
90
|
-
if path.end_with?("/") || !path.include?(".")
|
|
91
|
-
index_path = File.join(full_path, "index.html")
|
|
92
|
-
if File.file?(index_path)
|
|
93
|
-
return serve_static_file(index_path)
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
nil
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def serve_static_file(full_path)
|
|
101
|
-
ext = File.extname(full_path).downcase
|
|
102
|
-
content_type = Tina4::Response::MIME_TYPES[ext] || "application/octet-stream"
|
|
103
|
-
[200, { "content-type" => content_type }, [File.binread(full_path)]]
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def serve_swagger_ui
|
|
107
|
-
html = <<~HTML
|
|
108
|
-
<!DOCTYPE html>
|
|
109
|
-
<html lang="en">
|
|
110
|
-
<head>
|
|
111
|
-
<meta charset="UTF-8">
|
|
112
|
-
<title>API Documentation</title>
|
|
113
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
|
|
114
|
-
</head>
|
|
115
|
-
<body>
|
|
116
|
-
<div id="swagger-ui"></div>
|
|
117
|
-
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
|
118
|
-
<script>
|
|
119
|
-
SwaggerUIBundle({ url: '/swagger/openapi.json', dom_id: '#swagger-ui' });
|
|
120
|
-
</script>
|
|
121
|
-
</body>
|
|
122
|
-
</html>
|
|
123
|
-
HTML
|
|
124
|
-
[200, { "content-type" => "text/html; charset=utf-8" }, [html]]
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def serve_openapi_json
|
|
128
|
-
spec = Tina4::Swagger.generate
|
|
129
|
-
[200, { "content-type" => "application/json; charset=utf-8" }, [JSON.generate(spec)]]
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def handle_403
|
|
133
|
-
body = Tina4::Template.render_error(403) rescue "403 Forbidden"
|
|
134
|
-
[403, { "content-type" => "text/html" }, [body]]
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def handle_404(path)
|
|
138
|
-
Tina4::Debug.warning("404 Not Found: #{path}")
|
|
139
|
-
body = Tina4::Template.render_error(404) rescue "404 Not Found"
|
|
140
|
-
[404, { "content-type" => "text/html" }, [body]]
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def handle_500(error)
|
|
144
|
-
Tina4::Debug.error("500 Internal Server Error: #{error.message}")
|
|
145
|
-
Tina4::Debug.error(error.backtrace&.first(10)&.join("\n"))
|
|
146
|
-
body = Tina4::Template.render_error(500) rescue "500 Internal Server Error: #{error.message}"
|
|
147
|
-
[500, { "content-type" => "text/html" }, [body]]
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
end
|
data/lib/tina4/request.rb
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "uri"
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
|
-
module Tina4
|
|
6
|
-
class Request
|
|
7
|
-
attr_reader :env, :method, :path, :query_string, :content_type,
|
|
8
|
-
:path_params, :ip
|
|
9
|
-
|
|
10
|
-
def initialize(env, path_params = {})
|
|
11
|
-
@env = env
|
|
12
|
-
@method = env["REQUEST_METHOD"]
|
|
13
|
-
@path = env["PATH_INFO"] || "/"
|
|
14
|
-
@query_string = env["QUERY_STRING"] || ""
|
|
15
|
-
@content_type = env["CONTENT_TYPE"] || ""
|
|
16
|
-
@ip = env["REMOTE_ADDR"] || "127.0.0.1"
|
|
17
|
-
@path_params = path_params
|
|
18
|
-
|
|
19
|
-
# Lazy-initialized fields (nil = not yet computed)
|
|
20
|
-
@headers = nil
|
|
21
|
-
@cookies = nil
|
|
22
|
-
@session = nil
|
|
23
|
-
@body = nil
|
|
24
|
-
@params = nil
|
|
25
|
-
@files = nil
|
|
26
|
-
@json_body = nil
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Lazy accessors — only compute when needed
|
|
30
|
-
def headers
|
|
31
|
-
@headers ||= extract_headers
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def cookies
|
|
35
|
-
@cookies ||= parse_cookies
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def session
|
|
39
|
-
@session ||= @env["tina4.session"] || {}
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def body
|
|
43
|
-
@body ||= read_body
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def files
|
|
47
|
-
@files ||= extract_files
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def params
|
|
51
|
-
@params ||= build_params
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def [](key)
|
|
55
|
-
params[key.to_s] || params[key.to_sym] || @path_params[key.to_sym]
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def header(name)
|
|
59
|
-
headers[name.to_s.downcase.gsub("-", "_")]
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def json_body
|
|
63
|
-
@json_body ||= begin
|
|
64
|
-
JSON.parse(body)
|
|
65
|
-
rescue JSON::ParserError, TypeError
|
|
66
|
-
{}
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def bearer_token
|
|
71
|
-
auth = header("authorization") || ""
|
|
72
|
-
auth.sub(/\ABearer\s+/i, "") if auth =~ /\ABearer\s+/i
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
private
|
|
76
|
-
|
|
77
|
-
def extract_headers
|
|
78
|
-
h = {}
|
|
79
|
-
@env.each do |key, value|
|
|
80
|
-
if key.start_with?("HTTP_")
|
|
81
|
-
h[key[5..].downcase] = value
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
h
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def parse_cookies
|
|
88
|
-
cookie_str = @env["HTTP_COOKIE"]
|
|
89
|
-
return {} unless cookie_str && !cookie_str.empty?
|
|
90
|
-
|
|
91
|
-
result = {}
|
|
92
|
-
cookie_str.split(";").each do |pair|
|
|
93
|
-
key, value = pair.strip.split("=", 2)
|
|
94
|
-
result[key] = value if key
|
|
95
|
-
end
|
|
96
|
-
result
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def read_body
|
|
100
|
-
input = @env["rack.input"]
|
|
101
|
-
return "" unless input
|
|
102
|
-
input.rewind if input.respond_to?(:rewind)
|
|
103
|
-
data = input.read || ""
|
|
104
|
-
input.rewind if input.respond_to?(:rewind)
|
|
105
|
-
data
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def build_params
|
|
109
|
-
p = {}
|
|
110
|
-
|
|
111
|
-
# Query string params
|
|
112
|
-
parse_query_string(@query_string, p) unless @query_string.empty?
|
|
113
|
-
|
|
114
|
-
# Body params
|
|
115
|
-
if @content_type.include?("application/json")
|
|
116
|
-
json_body.each { |k, v| p[k.to_s] = v }
|
|
117
|
-
elsif @content_type.include?("application/x-www-form-urlencoded")
|
|
118
|
-
parse_query_string(body, p)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# Path params (highest priority)
|
|
122
|
-
@path_params.each { |k, v| p[k.to_s] = v }
|
|
123
|
-
p
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def parse_query_string(qs, target = {})
|
|
127
|
-
return target if qs.nil? || qs.empty?
|
|
128
|
-
qs.split("&").each do |pair|
|
|
129
|
-
key, value = pair.split("=", 2)
|
|
130
|
-
target[URI.decode_www_form_component(key.to_s)] = URI.decode_www_form_component(value.to_s)
|
|
131
|
-
end
|
|
132
|
-
target
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def extract_files
|
|
136
|
-
result = {}
|
|
137
|
-
return result unless @content_type.include?("multipart/form-data")
|
|
138
|
-
begin
|
|
139
|
-
form_hash = @env["rack.request.form_hash"]
|
|
140
|
-
if form_hash
|
|
141
|
-
form_hash.each do |key, value|
|
|
142
|
-
if value.is_a?(Hash) && value[:tempfile]
|
|
143
|
-
result[key] = {
|
|
144
|
-
filename: value[:filename],
|
|
145
|
-
type: value[:type],
|
|
146
|
-
tempfile: value[:tempfile],
|
|
147
|
-
size: value[:tempfile].size
|
|
148
|
-
}
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
rescue StandardError
|
|
153
|
-
# Multipart parsing failed
|
|
154
|
-
end
|
|
155
|
-
result
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
data/lib/tina4/response.rb
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "json"
|
|
3
|
-
require "uri"
|
|
4
|
-
|
|
5
|
-
module Tina4
|
|
6
|
-
class Response
|
|
7
|
-
MIME_TYPES = {
|
|
8
|
-
".html" => "text/html", ".htm" => "text/html",
|
|
9
|
-
".css" => "text/css", ".js" => "application/javascript",
|
|
10
|
-
".json" => "application/json", ".xml" => "application/xml",
|
|
11
|
-
".txt" => "text/plain", ".csv" => "text/csv",
|
|
12
|
-
".png" => "image/png", ".jpg" => "image/jpeg",
|
|
13
|
-
".jpeg" => "image/jpeg", ".gif" => "image/gif",
|
|
14
|
-
".svg" => "image/svg+xml", ".ico" => "image/x-icon",
|
|
15
|
-
".webp" => "image/webp", ".pdf" => "application/pdf",
|
|
16
|
-
".zip" => "application/zip", ".woff" => "font/woff",
|
|
17
|
-
".woff2" => "font/woff2", ".ttf" => "font/ttf",
|
|
18
|
-
".eot" => "application/vnd.ms-fontobject",
|
|
19
|
-
".mp3" => "audio/mpeg", ".mp4" => "video/mp4",
|
|
20
|
-
".webm" => "video/webm"
|
|
21
|
-
}.freeze
|
|
22
|
-
|
|
23
|
-
# Pre-frozen header values
|
|
24
|
-
JSON_CONTENT_TYPE = "application/json; charset=utf-8"
|
|
25
|
-
HTML_CONTENT_TYPE = "text/html; charset=utf-8"
|
|
26
|
-
TEXT_CONTENT_TYPE = "text/plain; charset=utf-8"
|
|
27
|
-
XML_CONTENT_TYPE = "application/xml; charset=utf-8"
|
|
28
|
-
|
|
29
|
-
attr_accessor :status, :headers, :body, :cookies
|
|
30
|
-
|
|
31
|
-
def initialize
|
|
32
|
-
@status = 200
|
|
33
|
-
@headers = { "content-type" => HTML_CONTENT_TYPE }
|
|
34
|
-
@body = ""
|
|
35
|
-
@cookies = nil # Lazy — most responses have no cookies
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def json(data, status_or_opts = nil, status: nil)
|
|
39
|
-
@status = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
|
|
40
|
-
@headers["content-type"] = JSON_CONTENT_TYPE
|
|
41
|
-
@body = data.is_a?(String) ? data : JSON.generate(data)
|
|
42
|
-
self
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def html(content, status_or_opts = nil, status: nil)
|
|
46
|
-
@status = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
|
|
47
|
-
@headers["content-type"] = HTML_CONTENT_TYPE
|
|
48
|
-
@body = content.to_s
|
|
49
|
-
self
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def text(content, status_or_opts = nil, status: nil)
|
|
53
|
-
@status = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
|
|
54
|
-
@headers["content-type"] = TEXT_CONTENT_TYPE
|
|
55
|
-
@body = content.to_s
|
|
56
|
-
self
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def xml(content, status: 200)
|
|
60
|
-
@status = status
|
|
61
|
-
@headers["content-type"] = XML_CONTENT_TYPE
|
|
62
|
-
@body = content.to_s
|
|
63
|
-
self
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def csv(content, filename: "export.csv", status: 200)
|
|
67
|
-
@status = status
|
|
68
|
-
@headers["content-type"] = "text/csv"
|
|
69
|
-
@headers["content-disposition"] = "attachment; filename=\"#{filename}\""
|
|
70
|
-
@body = content.to_s
|
|
71
|
-
self
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def redirect(url, status: 302)
|
|
75
|
-
@status = status
|
|
76
|
-
@headers["location"] = url
|
|
77
|
-
@body = ""
|
|
78
|
-
self
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def file(path, content_type: nil, download: false)
|
|
82
|
-
unless ::File.exist?(path)
|
|
83
|
-
@status = 404
|
|
84
|
-
@body = "File not found"
|
|
85
|
-
return self
|
|
86
|
-
end
|
|
87
|
-
ext = ::File.extname(path).downcase
|
|
88
|
-
@headers["content-type"] = content_type || MIME_TYPES[ext] || "application/octet-stream"
|
|
89
|
-
if download
|
|
90
|
-
@headers["content-disposition"] = "attachment; filename=\"#{::File.basename(path)}\""
|
|
91
|
-
end
|
|
92
|
-
@body = ::File.binread(path)
|
|
93
|
-
self
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def render(template_path, data = {}, status: 200)
|
|
97
|
-
@status = status
|
|
98
|
-
@headers["content-type"] = HTML_CONTENT_TYPE
|
|
99
|
-
@body = Tina4::Template.render(template_path, data)
|
|
100
|
-
self
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def set_cookie(name, value, opts = {})
|
|
104
|
-
cookie = "#{name}=#{URI.encode_www_form_component(value)}"
|
|
105
|
-
cookie += "; Path=#{opts[:path] || '/'}"
|
|
106
|
-
cookie += "; HttpOnly" if opts.fetch(:http_only, true)
|
|
107
|
-
cookie += "; Secure" if opts[:secure]
|
|
108
|
-
cookie += "; SameSite=#{opts[:same_site] || 'Lax'}"
|
|
109
|
-
cookie += "; Max-Age=#{opts[:max_age]}" if opts[:max_age]
|
|
110
|
-
cookie += "; Expires=#{opts[:expires].httpdate}" if opts[:expires]
|
|
111
|
-
@cookies ||= []
|
|
112
|
-
@cookies << cookie
|
|
113
|
-
self
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def delete_cookie(name, path: "/")
|
|
117
|
-
set_cookie(name, "", max_age: 0, path: path)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def add_header(key, value)
|
|
121
|
-
@headers[key] = value
|
|
122
|
-
self
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def add_cors_headers(origin: "*", methods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
126
|
-
headers_list: "Content-Type, Authorization, Accept", credentials: false)
|
|
127
|
-
@headers["access-control-allow-origin"] = origin
|
|
128
|
-
@headers["access-control-allow-methods"] = methods
|
|
129
|
-
@headers["access-control-allow-headers"] = headers_list
|
|
130
|
-
@headers["access-control-allow-credentials"] = "true" if credentials
|
|
131
|
-
@headers["access-control-max-age"] = "86400"
|
|
132
|
-
self
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def to_rack
|
|
136
|
-
# Fast path: no cookies (99% of API responses)
|
|
137
|
-
if @cookies.nil? || @cookies.empty?
|
|
138
|
-
return [@status, @headers, [@body.to_s]]
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Cookie path
|
|
142
|
-
final_headers = @headers.dup
|
|
143
|
-
final_headers["set-cookie"] = @cookies.join("\n")
|
|
144
|
-
[@status, final_headers, [@body.to_s]]
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def self.auto_detect(result, response)
|
|
148
|
-
case result
|
|
149
|
-
when Tina4::Response
|
|
150
|
-
result
|
|
151
|
-
when Hash, Array
|
|
152
|
-
response.json(result)
|
|
153
|
-
when String
|
|
154
|
-
if result.start_with?("<")
|
|
155
|
-
response.html(result)
|
|
156
|
-
else
|
|
157
|
-
response.text(result)
|
|
158
|
-
end
|
|
159
|
-
when Integer
|
|
160
|
-
response.status = result
|
|
161
|
-
response.body = ""
|
|
162
|
-
response
|
|
163
|
-
when NilClass
|
|
164
|
-
response.status = 204
|
|
165
|
-
response.body = ""
|
|
166
|
-
response
|
|
167
|
-
else
|
|
168
|
-
response.json(result.respond_to?(:to_hash) ? result.to_hash : { data: result.to_s })
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
end
|