hyraft 0.1.0.alpha1
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 +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +231 -0
- data/exe/hyraft +5 -0
- data/lib/hyraft/boot/asset_preloader.rb +185 -0
- data/lib/hyraft/boot/preloaded_static.rb +46 -0
- data/lib/hyraft/boot/preloader.rb +206 -0
- data/lib/hyraft/cli.rb +187 -0
- data/lib/hyraft/compiler/compiler.rb +34 -0
- data/lib/hyraft/compiler/html_purifier.rb +181 -0
- data/lib/hyraft/compiler/javascript_library.rb +281 -0
- data/lib/hyraft/compiler/javascript_obfuscator.rb +141 -0
- data/lib/hyraft/compiler/parser.rb +27 -0
- data/lib/hyraft/compiler/renderer.rb +217 -0
- data/lib/hyraft/engine/circuit.rb +35 -0
- data/lib/hyraft/engine/port.rb +17 -0
- data/lib/hyraft/engine/source.rb +19 -0
- data/lib/hyraft/engine.rb +11 -0
- data/lib/hyraft/router/api_router.rb +65 -0
- data/lib/hyraft/router/web_router.rb +136 -0
- data/lib/hyraft/system_info.rb +26 -0
- data/lib/hyraft/version.rb +5 -0
- data/lib/hyraft.rb +48 -0
- data/templates/do_app/Gemfile +50 -0
- data/templates/do_app/Rakefile +88 -0
- data/templates/do_app/adapter-intake/web-app/display/pages/home/home.hyr +174 -0
- data/templates/do_app/adapter-intake/web-app/request/home_web_adapter.rb +19 -0
- data/templates/do_app/boot.rb +41 -0
- data/templates/do_app/framework/adapters/server/server_api_adapter.rb +51 -0
- data/templates/do_app/framework/adapters/server/server_web_adapter.rb +178 -0
- data/templates/do_app/framework/compiler/style_resolver.rb +33 -0
- data/templates/do_app/framework/errors/error_handler.rb +75 -0
- data/templates/do_app/framework/errors/templates/304.html +22 -0
- data/templates/do_app/framework/errors/templates/400.html +22 -0
- data/templates/do_app/framework/errors/templates/401.html +22 -0
- data/templates/do_app/framework/errors/templates/403.html +22 -0
- data/templates/do_app/framework/errors/templates/404.html +62 -0
- data/templates/do_app/framework/errors/templates/500.html +73 -0
- data/templates/do_app/framework/middleware/cors_middleware.rb +37 -0
- data/templates/do_app/infra/config/environment.rb +86 -0
- data/templates/do_app/infra/config/error_config.rb +80 -0
- data/templates/do_app/infra/config/routes/api_routes.rb +2 -0
- data/templates/do_app/infra/config/routes/web_routes.rb +10 -0
- data/templates/do_app/infra/database/sequel_connection.rb +62 -0
- data/templates/do_app/infra/gems/database.rb +7 -0
- data/templates/do_app/infra/gems/load_all.rb +4 -0
- data/templates/do_app/infra/gems/utilities.rb +1 -0
- data/templates/do_app/infra/gems/web.rb +3 -0
- data/templates/do_app/infra/server/api-server.ru +13 -0
- data/templates/do_app/infra/server/web-server.ru +32 -0
- data/templates/do_app/package.json +9 -0
- data/templates/do_app/public/favicon.ico +0 -0
- data/templates/do_app/public/icons/docs.svg +10 -0
- data/templates/do_app/public/icons/expli.svg +13 -0
- data/templates/do_app/public/icons/git-repo.svg +13 -0
- data/templates/do_app/public/icons/hexagonal-arch.svg +15 -0
- data/templates/do_app/public/icons/template-engine.svg +26 -0
- data/templates/do_app/public/images/hyr-logo.png +0 -0
- data/templates/do_app/public/images/hyr-logo.webp +0 -0
- data/templates/do_app/public/index.html +22 -0
- data/templates/do_app/public/styles/css/main.css +418 -0
- data/templates/do_app/public/styles/css/spa.css +171 -0
- data/templates/do_app/shared/helpers/pagination_helper.rb +44 -0
- data/templates/do_app/shared/helpers/response_formatter.rb +25 -0
- data/templates/do_app/test/acceptance/api/articles_api_acceptance_test.rb +43 -0
- data/templates/do_app/test/acceptance/web/articles_acceptance_test.rb +31 -0
- data/templates/do_app/test/acceptance/web/home_acceptance_test.rb +17 -0
- data/templates/do_app/test/db.rb +106 -0
- data/templates/do_app/test/integration/adapter-exhaust/data-gateway/sequel_articles_gateway_test.rb +79 -0
- data/templates/do_app/test/integration/adapter-intake/api-app/request/articles_api_adapter_test.rb +61 -0
- data/templates/do_app/test/integration/adapter-intake/web-app/request/articles_web_adapter_test.rb +20 -0
- data/templates/do_app/test/integration/adapter-intake/web-app/request/home_web_adapter_test.rb +17 -0
- data/templates/do_app/test/integration/database/migration_test.rb +35 -0
- data/templates/do_app/test/support/mock_api_adapter.rb +82 -0
- data/templates/do_app/test/support/mock_articles_gateway.rb +41 -0
- data/templates/do_app/test/support/mock_web_adapter.rb +85 -0
- data/templates/do_app/test/support/test_patches.rb +33 -0
- data/templates/do_app/test/test_helper.rb +526 -0
- data/templates/do_app/test/unit/engine/circuit/articles_circuit_test.rb +167 -0
- data/templates/do_app/test/unit/engine/port/articles_gateway_port_test.rb +12 -0
- data/templates/do_app/test/unit/engine/source/article_test.rb +37 -0
- metadata +291 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# boot.rb
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
require_relative 'infra/gems/load_all'
|
|
4
|
+
|
|
5
|
+
require 'hyraft'
|
|
6
|
+
require 'sequel'
|
|
7
|
+
|
|
8
|
+
require_relative 'framework/middleware/cors_middleware'
|
|
9
|
+
|
|
10
|
+
ROOT = File.expand_path(__dir__) unless defined?(ROOT)
|
|
11
|
+
|
|
12
|
+
def require_root(path)
|
|
13
|
+
full_path = File.join(ROOT, path)
|
|
14
|
+
require full_path
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Load environment configuration FIRST
|
|
18
|
+
require_relative 'infra/config/environment'
|
|
19
|
+
|
|
20
|
+
# Load middleware
|
|
21
|
+
Dir["#{ROOT}/framework/middleware/*.rb"].each { |file| require file }
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Dir["#{ROOT}/**/*.rb"].each do |file|
|
|
26
|
+
# Skip files that should not be auto-loaded
|
|
27
|
+
next if file.include?('framework/middleware/') ||
|
|
28
|
+
file.include?('boot.rb') ||
|
|
29
|
+
file.end_with?('.ru') ||
|
|
30
|
+
file.end_with?('.hyr') ||
|
|
31
|
+
file.include?('infra/database/migrations/') ||
|
|
32
|
+
file.include?('test/') # Skip test files
|
|
33
|
+
require file
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Initialize the application after all files are loaded
|
|
38
|
+
Hyraft::Environment.load if defined?(Hyraft::Environment)
|
|
39
|
+
|
|
40
|
+
# Use the Environment module
|
|
41
|
+
puts "Starting #{Environment.environment_color} environment"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# framework/adapters/server/server_api_adapter.rb
|
|
2
|
+
require 'cgi'
|
|
3
|
+
|
|
4
|
+
class ServerApiAdapter
|
|
5
|
+
def initialize(router = Api)
|
|
6
|
+
@router = router
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(rack_env)
|
|
10
|
+
req = Rack::Request.new(rack_env)
|
|
11
|
+
|
|
12
|
+
# Pass full path with query string to router
|
|
13
|
+
full_path = req.path_info + (req.query_string.empty? ? '' : "?#{req.query_string}")
|
|
14
|
+
route, route_params = @router.resolve(req.request_method, full_path)
|
|
15
|
+
|
|
16
|
+
if route
|
|
17
|
+
handler_class = route.handler_class
|
|
18
|
+
action = route.action_name
|
|
19
|
+
handler = handler_class.new
|
|
20
|
+
|
|
21
|
+
# Create clean request object
|
|
22
|
+
request_object = {
|
|
23
|
+
query_string: req.query_string,
|
|
24
|
+
query_params: CGI.parse(req.query_string).transform_values { |v| v.length == 1 ? v.first : v },
|
|
25
|
+
body: req.body&.read,
|
|
26
|
+
headers: {
|
|
27
|
+
content_type: req.content_type,
|
|
28
|
+
content_length: req.content_length
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Read body for POST/PUT
|
|
33
|
+
if req.post? || req.put?
|
|
34
|
+
request_object[:body] = req.body.read
|
|
35
|
+
req.body.rewind
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Call handler with clean request object
|
|
39
|
+
if [:show, :update, :delete].include?(action)
|
|
40
|
+
# Pass route param (id) as separate argument
|
|
41
|
+
id = route_params.first if route_params.any?
|
|
42
|
+
handler.public_send(action, request_object, id)
|
|
43
|
+
else
|
|
44
|
+
# No route params for index/create
|
|
45
|
+
handler.public_send(action, request_object)
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
[404, { 'Content-Type' => 'application/json' }, [{ error: 'Not Found' }.to_json]]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# framework/adapters/server/server_web_adapter.rb
|
|
2
|
+
|
|
3
|
+
require_root 'framework/compiler/style_resolver'
|
|
4
|
+
require_root 'framework/errors/error_handler'
|
|
5
|
+
require_root 'infra/config/error_config'
|
|
6
|
+
|
|
7
|
+
class ServerWebAdapter
|
|
8
|
+
def initialize(router = Web)
|
|
9
|
+
@router = router
|
|
10
|
+
# Set up error config with template finder
|
|
11
|
+
ErrorConfig.set_template_finder(method(:find_template_anywhere))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(env)
|
|
15
|
+
req = Rack::Request.new(env)
|
|
16
|
+
method = env['REQUEST_METHOD']
|
|
17
|
+
path = env['PATH_INFO']
|
|
18
|
+
|
|
19
|
+
resolved = @router.resolve(method, path)
|
|
20
|
+
|
|
21
|
+
if resolved
|
|
22
|
+
route, params = resolved
|
|
23
|
+
handler_class, action = route.handler_class, route.action
|
|
24
|
+
handler = handler_class.new
|
|
25
|
+
req.update_param('route_params', params)
|
|
26
|
+
|
|
27
|
+
# Call handler
|
|
28
|
+
result = handler.public_send(action, req) || {}
|
|
29
|
+
status = (result[:status] || 200).to_i
|
|
30
|
+
locals = result[:locals] || {}
|
|
31
|
+
view_template = result[:display]
|
|
32
|
+
|
|
33
|
+
# EXPLICIT: If a display template is specified, ALWAYS use it
|
|
34
|
+
if view_template
|
|
35
|
+
begin
|
|
36
|
+
body = render_hyraft(view_template, locals)
|
|
37
|
+
return [status, { 'Content-Type' => 'text/html; charset=utf-8' }, [body]]
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
return handle_error(500, e)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Handle redirects
|
|
44
|
+
if result[:status] && (300..399).include?(result[:status].to_i) && result[:headers] && result[:headers]['Location']
|
|
45
|
+
return [result[:status], result[:headers], ['']]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# If no explicit template, then handle errors
|
|
49
|
+
if status >= 400
|
|
50
|
+
return handle_error(status, nil, path, locals)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Success with no template
|
|
54
|
+
return [status, { 'Content-Type' => 'text/html; charset=utf-8' }, ['']]
|
|
55
|
+
else
|
|
56
|
+
# route not found -> 404
|
|
57
|
+
return handle_error(404, nil, path)
|
|
58
|
+
end
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
return handle_error(500, e)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# ADD THIS MISSING METHOD
|
|
66
|
+
def handle_error(status, exception = nil, path = nil, locals = {})
|
|
67
|
+
# Check if custom template exists
|
|
68
|
+
if ErrorConfig.custom_template_exists?(status)
|
|
69
|
+
begin
|
|
70
|
+
template_path = ErrorConfig.custom_template_for(status)
|
|
71
|
+
error_locals = ErrorConfig.custom_locals_for(status, path || exception, locals)
|
|
72
|
+
|
|
73
|
+
body = render_hyraft(template_path, error_locals.merge(locals))
|
|
74
|
+
return [status, { 'Content-Type' => 'text/html; charset=utf-8' }, [body]]
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
# Fall back to default error handler if custom template fails
|
|
77
|
+
puts "Custom error template failed: #{e.message}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Fall back to default error handler
|
|
82
|
+
dispatch_error_status(status, path, locals.merge(exception: exception))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Map status -> ErrorHandler method, allow fallback/defaults
|
|
86
|
+
def dispatch_error_status(status, request_path, locals)
|
|
87
|
+
# prefer explicit methods like render_404, render_401, etc.
|
|
88
|
+
method_name = "render_#{status}"
|
|
89
|
+
|
|
90
|
+
if ErrorHandler.respond_to?(method_name)
|
|
91
|
+
case status
|
|
92
|
+
when 404
|
|
93
|
+
return ErrorHandler.public_send(method_name, (locals[:path] || request_path))
|
|
94
|
+
when 304
|
|
95
|
+
return ErrorHandler.public_send(method_name) # usually no placeholders
|
|
96
|
+
when 400, 401, 403
|
|
97
|
+
# pass an Exception-like object when templates expect {{message}}/{{backtrace}}
|
|
98
|
+
exc = locals[:exception] || locals[:error] || StandardError.new(locals[:message].to_s)
|
|
99
|
+
return ErrorHandler.public_send(method_name, exc)
|
|
100
|
+
when 500..599
|
|
101
|
+
exc = locals[:exception] || locals[:error] || StandardError.new(locals[:message].to_s)
|
|
102
|
+
return ErrorHandler.public_send(method_name, exc)
|
|
103
|
+
else
|
|
104
|
+
# Generic: try to call with locals if available
|
|
105
|
+
return ErrorHandler.public_send(method_name, locals) rescue generic_fallback(status, locals)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# If ErrorHandler has a generic `render(status, options)` method, use it
|
|
110
|
+
if ErrorHandler.respond_to?(:render)
|
|
111
|
+
return ErrorHandler.render(status, locals)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# final fallback: plain text minimal response
|
|
115
|
+
body = locals[:message] || "HTTP #{status}"
|
|
116
|
+
return [status, { 'Content-Type' => 'text/plain; charset=utf-8' }, [body]]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Generic fallback used in rescue above
|
|
120
|
+
def generic_fallback(status, locals)
|
|
121
|
+
if ErrorHandler.respond_to?(:render)
|
|
122
|
+
return ErrorHandler.render(status, locals)
|
|
123
|
+
else
|
|
124
|
+
return [status, { 'Content-Type' => 'text/plain; charset=utf-8' }, [locals[:message] || "HTTP #{status}"]]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Render .hyr templates using Hyraft compiler with auto-detect
|
|
129
|
+
def render_hyraft(template_path, locals = {})
|
|
130
|
+
template_key = template_path.gsub(/\.hyr$/, '')
|
|
131
|
+
|
|
132
|
+
# Use preloader if available
|
|
133
|
+
if defined?(Hyraft::Preloader) && Hyraft::Preloader.template_preloaded?(template_key)
|
|
134
|
+
return Hyraft::Preloader.render_template(template_key, locals)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Fallback to file compilation
|
|
138
|
+
template_file = find_template_anywhere(template_path)
|
|
139
|
+
|
|
140
|
+
unless File.exist?(template_file)
|
|
141
|
+
raise "Template not found: #{template_path}. Searched in all apps under adapter-intake"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
layout_file = File.join(ROOT, 'public', 'index.html')
|
|
145
|
+
compiler = HyraftCompiler.new(layout_file)
|
|
146
|
+
locals.each { |k, v| instance_variable_set("@#{k}", v) }
|
|
147
|
+
compiler.compile(template_file, locals)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Search all apps under adapter-intake for the template
|
|
151
|
+
def find_template_anywhere(template_name)
|
|
152
|
+
# Remove .hyr extension if present for flexible matching
|
|
153
|
+
clean_name = template_name.gsub(/\.hyr$/, '')
|
|
154
|
+
|
|
155
|
+
# Search pattern: adapter-intake/*/display/**/{template_name}.hyr
|
|
156
|
+
search_pattern = File.join(ROOT, 'adapter-intake', '*', 'display', '**', "#{clean_name}.hyr")
|
|
157
|
+
matching_files = Dir.glob(search_pattern)
|
|
158
|
+
|
|
159
|
+
if matching_files.any?
|
|
160
|
+
# Return the first match (you could add priority logic here)
|
|
161
|
+
return matching_files.first
|
|
162
|
+
else
|
|
163
|
+
# Show available templates for better debugging
|
|
164
|
+
available_templates = Dir.glob(File.join(ROOT, 'adapter-intake', '*', 'display', '**', '*.hyr'))
|
|
165
|
+
# puts "DEBUG: Available templates:"
|
|
166
|
+
# available_templates.each { |f| puts " - #{f.gsub(ROOT + '/', '')}" }
|
|
167
|
+
|
|
168
|
+
raise "Template '#{template_name}' not found in any app under adapter-intake"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def compile_styles(hyr_file_path, style_src)
|
|
173
|
+
Compiler::StyleResolver.resolve_style_path(style_src, from: hyr_file_path)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Optional: Keep alias for backward compatibility if needed
|
|
177
|
+
alias_method :render_hyraft_with_preloader, :render_hyraft
|
|
178
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# framework/compiler/style_resolver.rb
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Compiler
|
|
6
|
+
module StyleResolver
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def resolve_style_path(path, from:)
|
|
10
|
+
# normalize path (remove leading slash if present)
|
|
11
|
+
rel_path = path.start_with?('/') ? path.sub(%r{^/}, '') : path
|
|
12
|
+
|
|
13
|
+
# 1) public/ takes priority: public/styles/css/example.css -> /styles/css/example.css
|
|
14
|
+
public_path = File.join(ROOT, 'public', rel_path)
|
|
15
|
+
return "/#{rel_path}" if File.exist?(public_path)
|
|
16
|
+
|
|
17
|
+
# 2) FIXED: Update to new structure path
|
|
18
|
+
local_path = File.expand_path(File.join(File.dirname(from), rel_path))
|
|
19
|
+
|
|
20
|
+
# CORRECT: New structure path
|
|
21
|
+
display_root = File.expand_path(File.join(ROOT,'adapter-intake'))
|
|
22
|
+
|
|
23
|
+
if local_path.start_with?(display_root) && File.exist?(local_path)
|
|
24
|
+
# compute path relative to adapter-intake and return a browser URL
|
|
25
|
+
relative = Pathname.new(local_path).relative_path_from(Pathname.new(display_root)).to_s
|
|
26
|
+
return "/#{relative}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# not found
|
|
30
|
+
raise "Stylesheet not found: #{path} (searched #{public_path} and #{local_path})"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
class ErrorHandler
|
|
2
|
+
# 400 - Bad Request
|
|
3
|
+
def self.render_400(exception = nil)
|
|
4
|
+
template_file = File.join(ROOT, 'framework', 'errors', 'templates', '400.html')
|
|
5
|
+
unless File.exist?(template_file)
|
|
6
|
+
msg = exception ? exception.message : "Bad Request"
|
|
7
|
+
return [400, { 'Content-Type' => 'text/html; charset=utf-8' }, ["400 - Bad Request: #{msg}"]]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
body = File.read(template_file)
|
|
11
|
+
body = body.gsub('{{message}}', exception&.message.to_s)
|
|
12
|
+
[400, { 'Content-Type' => 'text/html; charset=utf-8' }, [body]]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# 401 - Unauthorized
|
|
16
|
+
def self.render_401(exception = nil)
|
|
17
|
+
template_file = File.join(ROOT, 'framework', 'errors', 'templates', '401.html')
|
|
18
|
+
unless File.exist?(template_file)
|
|
19
|
+
msg = exception ? exception.message : "Unauthorized"
|
|
20
|
+
return [401, { 'Content-Type' => 'text/html; charset=utf-8' }, ["401 - Unauthorized: #{msg}"]]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
body = File.read(template_file)
|
|
24
|
+
body = body.gsub('{{message}}', exception&.message.to_s)
|
|
25
|
+
[401, { 'Content-Type' => 'text/html; charset=utf-8' }, [body]]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 403 - Forbidden
|
|
29
|
+
def self.render_403(exception = nil)
|
|
30
|
+
template_file = File.join(ROOT, 'framework', 'errors', 'templates', '403.html')
|
|
31
|
+
unless File.exist?(template_file)
|
|
32
|
+
msg = exception ? exception.message : "Forbidden"
|
|
33
|
+
return [403, { 'Content-Type' => 'text/html; charset=utf-8' }, ["403 - Forbidden: #{msg}"]]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
body = File.read(template_file)
|
|
37
|
+
body = body.gsub('{{message}}', exception&.message.to_s)
|
|
38
|
+
[403, { 'Content-Type' => 'text/html; charset=utf-8' }, [body]]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 404 - Not Found
|
|
42
|
+
def self.render_404(path)
|
|
43
|
+
template_file = File.join(ROOT, 'framework', 'errors', 'templates', '404.html')
|
|
44
|
+
unless File.exist?(template_file)
|
|
45
|
+
return [404, { 'Content-Type' => 'text/html; charset=utf-8' }, ["404 - Page Not Found"]]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
body = File.read(template_file).gsub('{{path}}', path)
|
|
49
|
+
[404, { 'Content-Type' => 'text/html; charset=utf-8' }, [body]]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# 500 - Internal Server Error
|
|
53
|
+
def self.render_500(exception)
|
|
54
|
+
template_file = File.join(ROOT, 'framework', 'errors', 'templates', '500.html')
|
|
55
|
+
unless File.exist?(template_file)
|
|
56
|
+
return [500, { 'Content-Type' => 'text/html; charset=utf-8' }, ["500 - Internal Server Error: #{exception.message}"]]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
body = File.read(template_file)
|
|
60
|
+
.gsub('{{message}}', exception.message)
|
|
61
|
+
.gsub('{{backtrace}}', exception.backtrace.first(10).join("<br>"))
|
|
62
|
+
[500, { 'Content-Type' => 'text/html; charset=utf-8' }, [body]]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# 304 - Not Modified
|
|
66
|
+
def self.render_304
|
|
67
|
+
template_file = File.join(ROOT, 'framework', 'errors', 'templates', '304.html')
|
|
68
|
+
unless File.exist?(template_file)
|
|
69
|
+
return [304, { 'Content-Type' => 'text/html; charset=utf-8' }, ["304 - Not Modified"]]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
body = File.read(template_file)
|
|
73
|
+
[304, { 'Content-Type' => 'text/html; charset=utf-8' }, [body]]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>304 - Not Modified</title>
|
|
6
|
+
<style>
|
|
7
|
+
* { box-sizing:border-box; margin:0; padding:0; }
|
|
8
|
+
body { font-family:Arial, sans-serif; background:#f8f8f8; display:flex; justify-content:center; align-items:center; height:100vh; overflow:hidden; }
|
|
9
|
+
.container { max-width:800px; background:#fff; padding:40px; border-radius:8px; box-shadow:0 0 20px rgba(0,0,0,0.1); text-align:center; animation:fadeIn 1s ease-in-out; }
|
|
10
|
+
h1 { color:#5bc0de; font-size:48px; margin-bottom:20px; animation:bounce 1s infinite alternate; }
|
|
11
|
+
p { font-size:18px; }
|
|
12
|
+
@keyframes fadeIn { from {opacity:0; transform:translateY(-20px);} to {opacity:1; transform:translateY(0);} }
|
|
13
|
+
@keyframes bounce { 0%{transform:translateY(0);} 50%{transform:translateY(-10px);} 100%{transform:translateY(0);} }
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div class="container">
|
|
18
|
+
<h1>304 - Not Modified</h1>
|
|
19
|
+
<p>The requested resource has not been modified since your last request.</p>
|
|
20
|
+
</div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>400 - Bad Request</title>
|
|
6
|
+
<style>
|
|
7
|
+
* { box-sizing:border-box; margin:0; padding:0; }
|
|
8
|
+
body { font-family:Arial, sans-serif; background:#f8f8f8; display:flex; justify-content:center; align-items:center; height:100vh; overflow:hidden; }
|
|
9
|
+
.container { max-width:800px; background:#fff; padding:40px; border-radius:8px; box-shadow:0 0 20px rgba(0,0,0,0.1); text-align:center; animation:fadeIn 1s ease-in-out; }
|
|
10
|
+
h1 { color:#f0ad4e; font-size:48px; margin-bottom:20px; animation:bounce 1s infinite alternate; }
|
|
11
|
+
p { font-size:18px; }
|
|
12
|
+
@keyframes fadeIn { from {opacity:0; transform:translateY(-20px);} to {opacity:1; transform:translateY(0);} }
|
|
13
|
+
@keyframes bounce { 0%{transform:translateY(0);} 50%{transform:translateY(-10px);} 100%{transform:translateY(0);} }
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div class="container">
|
|
18
|
+
<h1>400 - Bad Request</h1>
|
|
19
|
+
<p>Your request cannot be processed by the server.</p>
|
|
20
|
+
</div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>401 - Unauthorized</title>
|
|
6
|
+
<style>
|
|
7
|
+
* { box-sizing:border-box; margin:0; padding:0; }
|
|
8
|
+
body { font-family:Arial, sans-serif; background:#f8f8f8; display:flex; justify-content:center; align-items:center; height:100vh; overflow:hidden; }
|
|
9
|
+
.container { max-width:800px; background:#fff; padding:40px; border-radius:8px; box-shadow:0 0 20px rgba(0,0,0,0.1); text-align:center; animation:fadeIn 1s ease-in-out; }
|
|
10
|
+
h1 { color:#d9534f; font-size:48px; margin-bottom:20px; animation:bounce 1s infinite alternate; }
|
|
11
|
+
p { font-size:18px; }
|
|
12
|
+
@keyframes fadeIn { from {opacity:0; transform:translateY(-20px);} to {opacity:1; transform:translateY(0);} }
|
|
13
|
+
@keyframes bounce { 0%{transform:translateY(0);} 50%{transform:translateY(-10px);} 100%{transform:translateY(0);} }
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div class="container">
|
|
18
|
+
<h1>401 - Unauthorized</h1>
|
|
19
|
+
<p>You are not authorized to view this page.</p>
|
|
20
|
+
</div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>403 - Forbidden</title>
|
|
6
|
+
<style>
|
|
7
|
+
* { box-sizing:border-box; margin:0; padding:0; }
|
|
8
|
+
body { font-family:Arial, sans-serif; background:#f8f8f8; display:flex; justify-content:center; align-items:center; height:100vh; overflow:hidden; }
|
|
9
|
+
.container { max-width:800px; background:#fff; padding:40px; border-radius:8px; box-shadow:0 0 20px rgba(0,0,0,0.1); text-align:center; animation:fadeIn 1s ease-in-out; }
|
|
10
|
+
h1 { color:#d9534f; font-size:48px; margin-bottom:20px; animation:bounce 1s infinite alternate; }
|
|
11
|
+
p { font-size:18px; }
|
|
12
|
+
@keyframes fadeIn { from {opacity:0; transform:translateY(-20px);} to {opacity:1; transform:translateY(0);} }
|
|
13
|
+
@keyframes bounce { 0%{transform:translateY(0);} 50%{transform:translateY(-10px);} 100%{transform:translateY(0);} }
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div class="container">
|
|
18
|
+
<h1>403 - Forbidden</h1>
|
|
19
|
+
<p>Access to this resource is denied.</p>
|
|
20
|
+
</div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>404 - Not Found</title>
|
|
6
|
+
<style>
|
|
7
|
+
* {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
body {
|
|
14
|
+
font-family: Arial, sans-serif;
|
|
15
|
+
background: #f8f8f8;
|
|
16
|
+
display: flex;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
align-items: center;
|
|
19
|
+
height: 100vh; /* full viewport height */
|
|
20
|
+
overflow: hidden; /* prevent scrollbars */
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.container {
|
|
24
|
+
max-width: 800px;
|
|
25
|
+
background: #fff;
|
|
26
|
+
padding: 40px; /* spacing inside container */
|
|
27
|
+
border-radius: 8px;
|
|
28
|
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
|
29
|
+
text-align: center;
|
|
30
|
+
animation: fadeIn 1s ease-in-out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
h1 {
|
|
34
|
+
color: #f0ad4e;
|
|
35
|
+
font-size: 48px;
|
|
36
|
+
margin-bottom: 20px;
|
|
37
|
+
animation: bounce 1s infinite alternate;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
p {
|
|
41
|
+
font-size: 18px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@keyframes fadeIn {
|
|
45
|
+
from { opacity: 0; transform: translateY(-20px); }
|
|
46
|
+
to { opacity: 1; transform: translateY(0); }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@keyframes bounce {
|
|
50
|
+
0% { transform: translateY(0); }
|
|
51
|
+
50% { transform: translateY(-10px); }
|
|
52
|
+
100% { transform: translateY(0); }
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
<body>
|
|
57
|
+
<div class="container">
|
|
58
|
+
<h1>404 - Page Not Found</h1>
|
|
59
|
+
<p>The requested URL <strong>{{path}}</strong> was not found on this server.</p>
|
|
60
|
+
</div>
|
|
61
|
+
</body>
|
|
62
|
+
</html>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>500 - Internal Server Error</title>
|
|
6
|
+
<style>
|
|
7
|
+
* {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
body {
|
|
14
|
+
font-family: Arial, sans-serif;
|
|
15
|
+
background: #f8f8f8;
|
|
16
|
+
display: flex;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
align-items: center;
|
|
19
|
+
height: 100vh;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.container {
|
|
24
|
+
max-width: 800px;
|
|
25
|
+
background: #fff;
|
|
26
|
+
padding: 40px;
|
|
27
|
+
border-radius: 8px;
|
|
28
|
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
|
29
|
+
text-align: center;
|
|
30
|
+
animation: fadeIn 1s ease-in-out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
h1 {
|
|
34
|
+
color: #d9534f;
|
|
35
|
+
font-size: 48px;
|
|
36
|
+
margin-bottom: 20px;
|
|
37
|
+
animation: bounce 1s infinite alternate;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
p, pre {
|
|
41
|
+
font-size: 16px;
|
|
42
|
+
margin-bottom: 10px;
|
|
43
|
+
word-wrap: break-word;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pre {
|
|
47
|
+
background: #f0f0f0;
|
|
48
|
+
padding: 15px;
|
|
49
|
+
border-radius: 5px;
|
|
50
|
+
text-align: left;
|
|
51
|
+
overflow-x: auto;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@keyframes fadeIn {
|
|
55
|
+
from { opacity: 0; transform: translateY(-20px); }
|
|
56
|
+
to { opacity: 1; transform: translateY(0); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@keyframes bounce {
|
|
60
|
+
0% { transform: translateY(0); }
|
|
61
|
+
50% { transform: translateY(-10px); }
|
|
62
|
+
100% { transform: translateY(0); }
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
65
|
+
</head>
|
|
66
|
+
<body>
|
|
67
|
+
<div class="container">
|
|
68
|
+
<h1>500 - Internal Server Error</h1>
|
|
69
|
+
<p><strong>Error:</strong> {{message}}</p>
|
|
70
|
+
<pre>{{backtrace}}</pre>
|
|
71
|
+
</div>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# framework/middleware/cors_middleware.rb
|
|
2
|
+
class CorsMiddleware
|
|
3
|
+
def initialize(app)
|
|
4
|
+
@app = app
|
|
5
|
+
# puts "CORS Middleware initialized!"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(env)
|
|
9
|
+
# puts "CORS Middleware processing: #{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
|
|
10
|
+
|
|
11
|
+
# Handle preflight OPTIONS request
|
|
12
|
+
if env['REQUEST_METHOD'] == 'OPTIONS'
|
|
13
|
+
# puts "Handling OPTIONS preflight request"
|
|
14
|
+
return [200, cors_headers, []]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
status, headers, body = @app.call(env)
|
|
18
|
+
|
|
19
|
+
# Add CORS headers to all responses
|
|
20
|
+
# puts "Adding CORS headers to response"
|
|
21
|
+
headers.merge!(cors_headers)
|
|
22
|
+
|
|
23
|
+
[status, headers, body]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def cors_headers
|
|
29
|
+
{
|
|
30
|
+
'Access-Control-Allow-Origin' => 'http://localhost:1091',
|
|
31
|
+
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
|
|
32
|
+
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-Requested-With',
|
|
33
|
+
'Access-Control-Allow-Credentials' => 'true',
|
|
34
|
+
'Access-Control-Max-Age' => '86400'
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|