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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +360 -559
- data/exe/{tina4 → tina4ruby} +1 -0
- data/lib/tina4/ai.rb +312 -0
- data/lib/tina4/auth.rb +44 -3
- data/lib/tina4/auto_crud.rb +163 -0
- data/lib/tina4/cli.rb +242 -77
- data/lib/tina4/constants.rb +46 -0
- data/lib/tina4/cors.rb +74 -0
- data/lib/tina4/database/sqlite3_adapter.rb +139 -0
- data/lib/tina4/database.rb +43 -7
- data/lib/tina4/debug.rb +4 -79
- data/lib/tina4/dev_admin.rb +1162 -0
- data/lib/tina4/dev_mailbox.rb +191 -0
- data/lib/tina4/dev_reload.rb +9 -9
- data/lib/tina4/drivers/firebird_driver.rb +19 -3
- data/lib/tina4/drivers/mssql_driver.rb +3 -3
- data/lib/tina4/drivers/mysql_driver.rb +4 -4
- data/lib/tina4/drivers/postgres_driver.rb +9 -2
- data/lib/tina4/drivers/sqlite_driver.rb +1 -1
- data/lib/tina4/env.rb +42 -2
- data/lib/tina4/error_overlay.rb +252 -0
- data/lib/tina4/events.rb +90 -0
- data/lib/tina4/field_types.rb +4 -0
- data/lib/tina4/frond.rb +1336 -0
- data/lib/tina4/gallery/auth/meta.json +1 -0
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -0
- data/lib/tina4/gallery/database/meta.json +1 -0
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -0
- data/lib/tina4/gallery/error-overlay/meta.json +1 -0
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -0
- data/lib/tina4/gallery/orm/meta.json +1 -0
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -0
- data/lib/tina4/gallery/queue/meta.json +1 -0
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +27 -0
- data/lib/tina4/gallery/rest-api/meta.json +1 -0
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -0
- data/lib/tina4/gallery/templates/meta.json +1 -0
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -0
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -0
- data/lib/tina4/health.rb +39 -0
- data/lib/tina4/html_element.rb +148 -0
- data/lib/tina4/localization.rb +2 -2
- data/lib/tina4/log.rb +203 -0
- data/lib/tina4/messenger.rb +484 -0
- data/lib/tina4/migration.rb +132 -29
- data/lib/tina4/orm.rb +337 -31
- data/lib/tina4/public/css/tina4.css +178 -1
- data/lib/tina4/public/css/tina4.min.css +1 -2
- data/lib/tina4/public/favicon.ico +0 -0
- data/lib/tina4/public/images/logo.svg +5 -0
- data/lib/tina4/public/images/tina4-logo-icon.webp +0 -0
- data/lib/tina4/public/js/frond.min.js +420 -0
- data/lib/tina4/public/js/tina4-dev-admin.min.js +367 -0
- data/lib/tina4/public/js/tina4.min.js +93 -0
- data/lib/tina4/public/swagger/index.html +90 -0
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -0
- data/lib/tina4/queue.rb +40 -4
- data/lib/tina4/queue_backends/lite_backend.rb +88 -0
- data/lib/tina4/rack_app.rb +314 -23
- data/lib/tina4/rate_limiter.rb +123 -0
- data/lib/tina4/request.rb +61 -15
- data/lib/tina4/response.rb +54 -24
- data/lib/tina4/response_cache.rb +134 -0
- data/lib/tina4/router.rb +90 -15
- data/lib/tina4/scss_compiler.rb +2 -2
- data/lib/tina4/seeder.rb +56 -61
- data/lib/tina4/service_runner.rb +303 -0
- data/lib/tina4/session.rb +85 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +1 -1
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -0
- data/lib/tina4/shutdown.rb +84 -0
- data/lib/tina4/sql_translation.rb +295 -0
- data/lib/tina4/template.rb +36 -6
- data/lib/tina4/templates/base.twig +2 -2
- data/lib/tina4/templates/errors/302.twig +14 -0
- data/lib/tina4/templates/errors/401.twig +9 -0
- data/lib/tina4/templates/errors/403.twig +22 -15
- data/lib/tina4/templates/errors/404.twig +22 -15
- data/lib/tina4/templates/errors/500.twig +31 -15
- data/lib/tina4/templates/errors/502.twig +9 -0
- data/lib/tina4/templates/errors/503.twig +12 -0
- data/lib/tina4/templates/errors/base.twig +37 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -18
- data/lib/tina4.rb +57 -21
- metadata +51 -19
- data/lib/tina4/public/js/tina4.js +0 -134
- data/lib/tina4/public/js/tina4helper.js +0 -387
data/README.md
CHANGED
|
@@ -1,761 +1,562 @@
|
|
|
1
|
-
|
|
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> •
|
|
14
|
+
<a href="#getting-started">Getting Started</a> •
|
|
15
|
+
<a href="#features">Features</a> •
|
|
16
|
+
<a href="#cli-reference">CLI Reference</a> •
|
|
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
|
-
|
|
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
|
+
## Quickstart
|
|
8
31
|
|
|
9
32
|
```bash
|
|
10
33
|
gem install tina4ruby
|
|
11
|
-
|
|
12
|
-
cd
|
|
13
|
-
|
|
14
|
-
|
|
34
|
+
tina4ruby init my-app
|
|
35
|
+
cd my-app
|
|
36
|
+
tina4ruby serve
|
|
37
|
+
# -> http://localhost:7147
|
|
15
38
|
```
|
|
16
39
|
|
|
17
|
-
|
|
40
|
+
That's it. Zero configuration, zero classes, zero boilerplate.
|
|
18
41
|
|
|
19
|
-
|
|
42
|
+
---
|
|
20
43
|
|
|
21
|
-
|
|
44
|
+
## What's Included
|
|
22
45
|
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
response.json({ deleted: true })
|
|
54
|
-
end
|
|
69
|
+
## Install
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
response.json({ method: request.method })
|
|
59
|
-
end
|
|
71
|
+
```bash
|
|
72
|
+
gem install tina4ruby
|
|
60
73
|
```
|
|
61
74
|
|
|
62
|
-
###
|
|
75
|
+
### Optional database drivers
|
|
63
76
|
|
|
64
|
-
|
|
77
|
+
Install only what you need:
|
|
65
78
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
Tina4.post "/api/webhook", auth: false do |request, response|
|
|
79
|
-
response.json({ received: true })
|
|
80
|
-
end
|
|
88
|
+
## Getting Started
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
custom_auth = lambda do |env|
|
|
84
|
-
env["HTTP_X_API_KEY"] == "my-secret"
|
|
85
|
-
end
|
|
90
|
+
### 1. Create a project
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
```bash
|
|
93
|
+
tina4ruby init my-app
|
|
94
|
+
cd my-app
|
|
90
95
|
```
|
|
91
96
|
|
|
92
|
-
|
|
97
|
+
This creates:
|
|
93
98
|
|
|
94
|
-
|
|
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.
|
|
98
|
-
response.json({
|
|
121
|
+
Tina4.get "/api/hello" do |request, response|
|
|
122
|
+
response.json({ message: "Hello from Tina4!" })
|
|
99
123
|
end
|
|
100
124
|
|
|
101
|
-
Tina4.
|
|
102
|
-
response.json({
|
|
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
|
-
|
|
130
|
+
Visit `http://localhost:7147/api/hello` -- routes are auto-discovered, no requires needed.
|
|
107
131
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
142
|
+
Create and run a migration:
|
|
116
143
|
|
|
117
|
-
```
|
|
118
|
-
|
|
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
|
-
|
|
148
|
+
Edit the generated SQL:
|
|
134
149
|
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
response.html("<h1>Hello</h1>")
|
|
163
|
+
### 4. Create an ORM model
|
|
142
164
|
|
|
143
|
-
|
|
144
|
-
response.render("pages/home.twig", { title: "Welcome" })
|
|
165
|
+
Create `src/orm/user.rb`:
|
|
145
166
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
response.text("OK")
|
|
176
|
+
### 5. Build a REST API
|
|
152
177
|
|
|
153
|
-
|
|
154
|
-
response.file("path/to/document.pdf")
|
|
178
|
+
Create `src/routes/users.rb`:
|
|
155
179
|
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
Tina4 uses a Twig-compatible template engine. Templates go in `templates/` or `src/templates/`.
|
|
200
|
+
### 6. Add a template
|
|
170
201
|
|
|
171
|
-
|
|
202
|
+
Create `src/templates/base.twig`:
|
|
172
203
|
|
|
173
204
|
```twig
|
|
174
205
|
<!DOCTYPE html>
|
|
175
206
|
<html>
|
|
176
207
|
<head>
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
{% if items %}
|
|
225
|
+
<div class="container mt-4">
|
|
226
|
+
<h1>{{ title }}</h1>
|
|
196
227
|
<ul>
|
|
197
|
-
{% for
|
|
198
|
-
|
|
228
|
+
{% for user in users %}
|
|
229
|
+
<li>{{ user.name }} -- {{ user.email }}</li>
|
|
199
230
|
{% endfor %}
|
|
200
231
|
</ul>
|
|
201
|
-
|
|
202
|
-
<p>No items found.</p>
|
|
203
|
-
{% endif %}
|
|
232
|
+
</div>
|
|
204
233
|
{% endblock %}
|
|
205
234
|
```
|
|
206
235
|
|
|
207
|
-
|
|
236
|
+
Render it from a route:
|
|
208
237
|
|
|
209
238
|
```ruby
|
|
210
|
-
Tina4.get "/
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
###
|
|
245
|
+
### 7. Seed, test, deploy
|
|
219
246
|
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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 }} {# <b>hi</b> #}
|
|
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
|
-
|
|
253
|
+
For the complete step-by-step guide, visit **[tina4.com](https://tina4.com)**.
|
|
256
254
|
|
|
257
|
-
|
|
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
|
-
|
|
270
|
-
db = Tina4::Database.new("mssql://localhost:1433/mydb")
|
|
271
|
-
```
|
|
257
|
+
## Features
|
|
272
258
|
|
|
273
|
-
###
|
|
259
|
+
### Routing
|
|
274
260
|
|
|
275
261
|
```ruby
|
|
276
|
-
#
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
#
|
|
299
|
-
|
|
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
|
-
#
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
+
### ORM
|
|
324
278
|
|
|
325
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
365
|
-
|
|
366
|
-
user.
|
|
367
|
-
user.load
|
|
296
|
+
# Relationships
|
|
297
|
+
orders = user.has_many("Order", "user_id")
|
|
298
|
+
profile = user.has_one("Profile", "user_id")
|
|
368
299
|
|
|
369
|
-
#
|
|
370
|
-
user.
|
|
371
|
-
|
|
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
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
314
|
+
### Database
|
|
391
315
|
|
|
392
|
-
|
|
393
|
-
# Create a migration
|
|
394
|
-
tina4 migrate --create "create users table"
|
|
316
|
+
Unified interface across 5 engines:
|
|
395
317
|
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
###
|
|
345
|
+
### JWT Authentication
|
|
438
346
|
|
|
439
347
|
```ruby
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
355
|
+
### Sessions
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
request.session["user_id"] = 42
|
|
359
|
+
user_id = request.session["user_id"]
|
|
455
360
|
```
|
|
456
361
|
|
|
457
|
-
|
|
362
|
+
Backends: file (default), Redis, MongoDB. Set via `TINA4_SESSION_HANDLER` in `.env`.
|
|
458
363
|
|
|
459
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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.
|
|
473
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
## Middleware
|
|
374
|
+
### GraphQL
|
|
481
375
|
|
|
482
376
|
```ruby
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
489
|
-
Tina4.after do |request, response|
|
|
490
|
-
puts "Response: #{response.status}"
|
|
491
|
-
end
|
|
382
|
+
### WebSocket
|
|
492
383
|
|
|
493
|
-
|
|
494
|
-
Tina4.
|
|
495
|
-
# Only runs for paths starting with /api
|
|
496
|
-
end
|
|
384
|
+
```ruby
|
|
385
|
+
ws = Tina4::WebSocketManager.new
|
|
497
386
|
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
392
|
+
### Swagger / OpenAPI
|
|
505
393
|
|
|
506
|
-
Auto-generated
|
|
394
|
+
Auto-generated at `/swagger`:
|
|
507
395
|
|
|
508
396
|
```ruby
|
|
509
397
|
Tina4.get "/api/users", swagger_meta: {
|
|
510
|
-
summary: "
|
|
511
|
-
tags: ["
|
|
512
|
-
description: "Returns a paginated list of users"
|
|
398
|
+
summary: "Get all users",
|
|
399
|
+
tags: ["users"]
|
|
513
400
|
} do |request, response|
|
|
514
|
-
response.json(
|
|
401
|
+
response.json(User.all.map(&:to_hash))
|
|
515
402
|
end
|
|
516
403
|
```
|
|
517
404
|
|
|
518
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
549
|
-
|
|
550
|
-
Generate full CRUD queries and mutations from your ORM models with one line:
|
|
415
|
+
### Template Engine (Frond)
|
|
551
416
|
|
|
552
|
-
|
|
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
|
-
|
|
558
|
-
|
|
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
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
###
|
|
445
|
+
### REST Client
|
|
596
446
|
|
|
597
447
|
```ruby
|
|
598
|
-
|
|
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
|
-
|
|
601
|
-
result = gql.execute('{ hello }')
|
|
602
|
-
puts result["data"]["hello"] # "Hello World!"
|
|
454
|
+
### Data Seeder
|
|
603
455
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
)
|
|
456
|
+
```ruby
|
|
457
|
+
fake = Tina4::FakeData.new
|
|
458
|
+
fake.name # "Alice Johnson"
|
|
459
|
+
fake.email # "alice.johnson@example.com"
|
|
609
460
|
|
|
610
|
-
|
|
611
|
-
result = gql.handle_request('{"query": "{ hello }"}')
|
|
461
|
+
Tina4.seed_orm(User, count: 50)
|
|
612
462
|
```
|
|
613
463
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
## REST API Client
|
|
464
|
+
### Email / Messenger
|
|
617
465
|
|
|
618
466
|
```ruby
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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
|
-
|
|
638
|
-
|
|
473
|
+
```ruby
|
|
474
|
+
cache = Tina4::Cache.new
|
|
475
|
+
cache.set("key", "value", ttl: 300)
|
|
476
|
+
cache.tag("users").flush
|
|
639
477
|
```
|
|
640
478
|
|
|
641
|
-
|
|
479
|
+
### SCSS, Localization, Inline Testing
|
|
642
480
|
|
|
643
|
-
|
|
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
|
-
|
|
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
|
-
|
|
487
|
+
## Dev Mode
|
|
656
488
|
|
|
657
|
-
|
|
489
|
+
Set `TINA4_DEBUG_LEVEL=DEBUG` in `.env` to enable:
|
|
658
490
|
|
|
659
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
498
|
+
## CLI Reference
|
|
676
499
|
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
513
|
+
## Environment
|
|
694
514
|
|
|
695
|
-
```
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
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
|
-
|
|
730
|
-
response.json(Todo.all.map(&:to_hash))
|
|
731
|
-
end
|
|
544
|
+
Run locally: `ruby benchmarks/run_carbonah.rb`
|
|
732
545
|
|
|
733
|
-
|
|
734
|
-
todo = Todo.create(title: request.json_body["title"])
|
|
735
|
-
response.json(todo.to_hash, 201)
|
|
736
|
-
end
|
|
546
|
+
---
|
|
737
547
|
|
|
738
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
552
|
+
## License
|
|
752
553
|
|
|
753
|
-
-
|
|
754
|
-
|
|
554
|
+
MIT (c) 2007-2026 Tina4 Stack
|
|
555
|
+
https://opensource.org/licenses/MIT
|
|
755
556
|
|
|
756
|
-
|
|
557
|
+
---
|
|
757
558
|
|
|
758
|
-
|
|
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
|
|