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/router.rb CHANGED
@@ -2,14 +2,17 @@
2
2
 
3
3
  module Tina4
4
4
  class Route
5
- attr_reader :method, :path, :handler, :auth_handler, :swagger_meta, :path_regex, :param_names
5
+ attr_reader :method, :path, :handler, :auth_handler, :swagger_meta,
6
+ :path_regex, :param_names, :middleware, :template
6
7
 
7
- def initialize(method, path, handler, auth_handler: nil, swagger_meta: {})
8
+ def initialize(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil)
8
9
  @method = method.to_s.upcase.freeze
9
10
  @path = normalize_path(path).freeze
10
11
  @handler = handler
11
12
  @auth_handler = auth_handler
12
13
  @swagger_meta = swagger_meta
14
+ @middleware = middleware.freeze
15
+ @template = template&.freeze
13
16
  @param_names = []
14
17
  @path_regex = compile_pattern(@path)
15
18
  @param_names.freeze
@@ -17,7 +20,7 @@ module Tina4
17
20
 
18
21
  # Returns params hash if matched, false otherwise
19
22
  def match?(request_path, request_method = nil)
20
- return false if request_method && @method != request_method.to_s.upcase
23
+ return false if request_method && @method != "ANY" && @method != request_method.to_s.upcase
21
24
  match_path(request_path)
22
25
  end
23
26
 
@@ -27,7 +30,6 @@ module Tina4
27
30
  return false unless match
28
31
 
29
32
  if @param_names.empty?
30
- # Static route — no params to extract
31
33
  {}
32
34
  else
33
35
  params = {}
@@ -39,6 +41,15 @@ module Tina4
39
41
  end
40
42
  end
41
43
 
44
+ # Run per-route middleware chain; returns true if all pass
45
+ def run_middleware(request, response)
46
+ @middleware.each do |mw|
47
+ result = mw.call(request, response)
48
+ return false if result == false
49
+ end
50
+ true
51
+ end
52
+
42
53
  private
43
54
 
44
55
  def normalize_path(path)
@@ -53,7 +64,15 @@ module Tina4
53
64
 
54
65
  parts = path.split("/").reject(&:empty?)
55
66
  regex_parts = parts.map do |part|
56
- if part =~ /\A\{(\w+)(?::(\w+))?\}\z/
67
+ if part =~ /\A\*(\w+)\z/
68
+ # Catch-all splat parameter: *path captures everything after
69
+ name = Regexp.last_match(1)
70
+ @param_names << { name: name.to_sym, type: "path" }
71
+ '(.+)'
72
+ elsif part =~ /\A\{(\w+)(?::(\w+))?\}\z/
73
+ # Tina4/Python-style brace params: {id} or {id:int}
74
+ # This is the ONLY supported param syntax, matching Python exactly.
75
+ # Do NOT add :id (colon) style params.
57
76
  name = Regexp.last_match(1)
58
77
  type = Regexp.last_match(2) || "string"
59
78
  @param_names << { name: name.to_sym, type: type }
@@ -97,14 +116,43 @@ module Tina4
97
116
  @method_index ||= Hash.new { |h, k| h[k] = [] }
98
117
  end
99
118
 
100
- def add_route(method, path, handler, auth_handler: nil, swagger_meta: {})
101
- route = Route.new(method, path, handler, auth_handler: auth_handler, swagger_meta: swagger_meta)
119
+ def add_route(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil)
120
+ route = Route.new(method, path, handler,
121
+ auth_handler: auth_handler,
122
+ swagger_meta: swagger_meta,
123
+ middleware: middleware,
124
+ template: template)
102
125
  routes << route
103
126
  method_index[route.method] << route
104
- Tina4::Debug.debug("Route registered: #{method.upcase} #{path}")
127
+ Tina4::Log.debug("Route registered: #{method.upcase} #{path}")
105
128
  route
106
129
  end
107
130
 
131
+ # Convenience registration methods matching tina4-python pattern
132
+ def get(path, middleware: [], swagger_meta: {}, template: nil, &block)
133
+ add_route("GET", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
134
+ end
135
+
136
+ def post(path, middleware: [], swagger_meta: {}, template: nil, &block)
137
+ add_route("POST", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
138
+ end
139
+
140
+ def put(path, middleware: [], swagger_meta: {}, template: nil, &block)
141
+ add_route("PUT", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
142
+ end
143
+
144
+ def patch(path, middleware: [], swagger_meta: {}, template: nil, &block)
145
+ add_route("PATCH", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
146
+ end
147
+
148
+ def delete(path, middleware: [], swagger_meta: {}, template: nil, &block)
149
+ add_route("DELETE", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
150
+ end
151
+
152
+ def any(path, middleware: [], swagger_meta: {}, template: nil, &block)
153
+ add_route("ANY", path, block, middleware: middleware, swagger_meta: swagger_meta, template: template)
154
+ end
155
+
108
156
  def find_route(path, method)
109
157
  normalized_method = method.upcase
110
158
  # Normalize path once (not per-route)
@@ -112,8 +160,8 @@ module Tina4
112
160
  normalized_path = "/#{normalized_path}" unless normalized_path.start_with?("/")
113
161
  normalized_path = normalized_path.chomp("/") unless normalized_path == "/"
114
162
 
115
- # Only scan routes matching this HTTP method
116
- candidates = method_index[normalized_method]
163
+ # Check ANY routes first, then method-specific routes
164
+ candidates = (method_index["ANY"] || []) + (method_index[normalized_method] || [])
117
165
  candidates.each do |route|
118
166
  params = route.match_path(normalized_path)
119
167
  return [route, params] if params
@@ -126,23 +174,50 @@ module Tina4
126
174
  @method_index = Hash.new { |h, k| h[k] = [] }
127
175
  end
128
176
 
129
- def group(prefix, auth_handler: nil, &block)
130
- GroupContext.new(prefix, auth_handler).instance_eval(&block)
177
+ def group(prefix, auth_handler: nil, middleware: [], &block)
178
+ GroupContext.new(prefix, auth_handler, middleware).instance_eval(&block)
179
+ end
180
+
181
+ # Load route files from a directory (file-based route discovery)
182
+ def load_routes(directory)
183
+ return unless Dir.exist?(directory)
184
+ Dir.glob(File.join(directory, "**/*.rb")).sort.each do |file|
185
+ begin
186
+ load file
187
+ Tina4::Log.debug("Route loaded: #{file}")
188
+ rescue => e
189
+ Tina4::Log.error("Failed to load route #{file}: #{e.message}")
190
+ end
191
+ end
131
192
  end
132
193
  end
133
194
 
134
195
  class GroupContext
135
- def initialize(prefix, auth_handler = nil)
196
+ def initialize(prefix, auth_handler = nil, middleware = [])
136
197
  @prefix = prefix.chomp("/")
137
198
  @auth_handler = auth_handler
199
+ @middleware = middleware
138
200
  end
139
201
 
140
202
  %w[get post put patch delete any].each do |m|
141
- define_method(m) do |path, swagger_meta: {}, &handler|
203
+ define_method(m) do |path, middleware: [], swagger_meta: {}, template: nil, &handler|
142
204
  full_path = "#{@prefix}#{path}"
143
- Tina4::Router.add_route(m, full_path, handler, auth_handler: @auth_handler, swagger_meta: swagger_meta)
205
+ combined_middleware = @middleware + middleware
206
+ Tina4::Router.add_route(m, full_path, handler,
207
+ auth_handler: @auth_handler,
208
+ swagger_meta: swagger_meta,
209
+ middleware: combined_middleware,
210
+ template: template)
144
211
  end
145
212
  end
213
+
214
+ # Nested groups
215
+ def group(prefix, auth_handler: nil, middleware: [], &block)
216
+ full_prefix = "#{@prefix}#{prefix}"
217
+ combined_middleware = @middleware + middleware
218
+ nested_auth = auth_handler || @auth_handler
219
+ GroupContext.new(full_prefix, nested_auth, combined_middleware).instance_eval(&block)
220
+ end
146
221
  end
147
222
  end
148
223
  end
@@ -31,9 +31,9 @@ module Tina4
31
31
  css_content = compile_scss(scss_content, File.dirname(scss_file))
32
32
  File.write(css_file, css_content)
33
33
 
34
- Tina4::Debug.debug("Compiled SCSS: #{scss_file} -> #{css_file}")
34
+ Tina4::Log.debug("Compiled SCSS: #{scss_file} -> #{css_file}")
35
35
  rescue => e
36
- Tina4::Debug.error("SCSS compilation failed: #{scss_file} - #{e.message}")
36
+ Tina4::Log.error("SCSS compilation failed: #{scss_file} - #{e.message}")
37
37
  end
38
38
 
39
39
  def compile_scss(content, base_dir)
data/lib/tina4/seeder.rb CHANGED
@@ -90,6 +90,13 @@ module Tina4
90
90
  @rng = seed ? Random.new(seed) : Random.new
91
91
  end
92
92
 
93
+ # Static factory — create a seeded FakeData instance.
94
+ # fake = FakeData.seed(42)
95
+ # fake.name # deterministic
96
+ def self.seed(seed)
97
+ new(seed: seed)
98
+ end
99
+
93
100
  def first_name
94
101
  FIRST_NAMES[@rng.rand(FIRST_NAMES.length)]
95
102
  end
@@ -328,13 +335,13 @@ module Tina4
328
335
  table = orm_class.table_name
329
336
 
330
337
  if fields.empty?
331
- Tina4::Debug.error("Seeder: No fields found on #{orm_class.name}")
338
+ Tina4::Log.error("Seeder: No fields found on #{orm_class.name}")
332
339
  return 0
333
340
  end
334
341
 
335
342
  db = Tina4.database
336
343
  unless db
337
- Tina4::Debug.error("Seeder: No database connection. Set Tina4.database first.")
344
+ Tina4::Log.error("Seeder: No database connection. Set Tina4.database first.")
338
345
  return 0
339
346
  end
340
347
 
@@ -343,7 +350,7 @@ module Tina4
343
350
  begin
344
351
  result = db.fetch_one("SELECT count(*) as cnt FROM #{table}")
345
352
  if result && result[:cnt].to_i >= count
346
- Tina4::Debug.info("Seeder: #{table} already has #{result[:cnt]} records, skipping")
353
+ Tina4::Log.info("Seeder: #{table} already has #{result[:cnt]} records, skipping")
347
354
  return 0
348
355
  end
349
356
  rescue => e
@@ -355,9 +362,9 @@ module Tina4
355
362
  if clear
356
363
  begin
357
364
  db.execute("DELETE FROM #{table}")
358
- Tina4::Debug.info("Seeder: Cleared #{table}")
365
+ Tina4::Log.info("Seeder: Cleared #{table}")
359
366
  rescue => e
360
- Tina4::Debug.warn("Seeder: Could not clear #{table}: #{e.message}")
367
+ Tina4::Log.warn("Seeder: Could not clear #{table}: #{e.message}")
361
368
  end
362
369
  end
363
370
 
@@ -384,14 +391,14 @@ module Tina4
384
391
  if obj.save
385
392
  inserted += 1
386
393
  else
387
- Tina4::Debug.warn("Seeder: Insert failed for #{table} row #{i + 1}: #{obj.errors.join(', ')}")
394
+ Tina4::Log.warn("Seeder: Insert failed for #{table} row #{i + 1}: #{obj.errors.join(', ')}")
388
395
  end
389
396
  rescue => e
390
- Tina4::Debug.warn("Seeder: Insert failed for #{table} row #{i + 1}: #{e.message}")
397
+ Tina4::Log.warn("Seeder: Insert failed for #{table} row #{i + 1}: #{e.message}")
391
398
  end
392
399
  end
393
400
 
394
- Tina4::Debug.info("Seeder: Inserted #{inserted}/#{count} records into #{table}")
401
+ Tina4::Log.info("Seeder: Inserted #{inserted}/#{count} records into #{table}")
395
402
  inserted
396
403
  end
397
404
 
@@ -409,7 +416,7 @@ module Tina4
409
416
  db = Tina4.database
410
417
 
411
418
  unless db
412
- Tina4::Debug.error("Seeder: No database connection.")
419
+ Tina4::Log.error("Seeder: No database connection.")
413
420
  return 0
414
421
  end
415
422
 
@@ -417,7 +424,7 @@ module Tina4
417
424
  begin
418
425
  db.execute("DELETE FROM #{table_name}")
419
426
  rescue => e
420
- Tina4::Debug.warn("Seeder: Could not clear #{table_name}: #{e.message}")
427
+ Tina4::Log.warn("Seeder: Could not clear #{table_name}: #{e.message}")
421
428
  end
422
429
  end
423
430
 
@@ -438,63 +445,51 @@ module Tina4
438
445
  db.insert(table_name, row)
439
446
  inserted += 1
440
447
  rescue => e
441
- Tina4::Debug.warn("Seeder: Insert failed for #{table_name} row #{i + 1}: #{e.message}")
448
+ Tina4::Log.warn("Seeder: Insert failed for #{table_name} row #{i + 1}: #{e.message}")
442
449
  end
443
450
  end
444
451
 
445
- Tina4::Debug.info("Seeder: Inserted #{inserted}/#{count} records into #{table_name}")
452
+ Tina4::Log.info("Seeder: Inserted #{inserted}/#{count} records into #{table_name}")
446
453
  inserted
447
454
  end
448
455
 
449
- # Builder class for seeding multiple ORM classes with dependency resolution.
456
+ # Seed multiple ORM classes in batch with optional dependency-aware clearing.
457
+ #
458
+ # @param tasks [Array<Hash>] each hash has :orm_class, :count, :overrides, :seed
459
+ # @param clear [Boolean] delete existing records (in reverse order) before seeding
460
+ # @return [Hash] { "ClassName" => inserted_count, ... }
450
461
  #
451
462
  # @example
452
- # seeder = Tina4::Seeder.new
453
- # seeder.add(User, count: 20)
454
- # seeder.add(Order, count: 100, overrides: { status: "pending" })
455
- # seeder.run(clear: true)
456
- class Seeder
457
- def initialize
458
- @tasks = []
459
- end
460
-
461
- def add(orm_class, count: 10, overrides: {}, seed: nil)
462
- @tasks << {
463
- orm_class: orm_class,
464
- count: count,
465
- overrides: overrides,
466
- seed: seed
467
- }
468
- self
469
- end
470
-
471
- def run(clear: false)
472
- results = {}
473
-
474
- if clear
475
- @tasks.reverse_each do |task|
476
- begin
477
- Tina4.database&.execute("DELETE FROM #{task[:orm_class].table_name}")
478
- Tina4::Debug.info("Seeder: Cleared #{task[:orm_class].table_name}")
479
- rescue => e
480
- Tina4::Debug.warn("Seeder: Could not clear #{task[:orm_class].table_name}: #{e.message}")
481
- end
482
- end
483
- end
463
+ # Tina4.seed_batch([
464
+ # { orm_class: User, count: 20 },
465
+ # { orm_class: Order, count: 100, overrides: { status: "pending" } }
466
+ # ], clear: true)
467
+ def self.seed_batch(tasks, clear: false)
468
+ results = {}
484
469
 
485
- @tasks.each do |task|
486
- n = Tina4.seed_orm(
487
- task[:orm_class],
488
- count: task[:count],
489
- overrides: task[:overrides],
490
- clear: false,
491
- seed: task[:seed]
492
- )
493
- results[task[:orm_class].name] = n
470
+ if clear
471
+ tasks.reverse_each do |task|
472
+ begin
473
+ Tina4.database&.execute("DELETE FROM #{task[:orm_class].table_name}")
474
+ Tina4::Log.info("Seeder: Cleared #{task[:orm_class].table_name}")
475
+ rescue => e
476
+ Tina4::Log.warn("Seeder: Could not clear #{task[:orm_class].table_name}: #{e.message}")
477
+ end
494
478
  end
479
+ end
495
480
 
496
- results
481
+ tasks.each do |task|
482
+ n = Tina4.seed_orm(
483
+ task[:orm_class],
484
+ count: task[:count] || 10,
485
+ overrides: task[:overrides] || {},
486
+ clear: false,
487
+ seed: task[:seed]
488
+ )
489
+ results[task[:orm_class].name] = n
497
490
  end
491
+
492
+ results
498
493
  end
499
494
 
500
495
  # Run all seed files in the given folder.
@@ -502,7 +497,7 @@ module Tina4
502
497
  # @param seed_folder [String] path to seed files (default: "seeds")
503
498
  def self.seed(seed_folder: "seeds", clear: false)
504
499
  unless Dir.exist?(seed_folder)
505
- Tina4::Debug.info("Seeder: No seeds folder found at #{seed_folder}")
500
+ Tina4::Log.info("Seeder: No seeds folder found at #{seed_folder}")
506
501
  return
507
502
  end
508
503
 
@@ -510,19 +505,19 @@ module Tina4
510
505
  files.reject! { |f| File.basename(f).start_with?("_") }
511
506
 
512
507
  if files.empty?
513
- Tina4::Debug.info("Seeder: No seed files found in #{seed_folder}")
508
+ Tina4::Log.info("Seeder: No seed files found in #{seed_folder}")
514
509
  return
515
510
  end
516
511
 
517
- Tina4::Debug.info("Seeder: Found #{files.length} seed file(s) in #{seed_folder}")
512
+ Tina4::Log.info("Seeder: Found #{files.length} seed file(s) in #{seed_folder}")
518
513
 
519
514
  files.each do |filepath|
520
515
  begin
521
- Tina4::Debug.info("Seeder: Running #{File.basename(filepath)}...")
516
+ Tina4::Log.info("Seeder: Running #{File.basename(filepath)}...")
522
517
  load filepath
523
- Tina4::Debug.info("Seeder: Completed #{File.basename(filepath)}")
518
+ Tina4::Log.info("Seeder: Completed #{File.basename(filepath)}")
524
519
  rescue => e
525
- Tina4::Debug.error("Seeder: Failed to run #{File.basename(filepath)}: #{e.message}")
520
+ Tina4::Log.error("Seeder: Failed to run #{File.basename(filepath)}: #{e.message}")
526
521
  end
527
522
  end
528
523
  end