cybertrain 0.1.0 → 0.2.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/README.md +240 -73
- data/cybertrain/cli/build.rb +192 -0
- data/cybertrain/cli/new_app.rb +19 -14
- data/cybertrain/cli/scaffold.rb +7 -0
- data/cybertrain/cli/templates.rb +29 -20
- data/cybertrain/cli/toolchain.rb +787 -0
- data/cybertrain/cli.rb +151 -11
- data/cybertrain/generator/inflector.rb +151 -86
- data/cybertrain/version.rb +5 -1
- data/exe/cybertrain +9 -1
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2bfb12d19791bff7ce88504f9ffb5fe6e2d90ae7a55aa24f5571b1909cf11976
|
|
4
|
+
data.tar.gz: 0eaac7a028bbf9be6c013b716ecec76f02c91fb10aa04e855f7b777f250c8712
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66bf793c97cf16d109519514b10c15d929e627137e58a4663d04ad3e88a746fa12e95b39358f15ebe8ba643107a2f8f30dbee54cbc9e95ea5127a4cc870fb417
|
|
7
|
+
data.tar.gz: 02bcced8e7ab059cb479b06835420eb241271b4447ed0e7d16c2801c6ed50b6b0d3587ff6557ae9b36501a5a108b05c2a4e7d5757caf7887c353ed41e4c2deb7
|
data/README.md
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="site/assets/brand/lockup-stacked-dark.svg">
|
|
4
|
+
<img alt="CyberTrain" src="site/assets/brand/lockup-stacked-light.svg" width="360">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://saeki-mototsune.github.io/CyberTrain/">Homepage</a> ·
|
|
9
|
+
<a href="https://saeki-mototsune.github.io/CyberTrain/tutorial.html">Tutorial</a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
1
12
|
# cybertrain
|
|
2
13
|
|
|
3
14
|
cybertrain is a Rails-shaped web application framework written natively for
|
|
4
15
|
[Spinel](https://github.com/matz/spinel), matz's ahead-of-time Ruby compiler.
|
|
5
16
|
There is no Rack, no gems at runtime, and no Ruby interpreter in the deployed
|
|
6
|
-
artifact: `
|
|
7
|
-
and the framework itself — into a single native binary. Where Rails leans on
|
|
17
|
+
artifact: `cybertrain build` compiles an application — controllers, models,
|
|
18
|
+
views and the framework itself — into a single native binary. Where Rails leans on
|
|
8
19
|
runtime metaprogramming (`method_missing`, `define_method`, `eval`) for
|
|
9
20
|
`before_action :set_post` or `Post.where(title: "x")`, cybertrain gets the
|
|
10
21
|
same vocabulary by generating plain, committed Ruby from `db/schema.rb` and
|
|
@@ -25,51 +36,135 @@ Rails" below.
|
|
|
25
36
|
|
|
26
37
|
## Requirements
|
|
27
38
|
|
|
28
|
-
- Spinel `2026.09.12
|
|
29
|
-
in [.github/workflows/ci.yml](.github/workflows/ci.yml));
|
|
30
|
-
other versions.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
- Spinel `2026.09.12` (`Cybertrain::SPINEL_TAG`, the same release as
|
|
40
|
+
`SPINEL_TAG` in [.github/workflows/ci.yml](.github/workflows/ci.yml));
|
|
41
|
+
untested against other versions. You do not install it by hand: the
|
|
42
|
+
`cybertrain` command builds that release into `~/.cybertrain` the first
|
|
43
|
+
time it needs it (`cybertrain setup` does that on request), unless a
|
|
44
|
+
`spinel` of that release is already on `PATH` — see "Installing the CLI".
|
|
45
|
+
- To build it: a C toolchain (`cc`, or the compiler `CC` names) and `make`
|
|
46
|
+
— Spinel compiles every program to C — plus `git` and `curl`.
|
|
34
47
|
- SQLite 3 headers/library (`libsqlite3-dev` on Debian/Ubuntu; present with
|
|
35
48
|
Xcode's command line tools on macOS) — models use Spinel's FFI directly,
|
|
36
|
-
not a gem.
|
|
37
|
-
|
|
49
|
+
not a gem.
|
|
50
|
+
- Ruby 3.2+ and RubyGems, for the `cybertrain` command only (applications
|
|
51
|
+
never run on CRuby).
|
|
52
|
+
- OpenSSL's headers (`libssl-dev`) are optional: without them Spinel builds
|
|
53
|
+
without its `openssl` package, which cybertrain does not use.
|
|
54
|
+
|
|
55
|
+
`cybertrain doctor` checks these tools and headers and the Spinel install;
|
|
56
|
+
when a required one is missing it also prints the install command for your
|
|
57
|
+
platform.
|
|
38
58
|
|
|
39
|
-
The framework has no separate test runner; it tests itself
|
|
59
|
+
The framework has no separate test runner; it tests itself. `spin` comes
|
|
60
|
+
with Spinel, and `cybertrain spin` runs the copy `cybertrain` installs
|
|
61
|
+
without touching `PATH` (or put that copy on `PATH` with the `export
|
|
62
|
+
PATH=...` line `cybertrain setup` prints, and call `spin` directly):
|
|
40
63
|
|
|
41
64
|
```sh
|
|
42
|
-
spin test
|
|
65
|
+
cybertrain spin test
|
|
43
66
|
```
|
|
44
67
|
|
|
45
68
|
compiles and runs every program under `test/` and diffs its output against a
|
|
46
69
|
committed `test/<name>.rb.expected` snapshot — minitest/RSpec can't run under
|
|
47
70
|
Spinel, since they find test methods by reflection and an ahead-of-time
|
|
48
|
-
compiler has nothing to reflect on at run time. `spin test
|
|
49
|
-
test/<name>.rb` rewrites a snapshot from CRuby's output; a few tests that
|
|
71
|
+
compiler has nothing to reflect on at run time. `cybertrain spin test
|
|
72
|
+
--regen test/<name>.rb` rewrites a snapshot from CRuby's output; a few tests that
|
|
50
73
|
touch SQLite through FFI take theirs from the compiled binary instead
|
|
51
|
-
([spikes/NOTES.md](spikes/NOTES.md), rule 23)
|
|
74
|
+
([spikes/NOTES.md](spikes/NOTES.md), rule 23); `script/regen-snapshot
|
|
75
|
+
test/<name>.rb` does that build-then-capture step for you and prints the diff stat.
|
|
52
76
|
|
|
53
77
|
## Installing the CLI
|
|
54
78
|
|
|
55
79
|
```sh
|
|
56
80
|
gem install cybertrain
|
|
81
|
+
cybertrain new blog # also installs Spinel, the first time (a few minutes)
|
|
82
|
+
cd blog
|
|
83
|
+
cybertrain g scaffold article title:string body:text
|
|
84
|
+
cybertrain db migrate
|
|
85
|
+
cybertrain server # http://127.0.0.1:3000
|
|
86
|
+
cybertrain build # production: dist/
|
|
57
87
|
```
|
|
58
88
|
|
|
59
89
|
The `cybertrain` command is plain Ruby and the gem runs it under CRuby
|
|
60
|
-
(3.2 or newer); the framework
|
|
61
|
-
`cybertrain
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
(3.2 or newer); neither the framework nor Spinel is in the gem. Its
|
|
91
|
+
commands (`cybertrain help` prints them):
|
|
92
|
+
|
|
93
|
+
- `cybertrain new NAME [--path DIR | --version V | --git URL [--ref R]]
|
|
94
|
+
[--skip-spin]` creates an application in NAME, then runs `spin lock` and
|
|
95
|
+
`spin run gen` in it, installing Spinel first when it is needed.
|
|
96
|
+
- `cybertrain generate scaffold NAME field:type ...` (alias `g scaffold`)
|
|
97
|
+
adds a resource to the application in the current directory.
|
|
98
|
+
- `cybertrain db migrate` generates, applies pending migrations, and
|
|
99
|
+
generates again (`spin run gen; spin run db -- migrate; spin run gen`);
|
|
100
|
+
`cybertrain migration` is kept as an alias.
|
|
101
|
+
- `cybertrain db COMMAND...` runs any other database command after `spin
|
|
102
|
+
run gen`: `status`, `rollback [N]` (which generates again afterwards),
|
|
103
|
+
`schema:dump`, `create`.
|
|
104
|
+
- `cybertrain server [PORT]` generates, then starts the development server
|
|
105
|
+
(`spin run gen; spin run NAME`, or `spin run NAME -- PORT`; default port
|
|
106
|
+
3000).
|
|
107
|
+
- `cybertrain build` builds NAME with `app/views/` embedded and assembles
|
|
108
|
+
`dist/` (the binary, `public/`, `storage/`, `tmp/`).
|
|
109
|
+
- `cybertrain spin ARGS...` runs `spin` with the pinned Spinel from any
|
|
110
|
+
directory (`cybertrain spin test`, `cybertrain spin run gen -- --check`).
|
|
111
|
+
- `cybertrain setup [--force]` installs Spinel `2026.09.12` into
|
|
112
|
+
`~/.cybertrain` unless a `spinel` of that release is already on `PATH`;
|
|
113
|
+
`--force` rebuilds the copy under `~/.cybertrain`.
|
|
114
|
+
- `cybertrain doctor` checks the C toolchain, the SQLite headers and the
|
|
115
|
+
Spinel install.
|
|
116
|
+
- `cybertrain version` prints the version.
|
|
117
|
+
|
|
118
|
+
`db`, `server` and `build` run inside an application; NAME is the
|
|
119
|
+
`[package] name` in its `spin.toml`.
|
|
120
|
+
|
|
121
|
+
Every command that runs `spin` (`new`, `db`, `server`, `build`, `spin`) first makes
|
|
122
|
+
sure a Spinel of the release in `Cybertrain::SPINEL_TAG` is available. A
|
|
123
|
+
`spinel` on `PATH` (with a `spin`) whose `spinel --version` reports that
|
|
124
|
+
release is used as it is; otherwise the copy `cybertrain` keeps under
|
|
125
|
+
`~/.cybertrain/spinel/2026.09.12/` is used; otherwise `cybertrain` builds
|
|
126
|
+
that copy from the release tag (`git clone`, `make deps`, `make -j`, `make
|
|
127
|
+
install PREFIX=...`), once, in a few minutes. The build log is
|
|
128
|
+
`~/.cybertrain/log/spinel-2026.09.12-build.log` (its last lines are printed
|
|
129
|
+
if a step fails); if a tool the build needs is missing, `cybertrain` says
|
|
130
|
+
which and prints the install command for your platform before it starts. A
|
|
131
|
+
`spinel` of another release on `PATH` is never used or touched:
|
|
132
|
+
`cybertrain` keeps its own copy (and says so when it installs it).
|
|
133
|
+
|
|
134
|
+
`cybertrain setup` does the same on request and, whenever its own copy is
|
|
135
|
+
the one in use, prints the line that puts it on `PATH`, for running `spin`
|
|
136
|
+
yourself (`~/.cybertrain/bin` holds `spinel` and `spin` links to the release
|
|
137
|
+
in use, so the line survives a release bump; `cybertrain spin ...` needs no
|
|
138
|
+
`PATH` at all):
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
export PATH="$HOME/.cybertrain/bin:$PATH"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Two environment variables override all this: `CYBERTRAIN_HOME` replaces
|
|
145
|
+
`~/.cybertrain` (a path without spaces, quotes, `$` or `:`, which Spinel's
|
|
146
|
+
Makefile and `PATH` cannot take), and `CYBERTRAIN_SPINEL_HOME=PREFIX` forces
|
|
147
|
+
an existing install — `cybertrain` then uses the `spinel` and `spin` in `PREFIX/bin` (or
|
|
148
|
+
in `PREFIX` itself, if `spinel` is directly in it) and stops with an error
|
|
149
|
+
if they are not that release.
|
|
150
|
+
|
|
151
|
+
`cybertrain new` writes the application with its `spin.toml` pointing at
|
|
152
|
+
the release matching the CLI (`cybertrain = { git =
|
|
153
|
+
"https://github.com/saeki-mototsune/cybertrain", ref = "v0.2.0" }`), makes
|
|
154
|
+
sure Spinel is installed, then runs `spin lock` (spin fetches the framework
|
|
155
|
+
into its cache, `~/.cache/spin/packages/`, and pins the commit in
|
|
156
|
+
`spin.lock`) and `spin run gen`, much as `rails new` runs `bundle install`,
|
|
157
|
+
and prints the next steps. Nothing of the framework is copied into the app;
|
|
158
|
+
commit `spin.toml` and `spin.lock`. If Spinel or one of the two `spin`
|
|
159
|
+
steps fails, the files stay, `new` exits with 1 and prints what to run once
|
|
160
|
+
the cause is fixed (for Spinel: `cybertrain setup`, then `cybertrain spin
|
|
161
|
+
lock && cybertrain spin run gen` in the new directory).
|
|
162
|
+
|
|
163
|
+
`--skip-spin` only writes the files: no Spinel install, no `spin lock`, no
|
|
164
|
+
`spin run gen`. `--path DIR` depends on a local checkout instead,
|
|
165
|
+
`--git URL [--ref R]` on the framework at URL (at branch or tag R), and
|
|
166
|
+
`--version V` on an index version once cybertrain is on a `spin-index`.
|
|
167
|
+
Give only one of `--path`, `--version` and `--git`; `--ref` needs `--git`.
|
|
73
168
|
|
|
74
169
|
Working on cybertrain itself, run the CLI from the checkout (`spin install`
|
|
75
170
|
builds `bin/cybertrain.rb` into `~/.local/bin/cybertrain`; `ruby -I.
|
|
@@ -87,7 +182,7 @@ articles and comments.
|
|
|
87
182
|
```sh
|
|
88
183
|
cybertrain new blog
|
|
89
184
|
cd blog
|
|
90
|
-
|
|
185
|
+
cybertrain server # works right away: `new` already locked the framework and ran `spin run gen`
|
|
91
186
|
```
|
|
92
187
|
|
|
93
188
|
```
|
|
@@ -95,10 +190,23 @@ blog/
|
|
|
95
190
|
spin.toml config/app.rb config/routes.rb
|
|
96
191
|
db/schema.rb db/migrate/
|
|
97
192
|
app/controllers/application_controller.rb app/models/ app/views/ app/helpers/
|
|
98
|
-
public/ bin/
|
|
193
|
+
public/ bin/blog.rb bin/gen.rb bin/db.rb
|
|
99
194
|
gen/ storage/ tmp/ test/
|
|
100
195
|
```
|
|
101
196
|
|
|
197
|
+
`bin/blog.rb` is the application's one entry point, the program `spin build
|
|
198
|
+
blog` compiles: `./blog` serves, `./blog migrate` and `./blog db status`
|
|
199
|
+
manage the database. `bin/gen.rb` runs the generator (`spin run gen`), and
|
|
200
|
+
`bin/db.rb` runs the migrator during development (`cybertrain db migrate`
|
|
201
|
+
uses it, since `bin/blog.rb` cannot compile until the first migration has
|
|
202
|
+
generated `gen/models/`). `gen/` already holds `views.rb`, the committed,
|
|
203
|
+
empty table of embedded views (see "How it works").
|
|
204
|
+
|
|
205
|
+
Apps created before `cybertrain build` existed have `bin/server.rb` and
|
|
206
|
+
no `bin/<name>.rb`: copy `bin/<name>.rb` from a fresh `cybertrain new` (with
|
|
207
|
+
the same NAME), delete `bin/server.rb`, add `/dist/` to `.gitignore` (the old
|
|
208
|
+
template did not ignore it) and run `cybertrain spin run gen`.
|
|
209
|
+
|
|
102
210
|
`ApplicationController` starts with the one thing every controller inherits:
|
|
103
211
|
|
|
104
212
|
```ruby
|
|
@@ -181,19 +289,23 @@ end
|
|
|
181
289
|
### Generate, migrate, run
|
|
182
290
|
|
|
183
291
|
```sh
|
|
184
|
-
spin run gen
|
|
185
|
-
|
|
186
|
-
spin run server # http://127.0.0.1:3000
|
|
292
|
+
cybertrain db migrate # spin run gen; spin run db -- migrate; spin run gen
|
|
293
|
+
cybertrain server # http://127.0.0.1:3000; rebuilds on Ruby edits, views reload without a rebuild
|
|
187
294
|
```
|
|
188
295
|
|
|
189
|
-
|
|
190
|
-
|
|
296
|
+
`spin run gen` reads `db/schema.rb` and `config/routes.rb`, scans `app/` and
|
|
297
|
+
writes `gen/`; `spin run db -- migrate` applies `db/migrate/*.rb` and
|
|
298
|
+
rewrites `db/schema.rb`.
|
|
299
|
+
|
|
300
|
+
Re-run `cybertrain spin run gen` (and commit `gen/`) after touching the
|
|
301
|
+
schema, routes, or a controller/model's callbacks and ivars — the dev server does this for
|
|
191
302
|
you automatically (see "How it works").
|
|
192
303
|
|
|
193
304
|
### Add comments, nested under articles
|
|
194
305
|
|
|
195
306
|
```sh
|
|
196
307
|
cybertrain generate scaffold comment commenter:string body:text article:references
|
|
308
|
+
cybertrain db migrate # the scaffold wrote a migration for the comments table
|
|
197
309
|
```
|
|
198
310
|
|
|
199
311
|
`article:references` adds an `article_id` column, index and foreign key —
|
|
@@ -277,6 +389,27 @@ child routes to the nested collection (`article_comments_path`), a persisted
|
|
|
277
389
|
one to the nested member (`article_comment_path`) — generated into
|
|
278
390
|
`gen/routes.rb` from the nested `resources` block above.
|
|
279
391
|
|
|
392
|
+
### Build for deployment
|
|
393
|
+
|
|
394
|
+
```sh
|
|
395
|
+
cybertrain build
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
writes `dist/`: `dist/blog`, the app compiled with every `app/views/`
|
|
399
|
+
template embedded (rendered from memory, error messages still say
|
|
400
|
+
`articles/show.html.erb:12`), `dist/public/`, and empty `dist/storage/` and
|
|
401
|
+
`dist/tmp/`. Copy `dist/` to a machine with the same OS and CPU (the binary
|
|
402
|
+
links the system SQLite) and run `./blog migrate && ./blog`; a built binary
|
|
403
|
+
is production by default (`CYBERTRAIN_ENV` overrides). Set
|
|
404
|
+
`CYBERTRAIN_SECRET_KEY_BASE`. Static files can also be served by the reverse
|
|
405
|
+
proxy from `dist/public/` — see [docs/deploy.md](docs/deploy.md).
|
|
406
|
+
|
|
407
|
+
A running `cybertrain server` skips its rebuilds while `cybertrain build` is
|
|
408
|
+
in progress (the build holds `tmp/cybertrain-build.lock`, since both write
|
|
409
|
+
`build/bin/blog`), logs the skip, and rebuilds as usual on the next save
|
|
410
|
+
once the build has finished. The lock records the build's PID; one left
|
|
411
|
+
behind by a killed build is ignored and removed.
|
|
412
|
+
|
|
280
413
|
## How it works
|
|
281
414
|
|
|
282
415
|
**Build-time code generation.** `spin run gen` (`bin/gen.rb`) executes
|
|
@@ -289,17 +422,24 @@ zero-argument `def`s. From that it writes plain, readable Ruby under `gen/`:
|
|
|
289
422
|
`gen/models/<name>.rb` (accessors, casts, finders, FK-derived associations),
|
|
290
423
|
`gen/routes.rb` (route table, dispatcher, `*_path`/`*_url` helpers),
|
|
291
424
|
`gen/controllers.rb` (`view_assigns` and `run_callback` per controller),
|
|
292
|
-
`gen/migrations.rb
|
|
425
|
+
`gen/migrations.rb`, `gen/views.rb` (the embedded views table, empty unless
|
|
426
|
+
building for deployment) and `gen/app.rb` (the `require_relative` manifest).
|
|
293
427
|
`gen/` is committed, not gitignored — `spin build`/`spin test` have no hook
|
|
294
428
|
to generate first, so the checked-in output has to already be what compiles.
|
|
295
429
|
CI re-runs `spin run gen` and fails on a diff, so stale generated code can't
|
|
296
430
|
merge.
|
|
297
431
|
|
|
298
432
|
**Views are interpreted, not compiled.** `app/views/**/*.html.erb` files look
|
|
299
|
-
like Rails ERB but are
|
|
300
|
-
|
|
301
|
-
development
|
|
302
|
-
|
|
433
|
+
like Rails ERB but are parsed into an AST at request time, then walked by a
|
|
434
|
+
tree-walking interpreter. Where the source comes from depends on the mode:
|
|
435
|
+
in development (and test) the engine reads `app/views/` from disk, and in
|
|
436
|
+
development re-parses a template when it changes, so view edits need no rebuild.
|
|
437
|
+
`cybertrain build` runs `spin run gen -- --embed-views`, which writes every
|
|
438
|
+
template's source into `gen/views.rb` as a string table for that one `spin
|
|
439
|
+
build`, then runs `spin run gen` again to restore the empty table that is
|
|
440
|
+
committed; the production binary renders only that table (parsing each
|
|
441
|
+
template once) and refuses to boot when it is empty (`error: views are not
|
|
442
|
+
embedded in this binary`). Spinel has no `eval`, so this is a closed grammar (literals, `@ivar`/local lookups, calls
|
|
303
443
|
through fixed per-type dispatch tables, `if`/`unless`,
|
|
304
444
|
`each`/`each_with_index`/one helper block), not real Ruby. Full grammar,
|
|
305
445
|
helpers and gaps: [docs/template-language.md](docs/template-language.md).
|
|
@@ -316,13 +456,15 @@ faster and stall-free for this I/O-bound shape at 100 concurrent connections.
|
|
|
316
456
|
and foreign keys on; every query is bound, never interpolated. It's the only
|
|
317
457
|
adapter today (PostgreSQL via `libpq` FFI is noted as possible future work).
|
|
318
458
|
|
|
319
|
-
**The development loop.** `spin run
|
|
320
|
-
`
|
|
459
|
+
**The development loop.** `cybertrain server` (`spin run gen`, then `spin run NAME`) in
|
|
460
|
+
`development` (the default `CYBERTRAIN_ENV`, except in a binary from
|
|
461
|
+
`cybertrain build`) also polls `app/**/*.rb`, `config/**/*.rb`, `db/schema.rb`
|
|
321
462
|
and `gen/**/*.rb` every half second (views are excluded — the engine reloads
|
|
322
|
-
those itself). A change runs `spin run gen && spin build
|
|
323
|
-
background; success
|
|
324
|
-
|
|
325
|
-
|
|
463
|
+
those itself — and so is `gen/views.rb`, empty in development). A change runs `spin run gen && spin build NAME` in the
|
|
464
|
+
background; success requests a restart (the same flag an external `kill -HUP`
|
|
465
|
+
sets): a monitor thread stops the listener and drains open requests, then the
|
|
466
|
+
main thread `execv`s the new binary on the same port and PID, invisible to a
|
|
467
|
+
client mid-session. A failed build keeps serving the old binary and banners the
|
|
326
468
|
compiler output on every HTML response. None of this loads in production.
|
|
327
469
|
|
|
328
470
|
## Differences from Rails
|
|
@@ -330,7 +472,7 @@ compiler output on every HTML response. None of this loads in production.
|
|
|
330
472
|
| Rails | cybertrain |
|
|
331
473
|
| --- | --- |
|
|
332
474
|
| `rails console` | No console — Spinel has no `eval` |
|
|
333
|
-
| Edit code, the running app picks it up | Ruby needs a rebuild; `
|
|
475
|
+
| Edit code, the running app picks it up | Ruby needs a rebuild; `cybertrain server` in development does this for you (rebuild, stop, `execv`) |
|
|
334
476
|
| Edit a view, no reload needed | Same — views are parsed from disk per request in development |
|
|
335
477
|
| `def new` | `def new_action` (`new` would shadow `Klass.new(ctx)`) |
|
|
336
478
|
| `before_action { do_thing }` (implicit `self`) | `before_action { \|c\| c.do_thing }` — no `instance_exec` on a stored block, so callbacks take the controller/record explicitly |
|
|
@@ -361,7 +503,7 @@ end
|
|
|
361
503
|
|
|
362
504
|
| Variable | Attribute | Default |
|
|
363
505
|
| --- | --- | --- |
|
|
364
|
-
| `CYBERTRAIN_ENV` | `env` | `"development"` (also `"test"
|
|
506
|
+
| `CYBERTRAIN_ENV` | `env` | `"development"`; `"production"` in a binary from `cybertrain build` (also `"test"`) |
|
|
365
507
|
| `PORT` | `port` | `3000` |
|
|
366
508
|
| `CYBERTRAIN_DATABASE` | `database_path` | `storage/#{env}.sqlite3` |
|
|
367
509
|
| `CYBERTRAIN_SECRET_KEY_BASE` | `secret_key_base` | required in production; dev/test auto-generate one into `tmp/secret_key` |
|
|
@@ -374,10 +516,12 @@ Other attributes with fixed, overridable defaults: `host` (`"127.0.0.1"`),
|
|
|
374
516
|
marks the session cookie `Secure`; `false` elsewhere), `pool_size` (4),
|
|
375
517
|
`static_files`/`csrf` (`true`).
|
|
376
518
|
|
|
377
|
-
**Deployment:** `
|
|
378
|
-
`
|
|
379
|
-
|
|
380
|
-
`
|
|
519
|
+
**Deployment:** `cybertrain build` produces `dist/`: the binary `dist/NAME`
|
|
520
|
+
with the views embedded, `dist/public/` (static assets, or let a reverse
|
|
521
|
+
proxy serve it), and `dist/storage/` (the SQLite file by default) and
|
|
522
|
+
`dist/tmp/`. Run it from `dist/` with `CYBERTRAIN_SECRET_KEY_BASE` set;
|
|
523
|
+
it is production unless `CYBERTRAIN_ENV` says otherwise, and `./NAME
|
|
524
|
+
migrate` applies the migrations compiled into it. The binary
|
|
381
525
|
speaks plain HTTP/1.1 only; put nginx/Caddy in front for TLS and HTTP/2 (the
|
|
382
526
|
session cookie is `Secure` in production, so serve it over HTTPS). In
|
|
383
527
|
production an exception answers 500 and a bare error response (the router's
|
|
@@ -395,7 +539,7 @@ systemd, Caddy with HTTPS, redeploys, rollbacks and backups.
|
|
|
395
539
|
App tests are plain Spinel programs under `test/`, like the framework's own:
|
|
396
540
|
|
|
397
541
|
```ruby
|
|
398
|
-
|
|
542
|
+
require_relative "support/blog_test" # the app, BLOG and Cybertrain::Test
|
|
399
543
|
|
|
400
544
|
test "title must be present" do
|
|
401
545
|
article = Article.new(body: "a body long enough to pass length")
|
|
@@ -410,29 +554,47 @@ full HTTP flows, `Cybertrain::Test::Client` drives an app in-process (no
|
|
|
410
554
|
socket) with a cookie jar, so sessions and CSRF behave as behind a browser:
|
|
411
555
|
|
|
412
556
|
```ruby
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
client.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
557
|
+
require_relative "support/blog_test" # the app, BLOG and Cybertrain::Test
|
|
558
|
+
|
|
559
|
+
test "POST /articles creates an article and redirects to it" do
|
|
560
|
+
client = Cybertrain::Test::Client.new(BLOG)
|
|
561
|
+
client.get("/articles/new")
|
|
562
|
+
res = client.post("/articles", { "authenticity_token" => BlogTest.form_token(client),
|
|
563
|
+
"article[title]" => "Hello Rails",
|
|
564
|
+
"article[body]" => "I am on Rails! This is my first article." })
|
|
565
|
+
assert_redirected_to res, BlogTest.article_path(Article.last)
|
|
566
|
+
assert_response res, :see_other
|
|
567
|
+
end
|
|
422
568
|
```
|
|
423
569
|
|
|
424
|
-
|
|
425
|
-
`test/support/blog_test.rb`)
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
570
|
+
Both snippets start from the example's helper,
|
|
571
|
+
[`examples/blog/test/support/blog_test.rb`](examples/blog/test/support/blog_test.rb):
|
|
572
|
+
copy it into an app's `test/support/`, where files are required, not run.
|
|
573
|
+
It migrates a fresh `storage/test.sqlite3`, boots the app as `BLOG`, loads
|
|
574
|
+
`Cybertrain::Test` and its client, and reads the CSRF token out of the last
|
|
575
|
+
rendered form (`BlogTest.form_token`). End each test file with
|
|
576
|
+
`Cybertrain::Test.run!`; `examples/blog/test/articles.rb` and `comments.rb`
|
|
577
|
+
show the full pattern.
|
|
578
|
+
|
|
579
|
+
Run with `cybertrain spin test`: it compiles each `test/*.rb` into its own
|
|
580
|
+
program and diffs its output (stdout and stderr together) against
|
|
581
|
+
`test/<name>.rb.expected`. Regenerate with `cybertrain spin test --regen
|
|
582
|
+
test/<name>.rb`; FFI/database tests can't run under CRuby, so their snapshot
|
|
583
|
+
comes from the compiled binary instead: `rm -f build/test/<name>; cybertrain
|
|
584
|
+
spin test test/<name>.rb; [ -x build/test/<name> ] && ./build/test/<name> >
|
|
585
|
+
test/<name>.rb.expected 2>&1` (`cybertrain spin test` exits non-zero while the
|
|
586
|
+
snapshot is stale, so it is followed by `;`, not `&&`; the `[ -x ]` check skips
|
|
587
|
+
the capture when the compile failed, so the old snapshot is kept; in the
|
|
588
|
+
framework checkout, `script/regen-snapshot test/<name>.rb` does exactly this and
|
|
589
|
+
prints the diff stat).
|
|
433
590
|
|
|
434
591
|
## Learn more
|
|
435
592
|
|
|
593
|
+
- [Homepage](https://saeki-mototsune.github.io/CyberTrain/) and
|
|
594
|
+
[tutorial](https://saeki-mototsune.github.io/CyberTrain/tutorial.html) —
|
|
595
|
+
the source lives in [site/](site/), published by
|
|
596
|
+
[.github/workflows/pages.yml](.github/workflows/pages.yml); the logo files and
|
|
597
|
+
brand notes are in [site/assets/brand/](site/assets/brand/BRAND.md).
|
|
436
598
|
- [docs/design.md](docs/design.md) — the design record (Japanese): every
|
|
437
599
|
decision, what was rejected and why, and the Spinel constraints behind it.
|
|
438
600
|
- [docs/template-language.md](docs/template-language.md) — the full template
|
|
@@ -453,8 +615,13 @@ before the gem is pushed:
|
|
|
453
615
|
1. Bump `Cybertrain::VERSION` (`cybertrain/version.rb`) and `version` in
|
|
454
616
|
`spin.toml` together (CI checks they match); spin caches a git
|
|
455
617
|
dependency by that version.
|
|
456
|
-
2. Merge to `main`, then tag and push: `git tag v0.
|
|
457
|
-
3. `gem build cybertrain.gemspec && gem push cybertrain-0.
|
|
618
|
+
2. Merge to `main`, then tag and push: `git tag v0.2.0 && git push origin v0.2.0`.
|
|
619
|
+
3. `gem build cybertrain.gemspec && gem push cybertrain-0.2.0.gem`.
|
|
620
|
+
|
|
621
|
+
`Cybertrain::SPINEL_TAG` (also in `cybertrain/version.rb`) is the Spinel
|
|
622
|
+
release `cybertrain setup` installs. It must equal `SPINEL_TAG` in
|
|
623
|
+
[.github/workflows/ci.yml](.github/workflows/ci.yml), which CI checks, so
|
|
624
|
+
change the two together when the framework moves to another Spinel release.
|
|
458
625
|
|
|
459
626
|
## License
|
|
460
627
|
|