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/README.md CHANGED
@@ -1,761 +1,562 @@
1
- # Tina4 Ruby
1
+ <p align="center">
2
+ <img src="https://tina4.com/logo.svg" alt="Tina4" width="200">
3
+ </p>
4
+
5
+ <h1 align="center">Tina4 Ruby</h1>
6
+ <h3 align="center">This is not a framework</h3>
7
+
8
+ <p align="center">
9
+ Laravel joy. Ruby speed. 10x less code. Zero third-party dependencies.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://tina4.com">Documentation</a> &bull;
14
+ <a href="#getting-started">Getting Started</a> &bull;
15
+ <a href="#features">Features</a> &bull;
16
+ <a href="#cli-reference">CLI Reference</a> &bull;
17
+ <a href="https://tina4.com">tina4.com</a>
18
+ </p>
19
+
20
+ <p align="center">
21
+ <img src="https://img.shields.io/badge/tests-1334%20passing-brightgreen" alt="Tests">
22
+ <img src="https://img.shields.io/badge/carbonah-A%2B%20rated-00cc44" alt="Carbonah A+">
23
+ <img src="https://img.shields.io/badge/zero--dep-core-blue" alt="Zero Dependencies">
24
+ <img src="https://img.shields.io/badge/ruby-3.1%2B-blue" alt="Ruby 3.1+">
25
+ <img src="https://img.shields.io/badge/license-MIT-lightgrey" alt="MIT License">
26
+ </p>
2
27
 
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.
28
+ ---
6
29
 
7
- ## Quick Start
30
+ ## Quickstart
8
31
 
9
32
  ```bash
10
33
  gem install tina4ruby
11
- tina4 init myapp
12
- cd myapp
13
- bundle install
14
- tina4 start
34
+ tina4ruby init my-app
35
+ cd my-app
36
+ tina4ruby serve
37
+ # -> http://localhost:7147
15
38
  ```
16
39
 
17
- Your app is now running at `http://localhost:7145`.
40
+ That's it. Zero configuration, zero classes, zero boilerplate.
18
41
 
19
- ## Routing
42
+ ---
20
43
 
21
- Register routes using a clean Ruby DSL:
44
+ ## What's Included
22
45
 
23
- ```ruby
24
- require "tina4"
46
+ Every feature is built from scratch -- no gem install, no node_modules, no third-party runtime dependencies in core.
25
47
 
26
- # GET request
27
- Tina4.get "/hello" do |request, response|
28
- response.json({ message: "Hello World!" })
29
- end
48
+ | Category | Features |
49
+ |----------|----------|
50
+ | **HTTP** | Rack 3 server, block routing, path params (`{id:int}`, `{p:path}`), middleware pipeline, CORS, rate limiting, graceful shutdown |
51
+ | **Templates** | Frond engine (Twig-compatible), inheritance, partials, 35+ filters, macros, fragment caching, sandboxing |
52
+ | **ORM** | Active Record, typed fields with validation, soft delete, relationships (`has_one`/`has_many`/`belongs_to`), scopes, result caching, multi-database |
53
+ | **Database** | SQLite, PostgreSQL, MySQL, MSSQL, Firebird -- unified adapter interface |
54
+ | **Auth** | Zero-dep JWT (RS256), sessions (file, Redis, MongoDB), password hashing, form tokens |
55
+ | **API** | Swagger/OpenAPI auto-generation, GraphQL with ORM auto-schema and GraphiQL IDE |
56
+ | **Background** | DB-backed queue with priority, delayed jobs, retry, batch processing, multi-queue |
57
+ | **Real-time** | Native WebSocket (RFC 6455), per-path routing, connection manager |
58
+ | **Frontend** | tina4-css (~24 KB), frond.js helper, SCSS compiler, live reload, CSS hot-reload |
59
+ | **DX** | Dev admin dashboard, error overlay, request inspector, AI tool integration, Carbonah green benchmarks |
60
+ | **Data** | Migrations with rollback, 50+ fake data generators, ORM and table seeders |
61
+ | **Other** | REST client, localization (6 languages), in-memory cache (TTL/tags/LRU), event system, inline testing, configurable error pages |
30
62
 
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
63
+ **676 tests across 28 modules. All Carbonah benchmarks rated A+.**
36
64
 
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
65
+ For full documentation visit **[tina4.com](https://tina4.com)**.
42
66
 
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
67
+ ---
51
68
 
52
- Tina4.delete "/api/users/{id:int}" do |request, response|
53
- response.json({ deleted: true })
54
- end
69
+ ## Install
55
70
 
56
- # Match any HTTP method
57
- Tina4.any "/webhook" do |request, response|
58
- response.json({ method: request.method })
59
- end
71
+ ```bash
72
+ gem install tina4ruby
60
73
  ```
61
74
 
62
- ### Auth Defaults
75
+ ### Optional database drivers
63
76
 
64
- Tina4 Ruby matches tina4_python's auth behavior:
77
+ Install only what you need:
65
78
 
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)
79
+ ```bash
80
+ gem install pg # PostgreSQL
81
+ gem install mysql2 # MySQL / MariaDB (driver name: mysql)
82
+ gem install tiny_tds # Microsoft SQL Server
83
+ gem install fb # Firebird
84
+ ```
70
85
 
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
86
+ ---
76
87
 
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
88
+ ## Getting Started
81
89
 
82
- # Custom auth handler
83
- custom_auth = lambda do |env|
84
- env["HTTP_X_API_KEY"] == "my-secret"
85
- end
90
+ ### 1. Create a project
86
91
 
87
- Tina4.post "/api/custom", auth: custom_auth do |request, response|
88
- response.json({ ok: true })
89
- end
92
+ ```bash
93
+ tina4ruby init my-app
94
+ cd my-app
90
95
  ```
91
96
 
92
- ### Secured Routes
97
+ This creates:
93
98
 
94
- For explicitly securing GET routes (which are public by default):
99
+ ```
100
+ my-app/
101
+ ├── app.rb # Entry point
102
+ ├── .env # Configuration
103
+ ├── Gemfile
104
+ ├── src/
105
+ │ ├── routes/ # API + page routes (auto-discovered)
106
+ │ ├── orm/ # Database models
107
+ │ ├── app/ # Service classes and shared helpers
108
+ │ ├── templates/ # Frond/Twig templates
109
+ │ ├── seeds/ # Database seeders
110
+ │ ├── scss/ # SCSS (auto-compiled to public/css/)
111
+ │ └── public/ # Static assets served at /
112
+ ├── migrations/ # SQL migration files
113
+ └── tests/ # RSpec tests
114
+ ```
115
+
116
+ ### 2. Create a route
117
+
118
+ Create `src/routes/hello.rb`:
95
119
 
96
120
  ```ruby
97
- Tina4.secure_get "/api/profile" do |request, response|
98
- response.json({ user: "authenticated" })
121
+ Tina4.get "/api/hello" do |request, response|
122
+ response.json({ message: "Hello from Tina4!" })
99
123
  end
100
124
 
101
- Tina4.secure_post "/api/admin/action" do |request, response|
102
- response.json({ success: true })
125
+ Tina4.get "/api/hello/{name}" do |request, response|
126
+ response.json({ message: "Hello, #{request.params["name"]}!" })
103
127
  end
104
128
  ```
105
129
 
106
- ### Route Groups
130
+ Visit `http://localhost:7147/api/hello` -- routes are auto-discovered, no requires needed.
107
131
 
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
132
+ ### 3. Add a database
133
+
134
+ Edit `.env`:
135
+
136
+ ```bash
137
+ DATABASE_URL=sqlite://data/app.db
138
+ DATABASE_USERNAME=
139
+ DATABASE_PASSWORD=
113
140
  ```
114
141
 
115
- ## Request Object
142
+ Create and run a migration:
116
143
 
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
144
+ ```bash
145
+ tina4ruby migrate --create "create users table"
131
146
  ```
132
147
 
133
- ## Response Object
148
+ Edit the generated SQL:
134
149
 
135
- ```ruby
136
- # JSON response
137
- response.json({ key: "value" })
138
- response.json({ key: "value" }, 201) # custom status
150
+ ```sql
151
+ CREATE TABLE IF NOT EXISTS users (
152
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
153
+ name TEXT NOT NULL,
154
+ email TEXT NOT NULL,
155
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
156
+ );
157
+ ```
158
+
159
+ ```bash
160
+ tina4ruby migrate
161
+ ```
139
162
 
140
- # HTML response
141
- response.html("<h1>Hello</h1>")
163
+ ### 4. Create an ORM model
142
164
 
143
- # Template rendering
144
- response.render("pages/home.twig", { title: "Welcome" })
165
+ Create `src/orm/user.rb`:
145
166
 
146
- # Redirect
147
- response.redirect("/dashboard")
148
- response.redirect("/login", 301) # permanent redirect
167
+ ```ruby
168
+ class User < Tina4::ORM
169
+ integer_field :id, primary_key: true, auto_increment: true
170
+ string_field :name, nullable: false, length: 100
171
+ string_field :email, length: 255
172
+ datetime_field :created_at
173
+ end
174
+ ```
149
175
 
150
- # Plain text
151
- response.text("OK")
176
+ ### 5. Build a REST API
152
177
 
153
- # File download
154
- response.file("path/to/document.pdf")
178
+ Create `src/routes/users.rb`:
155
179
 
156
- # Custom headers
157
- response.add_header("X-Custom", "value")
180
+ ```ruby
181
+ Tina4.get "/api/users" do |request, response|
182
+ response.json(User.all(limit: 100).map(&:to_hash))
183
+ end
158
184
 
159
- # Cookies
160
- response.set_cookie("theme", "dark", max_age: 86400)
161
- response.delete_cookie("theme")
185
+ Tina4.get "/api/users/{id}" do |request, response|
186
+ user = User.find(request.params["id"])
187
+ if user
188
+ response.json(user.to_hash)
189
+ else
190
+ response.json({ error: "Not found" }, 404)
191
+ end
192
+ end
162
193
 
163
- # CORS headers (auto-added by RackApp)
164
- response.add_cors_headers
194
+ Tina4.post "/api/users", auth: false do |request, response|
195
+ user = User.create(request.json_body)
196
+ response.json(user.to_hash, 201)
197
+ end
165
198
  ```
166
199
 
167
- ## Templates (Twig)
168
-
169
- Tina4 uses a Twig-compatible template engine. Templates go in `templates/` or `src/templates/`.
200
+ ### 6. Add a template
170
201
 
171
- ### Base template (`templates/base.twig`)
202
+ Create `src/templates/base.twig`:
172
203
 
173
204
  ```twig
174
205
  <!DOCTYPE html>
175
206
  <html>
176
207
  <head>
177
- <title>{% block title %}My App{% endblock %}</title>
208
+ <title>{% block title %}My App{% endblock %}</title>
209
+ <link rel="stylesheet" href="/css/tina4.min.css">
210
+ {% block stylesheets %}{% endblock %}
178
211
  </head>
179
212
  <body>
180
- {% block content %}{% endblock %}
213
+ {% block content %}{% endblock %}
214
+ <script src="/js/frond.js"></script>
215
+ {% block javascripts %}{% endblock %}
181
216
  </body>
182
217
  </html>
183
218
  ```
184
219
 
185
- ### Child template (`templates/home.twig`)
220
+ Create `src/templates/pages/home.twig`:
186
221
 
187
222
  ```twig
188
223
  {% extends "base.twig" %}
189
-
190
- {% block title %}Home{% endblock %}
191
-
192
224
  {% block content %}
193
- <h1>Hello {{ name }}!</h1>
194
-
195
- {% if items %}
225
+ <div class="container mt-4">
226
+ <h1>{{ title }}</h1>
196
227
  <ul>
197
- {% for item in items %}
198
- <li>{{ loop.index }}. {{ item | capitalize }}</li>
228
+ {% for user in users %}
229
+ <li>{{ user.name }} -- {{ user.email }}</li>
199
230
  {% endfor %}
200
231
  </ul>
201
- {% else %}
202
- <p>No items found.</p>
203
- {% endif %}
232
+ </div>
204
233
  {% endblock %}
205
234
  ```
206
235
 
207
- ### Rendering
236
+ Render it from a route:
208
237
 
209
238
  ```ruby
210
- Tina4.get "/home" do |request, response|
211
- response.render("home.twig", {
212
- name: "Alice",
213
- items: ["apple", "banana", "cherry"]
214
- })
239
+ Tina4.get "/" do |request, response|
240
+ users = User.all(limit: 20).map(&:to_hash)
241
+ response.render("pages/home.twig", { title: "Users", users: users })
215
242
  end
216
243
  ```
217
244
 
218
- ### Filters
245
+ ### 7. Seed, test, deploy
219
246
 
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 #}
247
+ ```bash
248
+ tina4ruby seed # Run seeders from src/seeds/
249
+ tina4ruby test # Run test suite
250
+ tina4ruby build # Build distributable
253
251
  ```
254
252
 
255
- ## Database
253
+ For the complete step-by-step guide, visit **[tina4.com](https://tina4.com)**.
256
254
 
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")
255
+ ---
268
256
 
269
- # MSSQL
270
- db = Tina4::Database.new("mssql://localhost:1433/mydb")
271
- ```
257
+ ## Features
272
258
 
273
- ### Querying
259
+ ### Routing
274
260
 
275
261
  ```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)")
262
+ Tina4.get "/api/items" do |request, response| # Public by default
263
+ response.json({ items: [] })
264
+ end
297
265
 
298
- # Transactions
299
- db.transaction do |tx|
300
- tx.insert("accounts", { name: "Savings", balance: 1000 })
301
- tx.update("accounts", { balance: 500 }, { id: 1 })
266
+ Tina4.post "/api/webhook", auth: false do |request, response| # Make a write route public
267
+ response.json({ ok: true })
302
268
  end
303
269
 
304
- # Introspection
305
- db.tables # ["users", "posts", ...]
306
- db.table_exists?("users") # true
307
- db.columns("users") # [{name: "id", type: "INTEGER", ...}, ...]
270
+ Tina4.secure_get "/api/admin/stats" do |request, response| # Protect a GET route
271
+ response.json({ secret: true })
272
+ end
308
273
  ```
309
274
 
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
- ```
275
+ Path parameter types: `{id}` (string), `{id:int}`, `{price:float}`, `{path:path}` (greedy).
322
276
 
323
- ## ORM
277
+ ### ORM
324
278
 
325
- Define models with a field DSL:
279
+ Active Record with typed fields, validation, soft delete, relationships, scopes, and multi-database support.
326
280
 
327
281
  ```ruby
328
282
  class User < Tina4::ORM
329
283
  integer_field :id, primary_key: true, auto_increment: true
330
- string_field :name, nullable: false
284
+ string_field :name, nullable: false, length: 100
331
285
  string_field :email, length: 255
286
+ string_field :role, default: "user"
332
287
  integer_field :age, default: 0
333
- datetime_field :created_at
334
288
  end
335
289
 
336
- # Set the database connection
337
- Tina4.database = Tina4::Database.new("sqlite://app.db")
338
- ```
339
-
340
- ### CRUD Operations
341
-
342
- ```ruby
343
- # Create
290
+ # CRUD
344
291
  user = User.new(name: "Alice", email: "alice@example.com")
345
292
  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
293
  user = User.find(1)
358
- user.name = "Alice Updated"
359
- user.save
360
-
361
- # Delete
362
294
  user.delete
363
295
 
364
- # Load into existing instance
365
- user = User.new
366
- user.id = 1
367
- user.load
296
+ # Relationships
297
+ orders = user.has_many("Order", "user_id")
298
+ profile = user.has_one("Profile", "user_id")
368
299
 
369
- # Serialization
370
- user.to_hash # { id: 1, name: "Alice", ... }
371
- user.to_json # '{"id":1,"name":"Alice",...}'
372
- ```
300
+ # Soft delete, scopes, caching
301
+ user.soft_delete
302
+ active_admins = User.scope("active").scope("admin").select
303
+ users = User.cached("SELECT * FROM users", ttl: 300)
373
304
 
374
- ### Field Types
305
+ # Multi-database
306
+ Tina4.database = Tina4::Database.new("sqlite://app.db") # Default
307
+ Tina4.database("audit", Tina4::Database.new("sqlite://audit.db")) # Named
375
308
 
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
309
+ class AuditLog < Tina4::ORM
310
+ self.db_name = "audit" # Uses named connection
311
+ end
388
312
  ```
389
313
 
390
- ## Migrations
314
+ ### Database
391
315
 
392
- ```bash
393
- # Create a migration
394
- tina4 migrate --create "create users table"
316
+ Unified interface across 5 engines:
395
317
 
396
- # Run pending migrations
397
- tina4 migrate
318
+ ```ruby
319
+ db = Tina4::Database.new("sqlite://data/app.db")
320
+ db = Tina4::Database.new("postgres://localhost:5432/mydb", username: "user", password: "pass")
321
+ db = Tina4::Database.new("mysql://localhost:3306/mydb", username: "user", password: "pass")
322
+ db = Tina4::Database.new("mssql://localhost:1433/mydb", username: "sa", password: "pass")
323
+ db = Tina4::Database.new("firebird://localhost:3050/path/to/db", username: "SYSDBA", password: "masterkey")
398
324
 
399
- # Rollback
400
- tina4 migrate --rollback 1
325
+ result = db.fetch("SELECT * FROM users WHERE age > ?", [18], limit: 20, skip: 0)
326
+ row = db.fetch_one("SELECT * FROM users WHERE id = ?", [1])
327
+ db.insert("users", { name: "Alice", email: "alice@test.com" })
328
+ db.commit
401
329
  ```
402
330
 
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:
331
+ ### Middleware
419
332
 
420
333
  ```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
334
+ Tina4.before("/protected") do |request, response|
335
+ unless request.headers["authorization"]
336
+ return request, response.json({ error: "Unauthorized" }, 401)
337
+ end
429
338
  end
430
339
 
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
340
+ Tina4.get "/protected" do |request, response|
341
+ response.json({ secret: true })
342
+ end
435
343
  ```
436
344
 
437
- ### Protecting Routes
345
+ ### JWT Authentication
438
346
 
439
347
  ```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
348
+ auth = Tina4::Auth.new
349
+ token = auth.get_token({ user_id: 42 })
350
+ payload = auth.get_payload(token)
351
+ ```
445
352
 
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
353
+ POST/PUT/PATCH/DELETE routes require `Authorization: Bearer <token>` by default. Use `auth: false` to make public, `secure_get` to protect GET routes.
451
354
 
452
- Tina4.secure_get "/api/data", auth: custom_auth do |request, response|
453
- response.json({ data: "protected" })
454
- end
355
+ ### Sessions
356
+
357
+ ```ruby
358
+ request.session["user_id"] = 42
359
+ user_id = request.session["user_id"]
455
360
  ```
456
361
 
457
- ## Sessions
362
+ Backends: file (default), Redis, MongoDB. Set via `TINA4_SESSION_HANDLER` in `.env`.
458
363
 
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
364
+ ### Queues
466
365
 
467
- Tina4.get "/profile" do |request, response|
468
- user_id = request.session["user_id"]
469
- response.json({ user_id: user_id })
470
- end
366
+ ```ruby
367
+ producer = Tina4::Producer.new(Tina4::Queue.new(topic: "emails"))
368
+ producer.produce({ to: "alice@example.com" })
471
369
 
472
- Tina4.post "/logout" do |request, response|
473
- request.session.destroy
474
- response.json({ logged_out: true })
475
- end
370
+ consumer = Tina4::Consumer.new(Tina4::Queue.new(topic: "emails"))
371
+ consumer.each { |msg| send_email(msg.data) }
476
372
  ```
477
373
 
478
- Session backends: `:file` (default), `:redis`, `:mongo`.
479
-
480
- ## Middleware
374
+ ### GraphQL
481
375
 
482
376
  ```ruby
483
- # Run before every request
484
- Tina4.before do |request, response|
485
- puts "Request: #{request.method} #{request.path}"
486
- end
377
+ gql = Tina4::GraphQL.new
378
+ gql.schema.from_orm(User)
379
+ gql.register_route("/graphql") # GET = GraphiQL IDE, POST = queries
380
+ ```
487
381
 
488
- # Run after every request
489
- Tina4.after do |request, response|
490
- puts "Response: #{response.status}"
491
- end
382
+ ### WebSocket
492
383
 
493
- # Pattern matching
494
- Tina4.before("/api") do |request, response|
495
- # Only runs for paths starting with /api
496
- end
384
+ ```ruby
385
+ ws = Tina4::WebSocketManager.new
497
386
 
498
- Tina4.before(/\/admin\/.*/) do |request, response|
499
- # Regex pattern matching
500
- return false unless request.session["role"] == "admin" # halts request
387
+ ws.route "/ws/chat" do |connection, message|
388
+ ws.broadcast("/ws/chat", "User said: #{message}")
501
389
  end
502
390
  ```
503
391
 
504
- ## Swagger / OpenAPI
392
+ ### Swagger / OpenAPI
505
393
 
506
- Auto-generated API documentation at `/swagger`:
394
+ Auto-generated at `/swagger`:
507
395
 
508
396
  ```ruby
509
397
  Tina4.get "/api/users", swagger_meta: {
510
- summary: "List all users",
511
- tags: ["Users"],
512
- description: "Returns a paginated list of users"
398
+ summary: "Get all users",
399
+ tags: ["users"]
513
400
  } do |request, response|
514
- response.json(users)
401
+ response.json(User.all.map(&:to_hash))
515
402
  end
516
403
  ```
517
404
 
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
405
+ ### Event System
525
406
 
526
407
  ```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
408
+ Tina4.on("user.created", priority: 10) do |user|
409
+ send_notification("New user: #{user[:name]}")
541
410
  end
542
411
 
543
- # Register the /graphql endpoint
544
- gql = Tina4::GraphQL.new(schema)
545
- gql.register_route # POST /graphql + GET /graphql (GraphiQL UI)
412
+ Tina4.emit("user.created", { name: "Alice" })
546
413
  ```
547
414
 
548
- ### ORM Auto-Schema
549
-
550
- Generate full CRUD queries and mutations from your ORM models with one line:
415
+ ### Template Engine (Frond)
551
416
 
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
417
+ Twig-compatible, 35+ filters, macros, inheritance, fragment caching, sandboxing:
556
418
 
557
- gql = Tina4::GraphQL.new(schema)
558
- gql.register_route("/graphql")
419
+ ```twig
420
+ {% extends "base.twig" %}
421
+ {% block content %}
422
+ <h1>{{ title | upper }}</h1>
423
+ {% for item in items %}
424
+ <p>{{ item.name }} -- {{ item.price | number_format(2) }}</p>
425
+ {% endfor %}
426
+
427
+ {% cache "sidebar" 300 %}
428
+ {% include "partials/sidebar.twig" %}
429
+ {% endcache %}
430
+ {% endblock %}
559
431
  ```
560
432
 
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 }
433
+ ### CRUD Scaffolding
570
434
 
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
- }
435
+ ```ruby
436
+ Tina4.get "/admin/users" do |request, response|
437
+ response.json(Tina4::CRUD.to_crud(request, {
438
+ sql: "SELECT id, name, email FROM users",
439
+ title: "User Management",
440
+ primary_key: "id"
441
+ }))
442
+ end
593
443
  ```
594
444
 
595
- ### Programmatic Execution
445
+ ### REST Client
596
446
 
597
447
  ```ruby
598
- gql = Tina4::GraphQL.new(schema)
448
+ api = Tina4::API.new("https://api.example.com", headers: {
449
+ "Authorization" => "Bearer xyz"
450
+ })
451
+ result = api.get("/users/42")
452
+ ```
599
453
 
600
- # Execute a query directly
601
- result = gql.execute('{ hello }')
602
- puts result["data"]["hello"] # "Hello World!"
454
+ ### Data Seeder
603
455
 
604
- # With variables
605
- result = gql.execute(
606
- 'query($id: ID!) { user(id: $id) { name } }',
607
- variables: { "id" => 42 }
608
- )
456
+ ```ruby
457
+ fake = Tina4::FakeData.new
458
+ fake.name # "Alice Johnson"
459
+ fake.email # "alice.johnson@example.com"
609
460
 
610
- # Handle an HTTP request body (JSON string)
611
- result = gql.handle_request('{"query": "{ hello }"}')
461
+ Tina4.seed_orm(User, count: 50)
612
462
  ```
613
463
 
614
- Visit `http://localhost:7145/graphql` for the interactive GraphiQL UI.
615
-
616
- ## REST API Client
464
+ ### Email / Messenger
617
465
 
618
466
  ```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
467
+ mail = Tina4::Messenger.new
468
+ mail.send(to: "user@test.com", subject: "Welcome", body: "<h1>Hi!</h1>", html: true)
469
+ ```
631
470
 
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")
471
+ ### In-Memory Cache
636
472
 
637
- # File upload
638
- api.upload("/files", "path/to/file.pdf")
473
+ ```ruby
474
+ cache = Tina4::Cache.new
475
+ cache.set("key", "value", ttl: 300)
476
+ cache.tag("users").flush
639
477
  ```
640
478
 
641
- ## Environment Variables
479
+ ### SCSS, Localization, Inline Testing
642
480
 
643
- Tina4 auto-creates and loads `.env` files:
481
+ - **SCSS**: Drop `.scss` in `src/scss/` -- auto-compiled to CSS. Variables, nesting, mixins, `@import`, `@extend`.
482
+ - **i18n**: JSON translation files, 6 languages (en, fr, af, zh, ja, es), placeholder interpolation.
483
+ - **Inline tests**: `test_method :add, assert_equal: [[5, 3], 8]` on any method.
644
484
 
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
- ```
485
+ ---
654
486
 
655
- `API_KEY` enables a static bearer token bypass — any request with `Authorization: Bearer <API_KEY>` is granted access without JWT validation.
487
+ ## Dev Mode
656
488
 
657
- Supports environment-specific files: `.env.development`, `.env.production`, `.env.test`.
489
+ Set `TINA4_DEBUG_LEVEL=DEBUG` in `.env` to enable:
658
490
 
659
- ## CLI Commands
491
+ - **Live reload** -- browser auto-refreshes on code changes
492
+ - **CSS hot-reload** -- SCSS changes apply without page refresh
493
+ - **Error overlay** -- rich error display in the browser
494
+ - **Dev admin** at `/__dev/` with tabs: Routes, Queue, Mailbox, Messages, Database, Requests, Errors, WebSocket, System, Tools, Tina4
660
495
 
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
- ```
496
+ ---
674
497
 
675
- ## Project Structure
498
+ ## CLI Reference
676
499
 
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
500
+ ```bash
501
+ tina4ruby init [dir] # Scaffold a new project
502
+ tina4ruby serve [port] # Start dev server (default: 7147)
503
+ tina4ruby migrate # Run pending migrations
504
+ tina4ruby migrate --create <desc># Create a migration file
505
+ tina4ruby migrate --rollback # Rollback last batch
506
+ tina4ruby seed # Run seeders from src/seeds/
507
+ tina4ruby routes # List all registered routes
508
+ tina4ruby test # Run test suite
509
+ tina4ruby build # Build distributable gem
510
+ tina4ruby ai [--all] # Detect AI tools and install context
691
511
  ```
692
512
 
693
- Routes in `routes/` are auto-discovered at startup:
513
+ ## Environment
694
514
 
695
- ```ruby
696
- # routes/users.rb
697
- Tina4.get "/api/users" do |request, response|
698
- response.json(User.all.map(&:to_hash))
699
- end
515
+ ```bash
516
+ SECRET=your-jwt-secret
517
+ DATABASE_URL=sqlite://data/app.db
518
+ DATABASE_USERNAME=
519
+ DATABASE_PASSWORD=
520
+ TINA4_DEBUG_LEVEL=DEBUG # DEBUG, INFO, WARNING, ERROR, ALL
521
+ TINA4_LANGUAGE=en # en, fr, af, zh, ja, es
522
+ TINA4_SESSION_HANDLER=SessionFileHandler
523
+ SWAGGER_TITLE=My API
700
524
  ```
701
525
 
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
526
+ ## Carbonah Green Benchmarks
709
527
 
710
- ```ruby
711
- # app.rb
712
- require "tina4"
713
-
714
- # Database
715
- Tina4.database = Tina4::Database.new("sqlite://app.db")
528
+ All 9 benchmarks rated **A+** (South Africa grid, 1000 iterations each):
716
529
 
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
530
+ | Benchmark | SCI (gCO2eq) | Grade |
531
+ |-----------|-------------|-------|
532
+ | JSON Hello World | 0.000897 | A+ |
533
+ | Single DB Query | 0.000561 | A+ |
534
+ | Multiple DB Queries | 0.001402 | A+ |
535
+ | Template Rendering | 0.003351 | A+ |
536
+ | Large JSON Payload | 0.001019 | A+ |
537
+ | Plaintext Response | 0.000391 | A+ |
538
+ | CRUD Cycle | 0.000473 | A+ |
539
+ | Paginated Query | 0.001027 | A+ |
540
+ | Framework Startup | 0.00267 | A+ |
723
541
 
724
- # Routes
725
- Tina4.get "/" do |request, response|
726
- response.render("home.twig", { todos: Todo.all.map(&:to_hash) })
727
- end
542
+ Startup: 37ms | Memory: 34.9MB | SCI: 0.00267
728
543
 
729
- Tina4.get "/api/todos" do |request, response|
730
- response.json(Todo.all.map(&:to_hash))
731
- end
544
+ Run locally: `ruby benchmarks/run_carbonah.rb`
732
545
 
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
546
+ ---
737
547
 
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
548
+ ## Documentation
744
549
 
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
- ```
550
+ Full guides, API reference, and examples at **[tina4.com](https://tina4.com)**.
750
551
 
751
- ## Requirements
552
+ ## License
752
553
 
753
- - Ruby >= 3.1.0
754
- - Works on Windows, macOS, and Linux
554
+ MIT (c) 2007-2026 Tina4 Stack
555
+ https://opensource.org/licenses/MIT
755
556
 
756
- ## License
557
+ ---
757
558
 
758
- MIT
559
+ <p align="center"><b>Tina4</b> -- The framework that keeps out of the way of your coding.</p>
759
560
 
760
561
  ---
761
562