tina4ruby 0.5.2 → 3.2.1

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