tina4ruby 0.5.2 → 3.0.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +360 -559
  4. data/exe/{tina4 → tina4ruby} +1 -0
  5. data/lib/tina4/ai.rb +312 -0
  6. data/lib/tina4/auth.rb +44 -3
  7. data/lib/tina4/auto_crud.rb +163 -0
  8. data/lib/tina4/cli.rb +242 -77
  9. data/lib/tina4/constants.rb +46 -0
  10. data/lib/tina4/cors.rb +74 -0
  11. data/lib/tina4/database/sqlite3_adapter.rb +139 -0
  12. data/lib/tina4/database.rb +43 -7
  13. data/lib/tina4/debug.rb +4 -79
  14. data/lib/tina4/dev_admin.rb +1162 -0
  15. data/lib/tina4/dev_mailbox.rb +191 -0
  16. data/lib/tina4/dev_reload.rb +9 -9
  17. data/lib/tina4/drivers/firebird_driver.rb +19 -3
  18. data/lib/tina4/drivers/mssql_driver.rb +3 -3
  19. data/lib/tina4/drivers/mysql_driver.rb +4 -4
  20. data/lib/tina4/drivers/postgres_driver.rb +9 -2
  21. data/lib/tina4/drivers/sqlite_driver.rb +1 -1
  22. data/lib/tina4/env.rb +42 -2
  23. data/lib/tina4/error_overlay.rb +252 -0
  24. data/lib/tina4/events.rb +90 -0
  25. data/lib/tina4/field_types.rb +4 -0
  26. data/lib/tina4/frond.rb +1336 -0
  27. data/lib/tina4/gallery/auth/meta.json +1 -0
  28. data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -0
  29. data/lib/tina4/gallery/database/meta.json +1 -0
  30. data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -0
  31. data/lib/tina4/gallery/error-overlay/meta.json +1 -0
  32. data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -0
  33. data/lib/tina4/gallery/orm/meta.json +1 -0
  34. data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -0
  35. data/lib/tina4/gallery/queue/meta.json +1 -0
  36. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +27 -0
  37. data/lib/tina4/gallery/rest-api/meta.json +1 -0
  38. data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -0
  39. data/lib/tina4/gallery/templates/meta.json +1 -0
  40. data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -0
  41. data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -0
  42. data/lib/tina4/health.rb +39 -0
  43. data/lib/tina4/html_element.rb +148 -0
  44. data/lib/tina4/localization.rb +2 -2
  45. data/lib/tina4/log.rb +203 -0
  46. data/lib/tina4/messenger.rb +484 -0
  47. data/lib/tina4/migration.rb +132 -29
  48. data/lib/tina4/orm.rb +337 -31
  49. data/lib/tina4/public/css/tina4.css +178 -1
  50. data/lib/tina4/public/css/tina4.min.css +1 -2
  51. data/lib/tina4/public/favicon.ico +0 -0
  52. data/lib/tina4/public/images/logo.svg +5 -0
  53. data/lib/tina4/public/images/tina4-logo-icon.webp +0 -0
  54. data/lib/tina4/public/js/frond.min.js +420 -0
  55. data/lib/tina4/public/js/tina4-dev-admin.min.js +367 -0
  56. data/lib/tina4/public/js/tina4.min.js +93 -0
  57. data/lib/tina4/public/swagger/index.html +90 -0
  58. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -0
  59. data/lib/tina4/queue.rb +40 -4
  60. data/lib/tina4/queue_backends/lite_backend.rb +88 -0
  61. data/lib/tina4/rack_app.rb +314 -23
  62. data/lib/tina4/rate_limiter.rb +123 -0
  63. data/lib/tina4/request.rb +61 -15
  64. data/lib/tina4/response.rb +54 -24
  65. data/lib/tina4/response_cache.rb +134 -0
  66. data/lib/tina4/router.rb +90 -15
  67. data/lib/tina4/scss_compiler.rb +2 -2
  68. data/lib/tina4/seeder.rb +56 -61
  69. data/lib/tina4/service_runner.rb +303 -0
  70. data/lib/tina4/session.rb +85 -0
  71. data/lib/tina4/session_handlers/mongo_handler.rb +1 -1
  72. data/lib/tina4/session_handlers/valkey_handler.rb +43 -0
  73. data/lib/tina4/shutdown.rb +84 -0
  74. data/lib/tina4/sql_translation.rb +295 -0
  75. data/lib/tina4/template.rb +36 -6
  76. data/lib/tina4/templates/base.twig +2 -2
  77. data/lib/tina4/templates/errors/302.twig +14 -0
  78. data/lib/tina4/templates/errors/401.twig +9 -0
  79. data/lib/tina4/templates/errors/403.twig +22 -15
  80. data/lib/tina4/templates/errors/404.twig +22 -15
  81. data/lib/tina4/templates/errors/500.twig +31 -15
  82. data/lib/tina4/templates/errors/502.twig +9 -0
  83. data/lib/tina4/templates/errors/503.twig +12 -0
  84. data/lib/tina4/templates/errors/base.twig +37 -0
  85. data/lib/tina4/version.rb +1 -1
  86. data/lib/tina4/webserver.rb +28 -18
  87. data/lib/tina4.rb +57 -21
  88. metadata +51 -19
  89. data/lib/tina4/public/js/tina4.js +0 -134
  90. data/lib/tina4/public/js/tina4helper.js +0 -387
data/lib/tina4/cli.rb CHANGED
@@ -1,32 +1,92 @@
1
1
  # frozen_string_literal: true
2
- require "thor"
2
+
3
+ require "optparse"
3
4
  require "fileutils"
4
5
 
5
6
  module Tina4
6
- class CLI < Thor
7
- desc "init [NAME]", "Initialize a new Tina4 project"
8
- option :template, type: :string, default: "default", desc: "Project template"
9
- def init(name = ".")
10
- dir = name == "." ? Dir.pwd : File.join(Dir.pwd, name)
7
+ class CLI
8
+ COMMANDS = %w[init start migrate seed seed:create test version routes console ai help].freeze
9
+
10
+ def self.start(argv)
11
+ new.run(argv)
12
+ end
13
+
14
+ def run(argv)
15
+ command = argv.shift || "help"
16
+ case command
17
+ when "init" then cmd_init(argv)
18
+ when "start" then cmd_start(argv)
19
+ when "migrate" then cmd_migrate(argv)
20
+ when "seed" then cmd_seed(argv)
21
+ when "seed:create" then cmd_seed_create(argv)
22
+ when "test" then cmd_test(argv)
23
+ when "version" then cmd_version
24
+ when "routes" then cmd_routes
25
+ when "console" then cmd_console
26
+ when "ai" then cmd_ai(argv)
27
+ when "help", "-h", "--help" then cmd_help
28
+ else
29
+ puts "Unknown command: #{command}"
30
+ cmd_help
31
+ exit 1
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # ── init ──────────────────────────────────────────────────────────────
38
+
39
+ def cmd_init(argv)
40
+ options = { template: "default" }
41
+ parser = OptionParser.new do |opts|
42
+ opts.banner = "Usage: tina4ruby init [PATH] [options]"
43
+ opts.on("--template TEMPLATE", "Project template (default: default)") { |v| options[:template] = v }
44
+ end
45
+ parser.parse!(argv)
46
+
47
+ name = argv.shift || "."
48
+ dir = File.expand_path(name)
11
49
  FileUtils.mkdir_p(dir)
12
50
 
51
+ project_name = File.basename(dir)
13
52
  create_project_structure(dir)
14
- create_sample_files(dir, name == "." ? File.basename(Dir.pwd) : name)
53
+ create_sample_files(dir, project_name)
15
54
 
16
- puts "Tina4 project initialized in #{dir}"
17
- puts "Run 'cd #{name} && bundle install && tina4 start' to get started" unless name == "."
55
+ puts "\nProject scaffolded at #{dir}"
56
+ if name == "."
57
+ puts " bundle install"
58
+ puts " ruby app.rb"
59
+ else
60
+ puts " cd #{dir}"
61
+ puts " bundle install"
62
+ puts " ruby app.rb"
63
+ end
18
64
  end
19
65
 
20
- desc "start", "Start the Tina4 web server"
21
- option :port, type: :numeric, default: 7145, aliases: "-p"
22
- option :host, type: :string, default: "0.0.0.0", aliases: "-h"
23
- option :dev, type: :boolean, default: false, aliases: "-d", desc: "Enable dev mode with auto-reload"
24
- def start
66
+ # ── start ─────────────────────────────────────────────────────────────
67
+
68
+ def cmd_start(argv)
69
+ options = { port: nil, host: nil, dev: false }
70
+ parser = OptionParser.new do |opts|
71
+ opts.banner = "Usage: tina4ruby start [options]"
72
+ opts.on("-p", "--port PORT", Integer, "Port (default: 7147)") { |v| options[:port] = v }
73
+ opts.on("-h", "--host HOST", "Host (default: 0.0.0.0)") { |v| options[:host] = v }
74
+ opts.on("-d", "--dev", "Enable dev mode with auto-reload") { options[:dev] = true }
75
+ end
76
+ parser.parse!(argv)
77
+
78
+ # Priority: CLI flag > ENV var > default
79
+ options[:port] = resolve_config(:port, options[:port])
80
+ options[:host] = resolve_config(:host, options[:host])
81
+
25
82
  require_relative "../tina4"
26
83
 
27
84
  root_dir = Dir.pwd
28
85
  Tina4.initialize!(root_dir)
29
86
 
87
+ # Register health check endpoint
88
+ Tina4::Health.register!
89
+
30
90
  # Load route files
31
91
  load_routes(root_dir)
32
92
 
@@ -56,20 +116,31 @@ module Tina4
56
116
  user_config.quiet
57
117
  end
58
118
 
59
- Tina4::Debug.info("Starting Puma server on http://#{puma_host}:#{puma_port}")
119
+ Tina4::Log.info("Starting Puma server on http://#{puma_host}:#{puma_port}")
120
+
121
+ # Setup graceful shutdown (Puma manages its own signals, but we handle DB cleanup)
122
+ Tina4::Shutdown.setup
123
+
60
124
  launcher = Puma::Launcher.new(config)
61
125
  launcher.run
62
126
  rescue LoadError
63
- Tina4::Debug.info("Puma not found, falling back to WEBrick")
127
+ Tina4::Log.info("Puma not found, falling back to WEBrick")
64
128
  server = Tina4::WebServer.new(app, host: options[:host], port: options[:port])
65
129
  server.start
66
130
  end
67
131
  end
68
132
 
69
- desc "migrate", "Run database migrations"
70
- option :create, type: :string, desc: "Create a new migration"
71
- option :rollback, type: :numeric, desc: "Rollback N migrations"
72
- def migrate
133
+ # ── migrate ───────────────────────────────────────────────────────────
134
+
135
+ def cmd_migrate(argv)
136
+ options = {}
137
+ parser = OptionParser.new do |opts|
138
+ opts.banner = "Usage: tina4ruby migrate [options]"
139
+ opts.on("--create NAME", "Create a new migration") { |v| options[:create] = v }
140
+ opts.on("--rollback N", Integer, "Rollback N migrations") { |v| options[:rollback] = v }
141
+ end
142
+ parser.parse!(argv)
143
+
73
144
  require_relative "../tina4"
74
145
  Tina4.initialize!(Dir.pwd)
75
146
 
@@ -100,8 +171,59 @@ module Tina4
100
171
  end
101
172
  end
102
173
 
103
- desc "test", "Run inline tests"
104
- def test
174
+ # ── seed ──────────────────────────────────────────────────────────────
175
+
176
+ def cmd_seed(argv)
177
+ options = { clear: false }
178
+ parser = OptionParser.new do |opts|
179
+ opts.banner = "Usage: tina4ruby seed [options]"
180
+ opts.on("--clear", "Clear tables before seeding") { options[:clear] = true }
181
+ end
182
+ parser.parse!(argv)
183
+
184
+ require_relative "../tina4"
185
+ Tina4.initialize!(Dir.pwd)
186
+ load_routes(Dir.pwd)
187
+ Tina4.seed(seed_folder: "seeds", clear: options[:clear])
188
+ end
189
+
190
+ # ── seed:create ───────────────────────────────────────────────────────
191
+
192
+ def cmd_seed_create(argv)
193
+ name = argv.shift
194
+ unless name
195
+ puts "Usage: tina4ruby seed:create NAME"
196
+ exit 1
197
+ end
198
+
199
+ dir = File.join(Dir.pwd, "seeds")
200
+ FileUtils.mkdir_p(dir)
201
+
202
+ existing = Dir.glob(File.join(dir, "*.rb")).select { |f| File.basename(f)[0] =~ /\d/ }.sort
203
+ numbers = existing.map { |f| File.basename(f).match(/^(\d+)/)[1].to_i }
204
+ next_num = numbers.empty? ? 1 : numbers.max + 1
205
+
206
+ clean_name = name.strip.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/^_|_$/, "")
207
+ filename = format("%03d_%s.rb", next_num, clean_name)
208
+ filepath = File.join(dir, filename)
209
+
210
+ File.write(filepath, <<~RUBY)
211
+ # Seed: #{name.strip}
212
+ #
213
+ # This file is executed by `tina4ruby seed`.
214
+ # Use Tina4.seed_orm or Tina4.seed_table to populate data.
215
+ #
216
+ # Examples:
217
+ # Tina4.seed_orm(User, count: 50)
218
+ # Tina4.seed_table("audit_log", { action: :string, created_at: :datetime }, count: 100)
219
+ RUBY
220
+
221
+ puts "Created seed file: #{filepath}"
222
+ end
223
+
224
+ # ── test ──────────────────────────────────────────────────────────────
225
+
226
+ def cmd_test(argv)
105
227
  require_relative "../tina4"
106
228
  Tina4.initialize!(Dir.pwd)
107
229
 
@@ -121,14 +243,16 @@ module Tina4
121
243
  exit(1) if results[:failed] > 0 || results[:errors] > 0
122
244
  end
123
245
 
124
- desc "version", "Show Tina4 version"
125
- def version
246
+ # ── version ───────────────────────────────────────────────────────────
247
+
248
+ def cmd_version
126
249
  require_relative "version"
127
250
  puts "Tina4 Ruby v#{Tina4::VERSION}"
128
251
  end
129
252
 
130
- desc "routes", "List all registered routes"
131
- def routes
253
+ # ── routes ────────────────────────────────────────────────────────────
254
+
255
+ def cmd_routes
132
256
  require_relative "../tina4"
133
257
  Tina4.initialize!(Dir.pwd)
134
258
  load_routes(Dir.pwd)
@@ -143,53 +267,92 @@ module Tina4
143
267
  puts "Total: #{Tina4::Router.routes.length} routes\n"
144
268
  end
145
269
 
146
- desc "seed", "Run all seed files in seeds/"
147
- option :clear, type: :boolean, default: false, desc: "Clear tables before seeding"
148
- def seed
270
+ # ── console ───────────────────────────────────────────────────────────
271
+
272
+ def cmd_console
149
273
  require_relative "../tina4"
150
274
  Tina4.initialize!(Dir.pwd)
151
275
  load_routes(Dir.pwd)
152
- Tina4.seed(seed_folder: "seeds", clear: options[:clear])
276
+
277
+ require "irb"
278
+ IRB.start
153
279
  end
154
280
 
155
- desc "seed:create NAME", "Create a new seed file"
156
- def seed_create(name)
157
- dir = File.join(Dir.pwd, "seeds")
158
- FileUtils.mkdir_p(dir)
281
+ # ── ai ────────────────────────────────────────────────────────────────
159
282
 
160
- existing = Dir.glob(File.join(dir, "*.rb")).select { |f| File.basename(f)[0] =~ /\d/ }.sort
161
- numbers = existing.map { |f| File.basename(f).match(/^(\d+)/)[1].to_i }
162
- next_num = numbers.empty? ? 1 : numbers.max + 1
283
+ def cmd_ai(argv)
284
+ options = { all: false, force: false }
285
+ parser = OptionParser.new do |opts|
286
+ opts.banner = "Usage: tina4ruby ai [options]"
287
+ opts.on("--all", "Install context for ALL AI tools (not just detected ones)") { options[:all] = true }
288
+ opts.on("--force", "Overwrite existing context files") { options[:force] = true }
289
+ end
290
+ parser.parse!(argv)
163
291
 
164
- clean_name = name.strip.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/^_|_$/, "")
165
- filename = format("%03d_%s.rb", next_num, clean_name)
166
- filepath = File.join(dir, filename)
292
+ require_relative "ai"
167
293
 
168
- File.write(filepath, <<~RUBY)
169
- # Seed: #{name.strip}
170
- #
171
- # This file is executed by `tina4 seed`.
172
- # Use Tina4.seed_orm or Tina4.seed_table to populate data.
173
- #
174
- # Examples:
175
- # Tina4.seed_orm(User, count: 50)
176
- # Tina4.seed_table("audit_log", { action: :string, created_at: :datetime }, count: 100)
177
- RUBY
294
+ root_dir = Dir.pwd
295
+ puts Tina4::AI.status_report(root_dir)
178
296
 
179
- puts "Created seed file: #{filepath}"
297
+ if options[:all]
298
+ created = Tina4::AI.install_all(root_dir, force: options[:force])
299
+ else
300
+ created = Tina4::AI.install_ai_context(root_dir, force: options[:force])
301
+ end
302
+
303
+ if created.any?
304
+ puts "Created/updated context files:"
305
+ created.each { |f| puts " #{f}" }
306
+ else
307
+ puts "No context files were created (files already exist; use --force to overwrite)."
308
+ end
180
309
  end
181
310
 
182
- desc "console", "Start an interactive console"
183
- def console
184
- require_relative "../tina4"
185
- Tina4.initialize!(Dir.pwd)
186
- load_routes(Dir.pwd)
311
+ # ── help ──────────────────────────────────────────────────────────────
312
+
313
+ def cmd_help
314
+ puts <<~HELP
315
+ Tina4 Ruby CLI
316
+
317
+ Usage: tina4ruby COMMAND [options]
318
+
319
+ Commands:
320
+ init [NAME] Initialize a new Tina4 project
321
+ start Start the Tina4 web server
322
+ migrate Run database migrations
323
+ seed Run all seed files in seeds/
324
+ seed:create NAME Create a new seed file
325
+ test Run inline tests
326
+ version Show Tina4 version
327
+ routes List all registered routes
328
+ console Start an interactive console
329
+ ai Detect AI tools and install context files
330
+ help Show this help message
331
+
332
+ Run 'tina4ruby COMMAND --help' for more information on a command.
333
+ HELP
334
+ end
187
335
 
188
- require "irb"
189
- IRB.start
336
+ # ── config resolution ──────────────────────────────────────────────────
337
+
338
+ DEFAULT_PORT = 7147
339
+ DEFAULT_HOST = "0.0.0.0"
340
+
341
+ # Priority: CLI flag > ENV var > default
342
+ def resolve_config(key, cli_value)
343
+ case key
344
+ when :port
345
+ return cli_value if cli_value
346
+ return ENV["PORT"].to_i if ENV["PORT"] && !ENV["PORT"].empty?
347
+ DEFAULT_PORT
348
+ when :host
349
+ return cli_value if cli_value
350
+ return ENV["HOST"] if ENV["HOST"] && !ENV["HOST"].empty?
351
+ DEFAULT_HOST
352
+ end
190
353
  end
191
354
 
192
- private
355
+ # ── shared helpers ────────────────────────────────────────────────────
193
356
 
194
357
  def load_routes(root_dir)
195
358
  route_dirs = %w[routes src/routes src/api api]
@@ -209,8 +372,9 @@ module Tina4
209
372
 
210
373
  def create_project_structure(dir)
211
374
  %w[
212
- routes templates public public/css public/js public/images
213
- migrations src logs
375
+ src/routes src/orm src/templates src/templates/errors
376
+ src/public src/public/css src/public/js src/public/images
377
+ migrations logs
214
378
  ].each do |subdir|
215
379
  FileUtils.mkdir_p(File.join(dir, subdir))
216
380
  end
@@ -221,14 +385,9 @@ module Tina4
221
385
  unless File.exist?(File.join(dir, "app.rb"))
222
386
  File.write(File.join(dir, "app.rb"), <<~RUBY)
223
387
  require "tina4"
224
-
225
- Tina4.get "/" do |request, response|
226
- response.html "<h1>Welcome to #{project_name}!</h1><p>Powered by Tina4 Ruby</p>"
227
- end
228
-
229
- Tina4.get "/api/hello" do |request, response|
230
- response.json({ message: "Hello from Tina4!", timestamp: Time.now.iso8601 })
231
- end
388
+ Tina4.initialize!(__dir__)
389
+ app = Tina4::RackApp.new
390
+ Tina4::WebServer.new(app, port: 7147).start
232
391
  RUBY
233
392
  end
234
393
 
@@ -236,10 +395,18 @@ module Tina4
236
395
  unless File.exist?(File.join(dir, "Gemfile"))
237
396
  File.write(File.join(dir, "Gemfile"), <<~RUBY)
238
397
  source "https://rubygems.org"
239
- gem "tina4ruby"
398
+ gem "tina4-ruby", "~> 3.0"
240
399
  RUBY
241
400
  end
242
401
 
402
+ # .env
403
+ unless File.exist?(File.join(dir, ".env"))
404
+ File.write(File.join(dir, ".env"), <<~TEXT)
405
+ TINA4_DEBUG=true
406
+ TINA4_LOG_LEVEL=ALL
407
+ TEXT
408
+ end
409
+
243
410
  # .gitignore
244
411
  unless File.exist?(File.join(dir, ".gitignore"))
245
412
  File.write(File.join(dir, ".gitignore"), <<~TEXT)
@@ -291,7 +458,7 @@ module Tina4
291
458
  # Copy application code
292
459
  COPY --from=builder /app /app
293
460
 
294
- EXPOSE 7145
461
+ EXPOSE 7147
295
462
 
296
463
  # Swagger defaults (override with env vars in docker-compose/k8s if needed)
297
464
  ENV SWAGGER_TITLE="Tina4 API"
@@ -299,9 +466,8 @@ module Tina4
299
466
  ENV SWAGGER_DESCRIPTION="Auto-generated API documentation"
300
467
 
301
468
  # Start the server on all interfaces
302
- CMD ["bundle", "exec", "tina4", "start", "-p", "7145", "-h", "0.0.0.0"]
469
+ CMD ["bundle", "exec", "tina4ruby", "start", "-p", "7147", "-h", "0.0.0.0"]
303
470
  DOCKERFILE
304
- puts " Created Dockerfile"
305
471
  end
306
472
 
307
473
  # .dockerignore
@@ -319,11 +485,10 @@ module Tina4
319
485
  spec/
320
486
  vendor/bundle
321
487
  TEXT
322
- puts " Created .dockerignore"
323
488
  end
324
489
 
325
490
  # Base template
326
- templates_dir = File.join(dir, "templates")
491
+ templates_dir = File.join(dir, "src", "templates")
327
492
  unless File.exist?(File.join(templates_dir, "base.twig"))
328
493
  File.write(File.join(templates_dir, "base.twig"), <<~HTML)
329
494
  <!DOCTYPE html>
@@ -337,8 +502,8 @@ module Tina4
337
502
  </head>
338
503
  <body>
339
504
  {% block content %}{% endblock %}
340
- <script src="/js/tina4.js"></script>
341
- <script src="/js/tina4helper.js"></script>
505
+ <script src="/js/tina4.min.js"></script>
506
+ <script src="/js/frond.min.js"></script>
342
507
  {% block scripts %}{% endblock %}
343
508
  </body>
344
509
  </html>
@@ -0,0 +1,46 @@
1
+ # Tina4 Constants — HTTP status codes and content types.
2
+ #
3
+ # Standard constants for use in route handlers across all Tina4 frameworks.
4
+ #
5
+ # Tina4.get "/api/users" do |request, response|
6
+ # response.call(users, Tina4::HTTP_OK)
7
+ # end
8
+
9
+ module Tina4
10
+ # ── HTTP Status Codes ──
11
+
12
+ HTTP_OK = 200
13
+ HTTP_CREATED = 201
14
+ HTTP_ACCEPTED = 202
15
+ HTTP_NO_CONTENT = 204
16
+
17
+ HTTP_MOVED = 301
18
+ HTTP_REDIRECT = 302
19
+ HTTP_NOT_MODIFIED = 304
20
+
21
+ HTTP_BAD_REQUEST = 400
22
+ HTTP_UNAUTHORIZED = 401
23
+ HTTP_FORBIDDEN = 403
24
+ HTTP_NOT_FOUND = 404
25
+ HTTP_METHOD_NOT_ALLOWED = 405
26
+ HTTP_CONFLICT = 409
27
+ HTTP_GONE = 410
28
+ HTTP_UNPROCESSABLE = 422
29
+ HTTP_TOO_MANY = 429
30
+
31
+ HTTP_SERVER_ERROR = 500
32
+ HTTP_BAD_GATEWAY = 502
33
+ HTTP_UNAVAILABLE = 503
34
+
35
+ # ── Content Types ──
36
+
37
+ APPLICATION_JSON = "application/json"
38
+ APPLICATION_XML = "application/xml"
39
+ APPLICATION_FORM = "application/x-www-form-urlencoded"
40
+ APPLICATION_OCTET = "application/octet-stream"
41
+
42
+ TEXT_HTML = "text/html; charset=utf-8"
43
+ TEXT_PLAIN = "text/plain; charset=utf-8"
44
+ TEXT_CSV = "text/csv"
45
+ TEXT_XML = "text/xml"
46
+ end
data/lib/tina4/cors.rb ADDED
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ module CorsMiddleware
5
+ class << self
6
+ def config
7
+ @config ||= load_config
8
+ end
9
+
10
+ def reset!
11
+ @config = nil
12
+ end
13
+
14
+ # Handle OPTIONS preflight request, returns a Rack response array
15
+ def preflight_response(env = {})
16
+ origin = resolve_origin(env)
17
+ [
18
+ 204,
19
+ {
20
+ "access-control-allow-origin" => origin,
21
+ "access-control-allow-methods" => config[:methods],
22
+ "access-control-allow-headers" => config[:headers],
23
+ "access-control-max-age" => config[:max_age],
24
+ "access-control-allow-credentials" => config[:credentials]
25
+ },
26
+ [""]
27
+ ]
28
+ end
29
+
30
+ # Apply CORS headers to a response headers hash
31
+ def apply_headers(response_headers, env = {})
32
+ origin = resolve_origin(env)
33
+ response_headers["access-control-allow-origin"] = origin
34
+ response_headers["access-control-allow-methods"] = config[:methods]
35
+ response_headers["access-control-allow-headers"] = config[:headers]
36
+ response_headers["access-control-max-age"] = config[:max_age]
37
+ response_headers["access-control-allow-credentials"] = config[:credentials] if config[:credentials] == "true"
38
+ response_headers
39
+ end
40
+
41
+ # Check if a given origin is allowed
42
+ def origin_allowed?(origin)
43
+ return true if config[:origins] == "*"
44
+
45
+ allowed = config[:origins].split(",").map(&:strip)
46
+ allowed.include?(origin)
47
+ end
48
+
49
+ private
50
+
51
+ def load_config
52
+ {
53
+ origins: ENV["TINA4_CORS_ORIGINS"] || "*",
54
+ methods: ENV["TINA4_CORS_METHODS"] || "GET, POST, PUT, PATCH, DELETE, OPTIONS",
55
+ headers: ENV["TINA4_CORS_HEADERS"] || "Content-Type, Authorization, Accept",
56
+ max_age: ENV["TINA4_CORS_MAX_AGE"] || "86400",
57
+ credentials: ENV["TINA4_CORS_CREDENTIALS"] || "false"
58
+ }.freeze
59
+ end
60
+
61
+ def resolve_origin(env)
62
+ request_origin = env["HTTP_ORIGIN"] || env["HTTP_REFERER"]
63
+
64
+ if config[:origins] == "*"
65
+ "*"
66
+ elsif request_origin && origin_allowed?(request_origin.chomp("/"))
67
+ request_origin.chomp("/")
68
+ else
69
+ config[:origins].split(",").first&.strip || "*"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end