tina4ruby 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +768 -0
  5. data/exe/tina4 +4 -0
  6. data/lib/tina4/api.rb +152 -0
  7. data/lib/tina4/auth.rb +139 -0
  8. data/lib/tina4/cli.rb +349 -0
  9. data/lib/tina4/crud.rb +124 -0
  10. data/lib/tina4/database.rb +135 -0
  11. data/lib/tina4/database_result.rb +89 -0
  12. data/lib/tina4/debug.rb +83 -0
  13. data/lib/tina4/dev.rb +15 -0
  14. data/lib/tina4/dev_reload.rb +68 -0
  15. data/lib/tina4/drivers/firebird_driver.rb +94 -0
  16. data/lib/tina4/drivers/mssql_driver.rb +112 -0
  17. data/lib/tina4/drivers/mysql_driver.rb +90 -0
  18. data/lib/tina4/drivers/postgres_driver.rb +99 -0
  19. data/lib/tina4/drivers/sqlite_driver.rb +85 -0
  20. data/lib/tina4/env.rb +55 -0
  21. data/lib/tina4/field_types.rb +84 -0
  22. data/lib/tina4/graphql.rb +837 -0
  23. data/lib/tina4/localization.rb +100 -0
  24. data/lib/tina4/middleware.rb +59 -0
  25. data/lib/tina4/migration.rb +124 -0
  26. data/lib/tina4/orm.rb +168 -0
  27. data/lib/tina4/public/css/tina4.css +2286 -0
  28. data/lib/tina4/public/css/tina4.min.css +2 -0
  29. data/lib/tina4/public/js/tina4.js +134 -0
  30. data/lib/tina4/public/js/tina4helper.js +387 -0
  31. data/lib/tina4/queue.rb +117 -0
  32. data/lib/tina4/queue_backends/kafka_backend.rb +80 -0
  33. data/lib/tina4/queue_backends/lite_backend.rb +79 -0
  34. data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -0
  35. data/lib/tina4/rack_app.rb +150 -0
  36. data/lib/tina4/request.rb +158 -0
  37. data/lib/tina4/response.rb +172 -0
  38. data/lib/tina4/router.rb +148 -0
  39. data/lib/tina4/scss/tina4css/_alerts.scss +34 -0
  40. data/lib/tina4/scss/tina4css/_badges.scss +22 -0
  41. data/lib/tina4/scss/tina4css/_buttons.scss +69 -0
  42. data/lib/tina4/scss/tina4css/_cards.scss +49 -0
  43. data/lib/tina4/scss/tina4css/_forms.scss +156 -0
  44. data/lib/tina4/scss/tina4css/_grid.scss +81 -0
  45. data/lib/tina4/scss/tina4css/_modals.scss +84 -0
  46. data/lib/tina4/scss/tina4css/_nav.scss +149 -0
  47. data/lib/tina4/scss/tina4css/_reset.scss +94 -0
  48. data/lib/tina4/scss/tina4css/_tables.scss +54 -0
  49. data/lib/tina4/scss/tina4css/_typography.scss +55 -0
  50. data/lib/tina4/scss/tina4css/_utilities.scss +197 -0
  51. data/lib/tina4/scss/tina4css/_variables.scss +117 -0
  52. data/lib/tina4/scss/tina4css/base.scss +1 -0
  53. data/lib/tina4/scss/tina4css/colors.scss +48 -0
  54. data/lib/tina4/scss/tina4css/tina4.scss +17 -0
  55. data/lib/tina4/scss_compiler.rb +131 -0
  56. data/lib/tina4/seeder.rb +529 -0
  57. data/lib/tina4/session.rb +145 -0
  58. data/lib/tina4/session_handlers/file_handler.rb +55 -0
  59. data/lib/tina4/session_handlers/mongo_handler.rb +49 -0
  60. data/lib/tina4/session_handlers/redis_handler.rb +43 -0
  61. data/lib/tina4/swagger.rb +123 -0
  62. data/lib/tina4/template.rb +478 -0
  63. data/lib/tina4/templates/base.twig +26 -0
  64. data/lib/tina4/templates/errors/403.twig +22 -0
  65. data/lib/tina4/templates/errors/404.twig +22 -0
  66. data/lib/tina4/templates/errors/500.twig +22 -0
  67. data/lib/tina4/testing.rb +213 -0
  68. data/lib/tina4/version.rb +5 -0
  69. data/lib/tina4/webserver.rb +101 -0
  70. data/lib/tina4/websocket.rb +167 -0
  71. data/lib/tina4/wsdl.rb +164 -0
  72. data/lib/tina4.rb +259 -0
  73. data/lib/tina4ruby.rb +4 -0
  74. metadata +324 -0
data/README.md ADDED
@@ -0,0 +1,768 @@
1
+ # Tina4 Ruby
2
+
3
+ **Simple. Fast. Human. This is not a framework.**
4
+
5
+ A lightweight, zero-configuration, Windows-friendly Ruby web framework. If you know [tina4_python](https://tina4.com) or tina4_php, you'll feel right at home.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ gem install tina4ruby
11
+ tina4 init myapp
12
+ cd myapp
13
+ bundle install
14
+ tina4 start
15
+ ```
16
+
17
+ Your app is now running at `http://localhost:7145`.
18
+
19
+ ## Routing
20
+
21
+ Register routes using a clean Ruby DSL:
22
+
23
+ ```ruby
24
+ require "tina4"
25
+
26
+ # GET request
27
+ Tina4.get "/hello" do |request, response|
28
+ response.json({ message: "Hello World!" })
29
+ end
30
+
31
+ # POST request
32
+ Tina4.post "/api/users" do |request, response|
33
+ data = request.json_body
34
+ response.json({ created: true, name: data["name"] }, 201)
35
+ end
36
+
37
+ # Path parameters with type constraints
38
+ Tina4.get "/api/users/{id:int}" do |request, response|
39
+ user_id = request.params["id"] # auto-cast to Integer
40
+ response.json({ user_id: user_id })
41
+ end
42
+
43
+ Tina4.get "/files/{path:path}" do |request, response|
44
+ response.json({ path: request.params["path"] })
45
+ end
46
+
47
+ # PUT, PATCH, DELETE
48
+ Tina4.put "/api/users/{id:int}" do |request, response|
49
+ response.json({ updated: true })
50
+ end
51
+
52
+ Tina4.delete "/api/users/{id:int}" do |request, response|
53
+ response.json({ deleted: true })
54
+ end
55
+
56
+ # Match any HTTP method
57
+ Tina4.any "/webhook" do |request, response|
58
+ response.json({ method: request.method })
59
+ end
60
+ ```
61
+
62
+ ### Auth Defaults
63
+
64
+ Tina4 Ruby matches tina4_python's auth behavior:
65
+
66
+ - **GET** routes are **public** by default
67
+ - **POST/PUT/PATCH/DELETE** routes are **secured** by default (require `Authorization: Bearer <token>`)
68
+ - Use `auth: false` to make a write route public (equivalent to tina4_python's `@noauth()`)
69
+ - Set `API_KEY` in `.env` to allow API key bypass (token matches `API_KEY` → access granted)
70
+
71
+ ```ruby
72
+ # POST is secured by default — requires Bearer token
73
+ Tina4.post "/api/users" do |request, response|
74
+ response.json({ created: true })
75
+ end
76
+
77
+ # Make a POST route public (no auth required)
78
+ Tina4.post "/api/webhook", auth: false do |request, response|
79
+ response.json({ received: true })
80
+ end
81
+
82
+ # Custom auth handler
83
+ custom_auth = lambda do |env|
84
+ env["HTTP_X_API_KEY"] == "my-secret"
85
+ end
86
+
87
+ Tina4.post "/api/custom", auth: custom_auth do |request, response|
88
+ response.json({ ok: true })
89
+ end
90
+ ```
91
+
92
+ ### Secured Routes
93
+
94
+ For explicitly securing GET routes (which are public by default):
95
+
96
+ ```ruby
97
+ Tina4.secure_get "/api/profile" do |request, response|
98
+ response.json({ user: "authenticated" })
99
+ end
100
+
101
+ Tina4.secure_post "/api/admin/action" do |request, response|
102
+ response.json({ success: true })
103
+ end
104
+ ```
105
+
106
+ ### Route Groups
107
+
108
+ ```ruby
109
+ Tina4.group "/api/v1" do
110
+ get("/users") { |req, res| res.json(users) }
111
+ post("/users") { |req, res| res.json({ created: true }) }
112
+ end
113
+ ```
114
+
115
+ ## Request Object
116
+
117
+ ```ruby
118
+ Tina4.post "/example" do |request, response|
119
+ request.method # "POST"
120
+ request.path # "/example"
121
+ request.params # merged path + query params
122
+ request.headers # HTTP headers hash
123
+ request.cookies # parsed cookies
124
+ request.body # raw body string
125
+ request.json_body # parsed JSON body (hash)
126
+ request.bearer_token # extracted Bearer token
127
+ request.ip # client IP address
128
+ request.files # uploaded files
129
+ request.session # lazy-loaded session
130
+ end
131
+ ```
132
+
133
+ ## Response Object
134
+
135
+ ```ruby
136
+ # JSON response
137
+ response.json({ key: "value" })
138
+ response.json({ key: "value" }, 201) # custom status
139
+
140
+ # HTML response
141
+ response.html("<h1>Hello</h1>")
142
+
143
+ # Template rendering
144
+ response.render("pages/home.twig", { title: "Welcome" })
145
+
146
+ # Redirect
147
+ response.redirect("/dashboard")
148
+ response.redirect("/login", 301) # permanent redirect
149
+
150
+ # Plain text
151
+ response.text("OK")
152
+
153
+ # File download
154
+ response.file("path/to/document.pdf")
155
+
156
+ # Custom headers
157
+ response.add_header("X-Custom", "value")
158
+
159
+ # Cookies
160
+ response.set_cookie("theme", "dark", max_age: 86400)
161
+ response.delete_cookie("theme")
162
+
163
+ # CORS headers (auto-added by RackApp)
164
+ response.add_cors_headers
165
+ ```
166
+
167
+ ## Templates (Twig)
168
+
169
+ Tina4 uses a Twig-compatible template engine. Templates go in `templates/` or `src/templates/`.
170
+
171
+ ### Base template (`templates/base.twig`)
172
+
173
+ ```twig
174
+ <!DOCTYPE html>
175
+ <html>
176
+ <head>
177
+ <title>{% block title %}My App{% endblock %}</title>
178
+ </head>
179
+ <body>
180
+ {% block content %}{% endblock %}
181
+ </body>
182
+ </html>
183
+ ```
184
+
185
+ ### Child template (`templates/home.twig`)
186
+
187
+ ```twig
188
+ {% extends "base.twig" %}
189
+
190
+ {% block title %}Home{% endblock %}
191
+
192
+ {% block content %}
193
+ <h1>Hello {{ name }}!</h1>
194
+
195
+ {% if items %}
196
+ <ul>
197
+ {% for item in items %}
198
+ <li>{{ loop.index }}. {{ item | capitalize }}</li>
199
+ {% endfor %}
200
+ </ul>
201
+ {% else %}
202
+ <p>No items found.</p>
203
+ {% endif %}
204
+ {% endblock %}
205
+ ```
206
+
207
+ ### Rendering
208
+
209
+ ```ruby
210
+ Tina4.get "/home" do |request, response|
211
+ response.render("home.twig", {
212
+ name: "Alice",
213
+ items: ["apple", "banana", "cherry"]
214
+ })
215
+ end
216
+ ```
217
+
218
+ ### Filters
219
+
220
+ ```twig
221
+ {{ name | upper }} {# ALICE #}
222
+ {{ name | lower }} {# alice #}
223
+ {{ name | capitalize }} {# Alice #}
224
+ {{ "hello world" | title }} {# Hello World #}
225
+ {{ " hi " | trim }} {# hi #}
226
+ {{ items | length }} {# 3 #}
227
+ {{ items | join(", ") }} {# a, b, c #}
228
+ {{ missing | default("N/A") }} {# N/A #}
229
+ {{ html | escape }} {# &lt;b&gt;hi&lt;/b&gt; #}
230
+ {{ text | nl2br }} {# line<br>break #}
231
+ {{ 3.14159 | round(2) }} {# 3.14 #}
232
+ {{ data | json_encode }} {# {"key":"value"} #}
233
+ ```
234
+
235
+ ### Includes
236
+
237
+ ```twig
238
+ {% include "partials/header.twig" %}
239
+ ```
240
+
241
+ ### Variables and Math
242
+
243
+ ```twig
244
+ {% set greeting = "Hello" %}
245
+ {{ greeting ~ " " ~ name }} {# string concatenation #}
246
+ {{ price * quantity }} {# math #}
247
+ ```
248
+
249
+ ### Comments
250
+
251
+ ```twig
252
+ {# This is a comment and won't be rendered #}
253
+ ```
254
+
255
+ ## Database
256
+
257
+ Multi-database support with a unified API:
258
+
259
+ ```ruby
260
+ # SQLite (default, zero-config)
261
+ db = Tina4::Database.new("sqlite://app.db")
262
+
263
+ # PostgreSQL
264
+ db = Tina4::Database.new("postgresql://localhost:5432/mydb")
265
+
266
+ # MySQL
267
+ db = Tina4::Database.new("mysql://localhost:3306/mydb")
268
+
269
+ # MSSQL
270
+ db = Tina4::Database.new("mssql://localhost:1433/mydb")
271
+ ```
272
+
273
+ ### Querying
274
+
275
+ ```ruby
276
+ # Fetch multiple rows
277
+ result = db.fetch("SELECT * FROM users WHERE age > ?", [18])
278
+ result.each { |row| puts row[:name] }
279
+
280
+ # Fetch one row
281
+ user = db.fetch_one("SELECT * FROM users WHERE id = ?", [1])
282
+
283
+ # Pagination
284
+ result = db.fetch("SELECT * FROM users", [], limit: 10, skip: 20)
285
+
286
+ # Insert
287
+ db.insert("users", { name: "Alice", email: "alice@example.com" })
288
+
289
+ # Update
290
+ db.update("users", { name: "Alice Updated" }, { id: 1 })
291
+
292
+ # Delete
293
+ db.delete("users", { id: 1 })
294
+
295
+ # Raw SQL
296
+ db.execute("CREATE INDEX idx_email ON users(email)")
297
+
298
+ # Transactions
299
+ db.transaction do |tx|
300
+ tx.insert("accounts", { name: "Savings", balance: 1000 })
301
+ tx.update("accounts", { balance: 500 }, { id: 1 })
302
+ end
303
+
304
+ # Introspection
305
+ db.tables # ["users", "posts", ...]
306
+ db.table_exists?("users") # true
307
+ db.columns("users") # [{name: "id", type: "INTEGER", ...}, ...]
308
+ ```
309
+
310
+ ### DatabaseResult
311
+
312
+ ```ruby
313
+ result = db.fetch("SELECT * FROM users")
314
+ result.count # number of rows
315
+ result.empty? # true/false
316
+ result.first # first row hash
317
+ result.to_array # array of hashes
318
+ result.to_json # JSON string
319
+ result.to_csv # CSV text
320
+ result.to_paginate # { records_total:, record_count:, data: }
321
+ ```
322
+
323
+ ## ORM
324
+
325
+ Define models with a field DSL:
326
+
327
+ ```ruby
328
+ class User < Tina4::ORM
329
+ integer_field :id, primary_key: true, auto_increment: true
330
+ string_field :name, nullable: false
331
+ string_field :email, length: 255
332
+ integer_field :age, default: 0
333
+ datetime_field :created_at
334
+ end
335
+
336
+ # Set the database connection
337
+ Tina4.database = Tina4::Database.new("sqlite://app.db")
338
+ ```
339
+
340
+ ### CRUD Operations
341
+
342
+ ```ruby
343
+ # Create
344
+ user = User.new(name: "Alice", email: "alice@example.com")
345
+ user.save
346
+
347
+ # Or create in one step
348
+ user = User.create(name: "Bob", email: "bob@example.com")
349
+
350
+ # Read
351
+ user = User.find(1) # by primary key
352
+ users = User.where("age > ?", [18]) # with conditions
353
+ all_users = User.all # all records
354
+ all_users = User.all(limit: 10, order_by: "name")
355
+
356
+ # Update
357
+ user = User.find(1)
358
+ user.name = "Alice Updated"
359
+ user.save
360
+
361
+ # Delete
362
+ user.delete
363
+
364
+ # Load into existing instance
365
+ user = User.new
366
+ user.id = 1
367
+ user.load
368
+
369
+ # Serialization
370
+ user.to_hash # { id: 1, name: "Alice", ... }
371
+ user.to_json # '{"id":1,"name":"Alice",...}'
372
+ ```
373
+
374
+ ### Field Types
375
+
376
+ ```ruby
377
+ integer_field :id
378
+ string_field :name, length: 255
379
+ text_field :bio
380
+ float_field :score
381
+ decimal_field :price, precision: 10, scale: 2
382
+ boolean_field :active
383
+ date_field :birthday
384
+ datetime_field :created_at
385
+ timestamp_field :updated_at
386
+ blob_field :avatar
387
+ json_field :metadata
388
+ ```
389
+
390
+ ## Migrations
391
+
392
+ ```bash
393
+ # Create a migration
394
+ tina4 migrate --create "create users table"
395
+
396
+ # Run pending migrations
397
+ tina4 migrate
398
+
399
+ # Rollback
400
+ tina4 migrate --rollback 1
401
+ ```
402
+
403
+ Migration files are plain SQL in `migrations/`:
404
+
405
+ ```sql
406
+ -- migrations/20260313120000_create_users_table.sql
407
+ CREATE TABLE users (
408
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
409
+ name TEXT NOT NULL,
410
+ email TEXT UNIQUE,
411
+ age INTEGER DEFAULT 0,
412
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
413
+ );
414
+ ```
415
+
416
+ ## Authentication
417
+
418
+ JWT RS256 tokens with auto-generated RSA keys:
419
+
420
+ ```ruby
421
+ # Generate a token
422
+ token = Tina4::Auth.generate_token({ user_id: 42, role: "admin" })
423
+
424
+ # Validate a token
425
+ result = Tina4::Auth.validate_token(token)
426
+ if result[:valid]
427
+ payload = result[:payload]
428
+ puts payload["user_id"] # 42
429
+ end
430
+
431
+ # Password hashing (bcrypt)
432
+ hash = Tina4::Auth.hash_password("secret123")
433
+ Tina4::Auth.verify_password("secret123", hash) # true
434
+ Tina4::Auth.verify_password("wrong", hash) # false
435
+ ```
436
+
437
+ ### Protecting Routes
438
+
439
+ ```ruby
440
+ # Built-in Bearer auth
441
+ Tina4.secure_get "/api/profile" do |request, response|
442
+ # Only runs if valid JWT Bearer token is provided
443
+ response.json({ user: "authenticated" })
444
+ end
445
+
446
+ # Custom auth handler
447
+ custom_auth = lambda do |env|
448
+ api_key = env["HTTP_X_API_KEY"]
449
+ api_key == "my-secret-key"
450
+ end
451
+
452
+ Tina4.secure_get "/api/data", auth: custom_auth do |request, response|
453
+ response.json({ data: "protected" })
454
+ end
455
+ ```
456
+
457
+ ## Sessions
458
+
459
+ ```ruby
460
+ Tina4.post "/login" do |request, response|
461
+ request.session["user_id"] = 42
462
+ request.session["role"] = "admin"
463
+ request.session.save
464
+ response.json({ logged_in: true })
465
+ end
466
+
467
+ Tina4.get "/profile" do |request, response|
468
+ user_id = request.session["user_id"]
469
+ response.json({ user_id: user_id })
470
+ end
471
+
472
+ Tina4.post "/logout" do |request, response|
473
+ request.session.destroy
474
+ response.json({ logged_out: true })
475
+ end
476
+ ```
477
+
478
+ Session backends: `:file` (default), `:redis`, `:mongo`.
479
+
480
+ ## Middleware
481
+
482
+ ```ruby
483
+ # Run before every request
484
+ Tina4.before do |request, response|
485
+ puts "Request: #{request.method} #{request.path}"
486
+ end
487
+
488
+ # Run after every request
489
+ Tina4.after do |request, response|
490
+ puts "Response: #{response.status}"
491
+ end
492
+
493
+ # Pattern matching
494
+ Tina4.before("/api") do |request, response|
495
+ # Only runs for paths starting with /api
496
+ end
497
+
498
+ Tina4.before(/\/admin\/.*/) do |request, response|
499
+ # Regex pattern matching
500
+ return false unless request.session["role"] == "admin" # halts request
501
+ end
502
+ ```
503
+
504
+ ## Swagger / OpenAPI
505
+
506
+ Auto-generated API documentation at `/swagger`:
507
+
508
+ ```ruby
509
+ Tina4.get "/api/users", swagger_meta: {
510
+ summary: "List all users",
511
+ tags: ["Users"],
512
+ description: "Returns a paginated list of users"
513
+ } do |request, response|
514
+ response.json(users)
515
+ end
516
+ ```
517
+
518
+ Visit `http://localhost:7145/swagger` for the interactive Swagger UI.
519
+
520
+ ## GraphQL
521
+
522
+ Zero-dependency GraphQL support with a custom parser, executor, and ORM auto-schema generation.
523
+
524
+ ### Manual Schema
525
+
526
+ ```ruby
527
+ schema = Tina4::GraphQLSchema.new
528
+
529
+ # Add queries
530
+ schema.add_query("hello", type: "String") { |_root, _args, _ctx| "Hello World!" }
531
+
532
+ schema.add_query("user", type: "User", args: { "id" => { type: "ID!" } }) do |_root, args, _ctx|
533
+ User.find(args["id"])&.to_hash
534
+ end
535
+
536
+ # Add mutations
537
+ schema.add_mutation("createUser", type: "User",
538
+ args: { "name" => { type: "String!" }, "email" => { type: "String!" } }
539
+ ) do |_root, args, _ctx|
540
+ User.create(name: args["name"], email: args["email"]).to_hash
541
+ end
542
+
543
+ # Register the /graphql endpoint
544
+ gql = Tina4::GraphQL.new(schema)
545
+ gql.register_route # POST /graphql + GET /graphql (GraphiQL UI)
546
+ ```
547
+
548
+ ### ORM Auto-Schema
549
+
550
+ Generate full CRUD queries and mutations from your ORM models with one line:
551
+
552
+ ```ruby
553
+ schema = Tina4::GraphQLSchema.new
554
+ schema.from_orm(User) # Creates: user, users, createUser, updateUser, deleteUser
555
+ schema.from_orm(Product) # Creates: product, products, createProduct, updateProduct, deleteProduct
556
+
557
+ gql = Tina4::GraphQL.new(schema)
558
+ gql.register_route("/graphql")
559
+ ```
560
+
561
+ This auto-generates:
562
+ - **Queries:** `user(id)` (single), `users(limit, offset)` (list with pagination)
563
+ - **Mutations:** `createUser(input)`, `updateUser(id, input)`, `deleteUser(id)`
564
+
565
+ ### Query Examples
566
+
567
+ ```graphql
568
+ # Simple query
569
+ { hello }
570
+
571
+ # Nested fields with arguments
572
+ { user(id: 42) { id name email } }
573
+
574
+ # List with pagination
575
+ { users(limit: 10, offset: 0) { id name } }
576
+
577
+ # Aliases
578
+ { admin: user(id: 1) { name } guest: user(id: 2) { name } }
579
+
580
+ # Variables
581
+ query GetUser($userId: ID!) {
582
+ user(id: $userId) { id name email }
583
+ }
584
+
585
+ # Fragments
586
+ fragment UserFields on User { id name email }
587
+ { user(id: 1) { ...UserFields } }
588
+
589
+ # Mutations
590
+ mutation {
591
+ createUser(name: "Alice", email: "alice@example.com") { id name }
592
+ }
593
+ ```
594
+
595
+ ### Programmatic Execution
596
+
597
+ ```ruby
598
+ gql = Tina4::GraphQL.new(schema)
599
+
600
+ # Execute a query directly
601
+ result = gql.execute('{ hello }')
602
+ puts result["data"]["hello"] # "Hello World!"
603
+
604
+ # With variables
605
+ result = gql.execute(
606
+ 'query($id: ID!) { user(id: $id) { name } }',
607
+ variables: { "id" => 42 }
608
+ )
609
+
610
+ # Handle an HTTP request body (JSON string)
611
+ result = gql.handle_request('{"query": "{ hello }"}')
612
+ ```
613
+
614
+ Visit `http://localhost:7145/graphql` for the interactive GraphiQL UI.
615
+
616
+ ## REST API Client
617
+
618
+ ```ruby
619
+ api = Tina4::API.new("https://api.example.com", headers: {
620
+ "Authorization" => "Bearer sk-abc123"
621
+ })
622
+
623
+ # GET
624
+ response = api.get("/users", params: { page: 1 })
625
+ puts response.json # parsed response body
626
+
627
+ # POST
628
+ response = api.post("/users", body: { name: "Alice" })
629
+ puts response.success? # true for 2xx status
630
+ puts response.status # 201
631
+
632
+ # PUT, PATCH, DELETE
633
+ api.put("/users/1", body: { name: "Updated" })
634
+ api.patch("/users/1", body: { name: "Patched" })
635
+ api.delete("/users/1")
636
+
637
+ # File upload
638
+ api.upload("/files", "path/to/file.pdf")
639
+ ```
640
+
641
+ ## Environment Variables
642
+
643
+ Tina4 auto-creates and loads `.env` files:
644
+
645
+ ```env
646
+ PROJECT_NAME=My App
647
+ VERSION=1.0.0
648
+ SECRET=my-jwt-secret
649
+ API_KEY=your-api-key-here
650
+ DATABASE_URL=sqlite://app.db
651
+ TINA4_DEBUG_LEVEL=[TINA4_LOG_DEBUG]
652
+ ENVIRONMENT=development
653
+ ```
654
+
655
+ `API_KEY` enables a static bearer token bypass — any request with `Authorization: Bearer <API_KEY>` is granted access without JWT validation.
656
+
657
+ Supports environment-specific files: `.env.development`, `.env.production`, `.env.test`.
658
+
659
+ ## CLI Commands
660
+
661
+ ```bash
662
+ tina4 init [NAME] # Scaffold a new project
663
+ tina4 start # Start the web server (default port 7145)
664
+ tina4 start -p 3000 # Custom port
665
+ tina4 start -d # Dev mode with auto-reload
666
+ tina4 migrate # Run pending migrations
667
+ tina4 migrate --create "desc" # Create a migration
668
+ tina4 migrate --rollback 1 # Rollback migrations
669
+ tina4 test # Run inline tests
670
+ tina4 routes # List all registered routes
671
+ tina4 console # Interactive Ruby console
672
+ tina4 version # Show version
673
+ ```
674
+
675
+ ## Project Structure
676
+
677
+ ```
678
+ myapp/
679
+ ├── app.rb # Entry point
680
+ ├── .env # Environment config
681
+ ├── Gemfile
682
+ ├── migrations/ # SQL migrations
683
+ ├── routes/ # Auto-discovered route files
684
+ ├── templates/ # Twig/ERB templates
685
+ ├── public/ # Static files (CSS, JS, images)
686
+ │ ├── css/
687
+ │ ├── js/
688
+ │ └── images/
689
+ ├── src/ # Application code
690
+ └── logs/ # Log files
691
+ ```
692
+
693
+ Routes in `routes/` are auto-discovered at startup:
694
+
695
+ ```ruby
696
+ # routes/users.rb
697
+ Tina4.get "/api/users" do |request, response|
698
+ response.json(User.all.map(&:to_hash))
699
+ end
700
+ ```
701
+
702
+ ## Auto-Discovery
703
+
704
+ Tina4 automatically loads:
705
+ - Route files from `routes/`, `src/routes/`, `src/api/`, `api/`
706
+ - `app.rb` and `index.rb` from the project root
707
+
708
+ ## Full Example App
709
+
710
+ ```ruby
711
+ # app.rb
712
+ require "tina4"
713
+
714
+ # Database
715
+ Tina4.database = Tina4::Database.new("sqlite://app.db")
716
+
717
+ # Model
718
+ class Todo < Tina4::ORM
719
+ integer_field :id, primary_key: true, auto_increment: true
720
+ string_field :title, nullable: false
721
+ boolean_field :done, default: false
722
+ end
723
+
724
+ # Routes
725
+ Tina4.get "/" do |request, response|
726
+ response.render("home.twig", { todos: Todo.all.map(&:to_hash) })
727
+ end
728
+
729
+ Tina4.get "/api/todos" do |request, response|
730
+ response.json(Todo.all.map(&:to_hash))
731
+ end
732
+
733
+ Tina4.post "/api/todos" do |request, response|
734
+ todo = Todo.create(title: request.json_body["title"])
735
+ response.json(todo.to_hash, 201)
736
+ end
737
+
738
+ Tina4.put "/api/todos/{id:int}" do |request, response|
739
+ todo = Todo.find(request.params["id"])
740
+ todo.done = request.json_body["done"]
741
+ todo.save
742
+ response.json(todo.to_hash)
743
+ end
744
+
745
+ Tina4.delete "/api/todos/{id:int}" do |request, response|
746
+ Todo.find(request.params["id"]).delete
747
+ response.json({ deleted: true })
748
+ end
749
+ ```
750
+
751
+ ## Requirements
752
+
753
+ - Ruby >= 3.1.0
754
+ - Works on Windows, macOS, and Linux
755
+
756
+ ## License
757
+
758
+ MIT
759
+
760
+ ---
761
+
762
+ ## Our Sponsors
763
+
764
+ **Sponsored with 🩵 by Code Infinity**
765
+
766
+ [<img src="https://codeinfinity.co.za/wp-content/uploads/2025/09/c8e-logo-github.png" alt="Code Infinity" width="100">](https://codeinfinity.co.za/about-open-source-policy?utm_source=github&utm_medium=website&utm_campaign=opensource_campaign&utm_id=opensource)
767
+
768
+ *Supporting open source communities <span style="color: #1DC7DE;">•</span> Innovate <span style="color: #1DC7DE;">•</span> Code <span style="color: #1DC7DE;">•</span> Empower*