cybertrain 0.1.1 → 0.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.
- checksums.yaml +4 -4
- data/README.md +233 -67
- data/cybertrain/app_name.rb +36 -0
- data/cybertrain/cli/build.rb +93 -16
- data/cybertrain/cli/new_app.rb +22 -12
- data/cybertrain/cli/scaffold.rb +78 -11
- data/cybertrain/cli/templates.rb +16 -20
- data/cybertrain/cli/toolchain.rb +787 -0
- data/cybertrain/cli.rb +114 -25
- data/cybertrain/generator/inflector.rb +154 -88
- data/cybertrain/ident.rb +171 -0
- data/cybertrain/version.rb +5 -1
- metadata +8 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0800072e567ad58afad9d1e6531aeff2aa0e82d8d9c69f2091fb6d3f5e0a5d61'
|
|
4
|
+
data.tar.gz: 9e93d0e6dc888ad648ab6acef7d78f52822b72f11aa7435dfd4aeae601334e8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6df2e10e9e78787c2328aa2d96116d44bf437a3de08bd312b513129f8321043503df2fae5afc1b8b14e332e0c44d7990765d846e7bdfd04a8f503490f0066a3a
|
|
7
|
+
data.tar.gz: eb2b9396571a00406590c504e96c330937570609f0d8d860885f9cf739ab13e6c90a4859d5266d77f847fc2a7ac25078779721046237da3400e0ecde8a24e6e6
|
data/README.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
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
|
+
<a href="https://saeki-mototsune.github.io/CyberTrain/api/">API reference</a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
1
13
|
# cybertrain
|
|
2
14
|
|
|
3
15
|
cybertrain is a Rails-shaped web application framework written natively for
|
|
@@ -23,30 +35,74 @@ authentication, mailers, jobs, WebSockets/ActionCable, an asset pipeline,
|
|
|
23
35
|
i18n, and anything beyond a minimal `render json:` — see "Differences from
|
|
24
36
|
Rails" below.
|
|
25
37
|
|
|
38
|
+
## Try it in the browser
|
|
39
|
+
|
|
40
|
+
[Open the playground in GitHub Codespaces](https://codespaces.new/saeki-mototsune/CyberTrain?quickstart=1)
|
|
41
|
+
to try cybertrain without installing anything: VS Code opens in your browser with
|
|
42
|
+
Spinel 2026.09.12, the `cybertrain` CLI and the blog from the walkthrough below
|
|
43
|
+
already created (`cybertrain new blog`, the article scaffold, the root route and
|
|
44
|
+
the first migration, built once so the server starts without compiling), its
|
|
45
|
+
development server running in a terminal and the app, showing the article list,
|
|
46
|
+
in the editor's preview. The app is a git repository with one commit, so Source
|
|
47
|
+
Control shows what you change. `PLAYGROUND.md` in the app lists what to try, from
|
|
48
|
+
editing a view to carrying on with the walkthrough's comments. A view edit shows
|
|
49
|
+
on the next reload; a Ruby edit is a full rebuild, about a minute, after which the
|
|
50
|
+
server restarts by itself; the preview does not reload by itself. `cybertrain new`
|
|
51
|
+
works there with no network.
|
|
52
|
+
|
|
53
|
+
- A GitHub account is required, and the codespace runs on your own Codespaces
|
|
54
|
+
quota: GitHub's free plan includes 120 core-hours and 15 GB-month of storage a
|
|
55
|
+
month, about 60 hours on the default 2-core machine the playground uses. GitHub
|
|
56
|
+
stops an idle codespace after 30 minutes by default; its storage counts until
|
|
57
|
+
you delete it.
|
|
58
|
+
- Inside the preview, pop-ups and `confirm()` dialogs do not work; the Ports
|
|
59
|
+
view's "Open in Browser" shows the app in a normal tab.
|
|
60
|
+
- The same image runs locally (it is published for linux/amd64; on arm64, build
|
|
61
|
+
it from a checkout):
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
docker run --rm -it --init -p 127.0.0.1:3000:3000 -e CYBERTRAIN_HOST=0.0.0.0 ghcr.io/saeki-mototsune/cybertrain-playground
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then open http://localhost:3000. [playground/README.md](playground/README.md)
|
|
68
|
+
describes the image, building it and the Codespaces setup.
|
|
69
|
+
|
|
26
70
|
## Requirements
|
|
27
71
|
|
|
28
|
-
- Spinel `2026.09.12
|
|
29
|
-
in [.github/workflows/ci.yml](.github/workflows/ci.yml));
|
|
30
|
-
other versions.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
72
|
+
- Spinel `2026.09.12` (`Cybertrain::SPINEL_TAG`, the same release as
|
|
73
|
+
`SPINEL_TAG` in [.github/workflows/ci.yml](.github/workflows/ci.yml));
|
|
74
|
+
untested against other versions. You do not install it by hand: the
|
|
75
|
+
`cybertrain` command builds that release into `~/.cybertrain` the first
|
|
76
|
+
time it needs it (`cybertrain setup` does that on request), unless a
|
|
77
|
+
`spinel` of that release is already on `PATH` — see "Installing the CLI".
|
|
78
|
+
- To build it: a C toolchain (`cc`, or the compiler `CC` names) and `make`
|
|
79
|
+
— Spinel compiles every program to C — plus `git` and `curl`.
|
|
34
80
|
- SQLite 3 headers/library (`libsqlite3-dev` on Debian/Ubuntu; present with
|
|
35
81
|
Xcode's command line tools on macOS) — models use Spinel's FFI directly,
|
|
36
|
-
not a gem.
|
|
37
|
-
|
|
82
|
+
not a gem.
|
|
83
|
+
- Ruby 3.2+ and RubyGems, for the `cybertrain` command only (applications
|
|
84
|
+
never run on CRuby).
|
|
85
|
+
- OpenSSL's headers (`libssl-dev`) are optional: without them Spinel builds
|
|
86
|
+
without its `openssl` package, which cybertrain does not use.
|
|
87
|
+
|
|
88
|
+
`cybertrain doctor` checks these tools and headers and the Spinel install;
|
|
89
|
+
when a required one is missing it also prints the install command for your
|
|
90
|
+
platform.
|
|
38
91
|
|
|
39
|
-
The framework has no separate test runner; it tests itself
|
|
92
|
+
The framework has no separate test runner; it tests itself. `spin` comes
|
|
93
|
+
with Spinel, and `cybertrain spin` runs the copy `cybertrain` installs
|
|
94
|
+
without touching `PATH` (or put that copy on `PATH` with the `export
|
|
95
|
+
PATH=...` line `cybertrain setup` prints, and call `spin` directly):
|
|
40
96
|
|
|
41
97
|
```sh
|
|
42
|
-
spin test
|
|
98
|
+
cybertrain spin test
|
|
43
99
|
```
|
|
44
100
|
|
|
45
101
|
compiles and runs every program under `test/` and diffs its output against a
|
|
46
102
|
committed `test/<name>.rb.expected` snapshot — minitest/RSpec can't run under
|
|
47
103
|
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
|
|
104
|
+
compiler has nothing to reflect on at run time. `cybertrain spin test
|
|
105
|
+
--regen test/<name>.rb` rewrites a snapshot from CRuby's output; a few tests that
|
|
50
106
|
touch SQLite through FFI take theirs from the compiled binary instead
|
|
51
107
|
([spikes/NOTES.md](spikes/NOTES.md), rule 23); `script/regen-snapshot
|
|
52
108
|
test/<name>.rb` does that build-then-capture step for you and prints the diff stat.
|
|
@@ -55,38 +111,93 @@ test/<name>.rb` does that build-then-capture step for you and prints the diff st
|
|
|
55
111
|
|
|
56
112
|
```sh
|
|
57
113
|
gem install cybertrain
|
|
114
|
+
cybertrain new blog # also installs Spinel, the first time (a few minutes)
|
|
115
|
+
cd blog
|
|
116
|
+
cybertrain g scaffold article title:string body:text
|
|
117
|
+
cybertrain db migrate
|
|
118
|
+
cybertrain server # http://127.0.0.1:3000
|
|
119
|
+
cybertrain build # production: dist/
|
|
58
120
|
```
|
|
59
121
|
|
|
60
122
|
The `cybertrain` command is plain Ruby and the gem runs it under CRuby
|
|
61
|
-
(3.2 or newer); the framework
|
|
62
|
-
(`cybertrain help` prints them):
|
|
123
|
+
(3.2 or newer); neither the framework nor Spinel is in the gem. Its
|
|
124
|
+
commands (`cybertrain help` prints them):
|
|
63
125
|
|
|
64
|
-
- `cybertrain new NAME
|
|
65
|
-
|
|
126
|
+
- `cybertrain new NAME [--path DIR | --version V | --git URL [--ref R]]
|
|
127
|
+
[--skip-spin]` creates an application in NAME, then runs `spin lock` and
|
|
128
|
+
`spin run gen` in it, installing Spinel first when it is needed.
|
|
66
129
|
- `cybertrain generate scaffold NAME field:type ...` (alias `g scaffold`)
|
|
67
130
|
adds a resource to the application in the current directory.
|
|
68
|
-
- `cybertrain
|
|
69
|
-
generates again (`spin run gen; spin run db -- migrate; spin run gen`)
|
|
70
|
-
|
|
71
|
-
|
|
131
|
+
- `cybertrain db migrate` generates, applies pending migrations, and
|
|
132
|
+
generates again (`spin run gen; spin run db -- migrate; spin run gen`);
|
|
133
|
+
`cybertrain migration` is kept as an alias.
|
|
134
|
+
- `cybertrain db COMMAND...` runs any other database command after `spin
|
|
135
|
+
run gen`: `status`, `rollback [N]` (which generates again afterwards),
|
|
136
|
+
`schema:dump`, `create`.
|
|
72
137
|
- `cybertrain server [PORT]` generates, then starts the development server
|
|
73
138
|
(`spin run gen; spin run NAME`, or `spin run NAME -- PORT`; default port
|
|
74
139
|
3000).
|
|
75
140
|
- `cybertrain build` builds NAME with `app/views/` embedded and assembles
|
|
76
141
|
`dist/` (the binary, `public/`, `storage/`, `tmp/`).
|
|
142
|
+
- `cybertrain spin ARGS...` runs `spin` with the pinned Spinel from any
|
|
143
|
+
directory (`cybertrain spin test`, `cybertrain spin run gen -- --check`).
|
|
144
|
+
- `cybertrain setup [--force]` installs Spinel `2026.09.12` into
|
|
145
|
+
`~/.cybertrain` unless a `spinel` of that release is already on `PATH`;
|
|
146
|
+
`--force` rebuilds the copy under `~/.cybertrain`.
|
|
147
|
+
- `cybertrain doctor` checks the C toolchain, the SQLite headers and the
|
|
148
|
+
Spinel install.
|
|
149
|
+
- `cybertrain version` prints the version.
|
|
150
|
+
|
|
151
|
+
`db`, `server` and `build` run inside an application; NAME is the
|
|
152
|
+
`[package] name` in its `spin.toml`.
|
|
153
|
+
|
|
154
|
+
Every command that runs `spin` (`new`, `db`, `server`, `build`, `spin`) first makes
|
|
155
|
+
sure a Spinel of the release in `Cybertrain::SPINEL_TAG` is available. A
|
|
156
|
+
`spinel` on `PATH` (with a `spin`) whose `spinel --version` reports that
|
|
157
|
+
release is used as it is; otherwise the copy `cybertrain` keeps under
|
|
158
|
+
`~/.cybertrain/spinel/2026.09.12/` is used; otherwise `cybertrain` builds
|
|
159
|
+
that copy from the release tag (`git clone`, `make deps`, `make -j`, `make
|
|
160
|
+
install PREFIX=...`), once, in a few minutes. The build log is
|
|
161
|
+
`~/.cybertrain/log/spinel-2026.09.12-build.log` (its last lines are printed
|
|
162
|
+
if a step fails); if a tool the build needs is missing, `cybertrain` says
|
|
163
|
+
which and prints the install command for your platform before it starts. A
|
|
164
|
+
`spinel` of another release on `PATH` is never used or touched:
|
|
165
|
+
`cybertrain` keeps its own copy (and says so when it installs it).
|
|
166
|
+
|
|
167
|
+
`cybertrain setup` does the same on request and, whenever its own copy is
|
|
168
|
+
the one in use, prints the line that puts it on `PATH`, for running `spin`
|
|
169
|
+
yourself (`~/.cybertrain/bin` holds `spinel` and `spin` links to the release
|
|
170
|
+
in use, so the line survives a release bump; `cybertrain spin ...` needs no
|
|
171
|
+
`PATH` at all):
|
|
77
172
|
|
|
78
|
-
|
|
79
|
-
|
|
173
|
+
```sh
|
|
174
|
+
export PATH="$HOME/.cybertrain/bin:$PATH"
|
|
175
|
+
```
|
|
80
176
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
177
|
+
Two environment variables override all this: `CYBERTRAIN_HOME` replaces
|
|
178
|
+
`~/.cybertrain` (a path without spaces, quotes, `$` or `:`, which Spinel's
|
|
179
|
+
Makefile and `PATH` cannot take), and `CYBERTRAIN_SPINEL_HOME=PREFIX` forces
|
|
180
|
+
an existing install — `cybertrain` then uses the `spinel` and `spin` in `PREFIX/bin` (or
|
|
181
|
+
in `PREFIX` itself, if `spinel` is directly in it) and stops with an error
|
|
182
|
+
if they are not that release.
|
|
183
|
+
|
|
184
|
+
`cybertrain new` writes the application with its `spin.toml` pointing at
|
|
185
|
+
the release matching the CLI (`cybertrain = { git =
|
|
186
|
+
"https://github.com/saeki-mototsune/cybertrain", ref = "v0.2.1" }`), makes
|
|
187
|
+
sure Spinel is installed, then runs `spin lock` (spin fetches the framework
|
|
188
|
+
into its cache, `~/.cache/spin/packages/`, and pins the commit in
|
|
189
|
+
`spin.lock`) and `spin run gen`, much as `rails new` runs `bundle install`,
|
|
190
|
+
and prints the next steps. Nothing of the framework is copied into the app;
|
|
191
|
+
commit `spin.toml` and `spin.lock`. If Spinel or one of the two `spin`
|
|
192
|
+
steps fails, the files stay, `new` exits with 1 and prints what to run once
|
|
193
|
+
the cause is fixed (for Spinel: `cybertrain setup`, then `cybertrain spin
|
|
194
|
+
lock && cybertrain spin run gen` in the new directory).
|
|
195
|
+
|
|
196
|
+
`--skip-spin` only writes the files: no Spinel install, no `spin lock`, no
|
|
197
|
+
`spin run gen`. `--path DIR` depends on a local checkout instead,
|
|
198
|
+
`--git URL [--ref R]` on the framework at URL (at branch or tag R), and
|
|
199
|
+
`--version V` on an index version once cybertrain is on a `spin-index`.
|
|
200
|
+
Give only one of `--path`, `--version` and `--git`; `--ref` needs `--git`.
|
|
90
201
|
|
|
91
202
|
Working on cybertrain itself, run the CLI from the checkout (`spin install`
|
|
92
203
|
builds `bin/cybertrain.rb` into `~/.local/bin/cybertrain`; `ruby -I.
|
|
@@ -119,14 +230,15 @@ blog/
|
|
|
119
230
|
`bin/blog.rb` is the application's one entry point, the program `spin build
|
|
120
231
|
blog` compiles: `./blog` serves, `./blog migrate` and `./blog db status`
|
|
121
232
|
manage the database. `bin/gen.rb` runs the generator (`spin run gen`), and
|
|
122
|
-
`bin/db.rb` runs the migrator during development (`cybertrain
|
|
233
|
+
`bin/db.rb` runs the migrator during development (`cybertrain db migrate`
|
|
123
234
|
uses it, since `bin/blog.rb` cannot compile until the first migration has
|
|
124
235
|
generated `gen/models/`). `gen/` already holds `views.rb`, the committed,
|
|
125
236
|
empty table of embedded views (see "How it works").
|
|
126
237
|
|
|
127
238
|
Apps created before `cybertrain build` existed have `bin/server.rb` and
|
|
128
239
|
no `bin/<name>.rb`: copy `bin/<name>.rb` from a fresh `cybertrain new` (with
|
|
129
|
-
the same NAME), delete `bin/server.rb
|
|
240
|
+
the same NAME), delete `bin/server.rb`, add `/dist/` to `.gitignore` (the old
|
|
241
|
+
template did not ignore it) and run `cybertrain spin run gen`.
|
|
130
242
|
|
|
131
243
|
`ApplicationController` starts with the one thing every controller inherits:
|
|
132
244
|
|
|
@@ -210,7 +322,7 @@ end
|
|
|
210
322
|
### Generate, migrate, run
|
|
211
323
|
|
|
212
324
|
```sh
|
|
213
|
-
cybertrain
|
|
325
|
+
cybertrain db migrate # spin run gen; spin run db -- migrate; spin run gen
|
|
214
326
|
cybertrain server # http://127.0.0.1:3000; rebuilds on Ruby edits, views reload without a rebuild
|
|
215
327
|
```
|
|
216
328
|
|
|
@@ -218,14 +330,15 @@ cybertrain server # http://127.0.0.1:3000; rebuilds on Ruby edits, views
|
|
|
218
330
|
writes `gen/`; `spin run db -- migrate` applies `db/migrate/*.rb` and
|
|
219
331
|
rewrites `db/schema.rb`.
|
|
220
332
|
|
|
221
|
-
Re-run `spin run gen` (and commit `gen/`) after touching the
|
|
222
|
-
or a controller/model's callbacks and ivars — the dev server does this for
|
|
333
|
+
Re-run `cybertrain spin run gen` (and commit `gen/`) after touching the
|
|
334
|
+
schema, routes, or a controller/model's callbacks and ivars — the dev server does this for
|
|
223
335
|
you automatically (see "How it works").
|
|
224
336
|
|
|
225
337
|
### Add comments, nested under articles
|
|
226
338
|
|
|
227
339
|
```sh
|
|
228
340
|
cybertrain generate scaffold comment commenter:string body:text article:references
|
|
341
|
+
cybertrain db migrate # the scaffold wrote a migration for the comments table
|
|
229
342
|
```
|
|
230
343
|
|
|
231
344
|
`article:references` adds an `article_id` column, index and foreign key —
|
|
@@ -352,8 +465,8 @@ merge.
|
|
|
352
465
|
**Views are interpreted, not compiled.** `app/views/**/*.html.erb` files look
|
|
353
466
|
like Rails ERB but are parsed into an AST at request time, then walked by a
|
|
354
467
|
tree-walking interpreter. Where the source comes from depends on the mode:
|
|
355
|
-
in development (and test) the engine reads `app/views/` from disk and
|
|
356
|
-
re-parses a template when it changes, so view edits need no rebuild.
|
|
468
|
+
in development (and test) the engine reads `app/views/` from disk, and in
|
|
469
|
+
development re-parses a template when it changes, so view edits need no rebuild.
|
|
357
470
|
`cybertrain build` runs `spin run gen -- --embed-views`, which writes every
|
|
358
471
|
template's source into `gen/views.rb` as a string table for that one `spin
|
|
359
472
|
build`, then runs `spin run gen` again to restore the empty table that is
|
|
@@ -381,9 +494,10 @@ adapter today (PostgreSQL via `libpq` FFI is noted as possible future work).
|
|
|
381
494
|
`cybertrain build`) also polls `app/**/*.rb`, `config/**/*.rb`, `db/schema.rb`
|
|
382
495
|
and `gen/**/*.rb` every half second (views are excluded — the engine reloads
|
|
383
496
|
those itself — and so is `gen/views.rb`, empty in development). A change runs `spin run gen && spin build NAME` in the
|
|
384
|
-
background; success
|
|
385
|
-
|
|
386
|
-
|
|
497
|
+
background; success requests a restart (the same flag an external `kill -HUP`
|
|
498
|
+
sets): a monitor thread stops the listener and drains open requests, then the
|
|
499
|
+
main thread `execv`s the new binary on the same port and PID, invisible to a
|
|
500
|
+
client mid-session. A failed build keeps serving the old binary and banners the
|
|
387
501
|
compiler output on every HTML response. None of this loads in production.
|
|
388
502
|
|
|
389
503
|
## Differences from Rails
|
|
@@ -391,7 +505,7 @@ compiler output on every HTML response. None of this loads in production.
|
|
|
391
505
|
| Rails | cybertrain |
|
|
392
506
|
| --- | --- |
|
|
393
507
|
| `rails console` | No console — Spinel has no `eval` |
|
|
394
|
-
| Edit code, the running app picks it up | Ruby needs a rebuild; `cybertrain server` in development does this for you (rebuild,
|
|
508
|
+
| Edit code, the running app picks it up | Ruby needs a rebuild; `cybertrain server` in development does this for you (rebuild, stop, `execv`) |
|
|
395
509
|
| Edit a view, no reload needed | Same — views are parsed from disk per request in development |
|
|
396
510
|
| `def new` | `def new_action` (`new` would shadow `Klass.new(ctx)`) |
|
|
397
511
|
| `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 |
|
|
@@ -400,7 +514,10 @@ compiler output on every HTML response. None of this loads in production.
|
|
|
400
514
|
| Session holds any marshalled object | Session values are Strings only, HMAC-signed cookie |
|
|
401
515
|
| Many database adapters | SQLite only, via FFI |
|
|
402
516
|
| `has_many :through`, `includes`, `pluck`, `dependent:`, enums, STI, polymorphic associations | Not implemented; associations come from schema foreign keys only |
|
|
517
|
+
| Any column name; an association can shadow `errors` | A column name must be an ASCII identifier (`[A-Za-z_][A-Za-z0-9_]*`): a schema with a non-ASCII column name (`名前`, `prénom`) generated under CRuby before PR #10 and `spin run gen` refuses it now, so rename the column when upgrading (a name like `first-name` was never a method name). A column named like a method the generated class or the framework calls on a record (`errors`, `save`, `attributes`, `hash`, `to_s`, `to_ary`, `raise`, ...) or like a Ruby keyword (`end`, `class`, `begin`) generates under `<column>_column` (`hash_column`, reader, writer and ivar) with a note in the generated file; its SQL name keeps working in `read_attribute`, params and `attributes["hash"]`, and a template reaches it under either name (`post.hash`, `post.hash_column`), except `errors` and `to_param`, which a template resolves as the model methods, so use `post.errors_column` there; the query API (`where`, `order`, `find_by`) always takes the SQL column name (`Post.where(hash: 1)`), the Ruby name being the reader/writer and the template name only. A column named like another Object method (`display`, `tap`, `methods`) generates under its own name with a note. `cybertrain generate scaffold` refuses keywords and reserved names (it would have to call a renamed reader) and accepts a shadowing name with a note, like the generator. An association whose plain name a column, another association or a `Model` method already owns is emitted under a fallback name with a comment in the generated file: `<table>_as_<column stem>` for a `has_many` (`comments_as_article`), `<stem>_as_<column>` for a `belongs_to` (`author_as_author_id`) |
|
|
403
518
|
| `namespace`, format/`respond_to`, `constraints`, `mount` | Not implemented — flat names, `render json:` only |
|
|
519
|
+
| `rescue StandardError` catches a bad `JSON.parse` | Under Spinel `JSON::ParserError` is not a `StandardError`: app code must `rescue JSON::ParserError, StandardError`. The server catches it as a last resort and answers 500 |
|
|
520
|
+
| `order("lower(title)")`, `order("posts.title")`, `order(params[:sort])` | `order` takes only `column [ASC\|DESC]` lists (each column quoted) and raises `ArgumentError` (a 500: the string is the developer's) on anything else; request data must go through an allowlist first, e.g. `SORTS = { "title" => "title", "newest" => "created_at DESC" }` and `Post.order(SORTS.fetch(params[:sort].to_s, "id"))`, because a well-formed term can still name a missing column (a 500 from SQLite) or a column the client must not sort by; raw ORDER BY text goes through `order_sql` (`Post.order_sql("lower(title)")`, also on a relation), which must never see request data. A `limit`/`offset` on `delete_all` is honoured (a subselect), and request parameters nest at most 32 levels / 4096 pairs (400 past that). `Query.parse`, `Query.value_of`, `Query.decode` and `request.form_value` / `query_value` raise `Cybertrain::QueryMalformed` (a `StandardError` under `Cybertrain::QueryInvalid`, not an `ArgumentError`) on a malformed percent-escape, on both runtimes (`Cookies.parse` still keeps the raw value). They raise it too on an invalid UTF-8 byte sequence, raw (whatever the String's tag) or percent-encoded (Rails likewise answers 400 for an invalid parameter encoding), and so does a raw byte that only an escape would complete (`a=\xC3%A9`); `Cookies.parse` keeps such a value raw, and a path with an invalid sequence (raw, or percent-decoded in a segment) is a 400 as well (`request.path_segments`; a malformed escape in a path stays literal, a 404 by non-match). App code that rescued `ArgumentError` around them should rescue `Cybertrain::QueryInvalid` instead (upgrade note). The request keeps no parameter tree of its own (the Router builds `params`, the middleware read one key); an app that wants the whole tree parses it itself, `Query.parse(request.utf8_query_string)`, and owns the result |
|
|
404
521
|
| Full backtrace on an exception | Class, message, request line, template name/line — Spinel exposes no backtraces |
|
|
405
522
|
| Rack, its middleware, and any gem in a `Gemfile` | No Rack compatibility; a small fixed middleware set; Spinel's own `spin-index`, limited to what compiles under its Ruby subset |
|
|
406
523
|
| minitest / RSpec | `Cybertrain::Test` — reflection-based runners can't work ahead-of-time |
|
|
@@ -427,13 +544,27 @@ end
|
|
|
427
544
|
| `CYBERTRAIN_DATABASE` | `database_path` | `storage/#{env}.sqlite3` |
|
|
428
545
|
| `CYBERTRAIN_SECRET_KEY_BASE` | `secret_key_base` | required in production; dev/test auto-generate one into `tmp/secret_key` |
|
|
429
546
|
| `SPINEL_WORKERS` | `workers` | `1` |
|
|
547
|
+
| `CYBERTRAIN_HOST` | `host` | `"127.0.0.1"`; `0.0.0.0` listens on every interface (containers) |
|
|
548
|
+
| `CYBERTRAIN_SESSION_SAME_SITE` | `session_same_site` | `"Lax"`; also `Strict` or `None` (`None` always adds `Secure`); anything else stops the server at boot |
|
|
549
|
+
| `CYBERTRAIN_SESSION_PARTITIONED` | `session_partitioned` | `false`; `1` or `true` adds `Partitioned` (and `Secure`) |
|
|
430
550
|
|
|
431
|
-
|
|
432
|
-
|
|
551
|
+
Use `None` (with `CYBERTRAIN_SESSION_PARTITIONED=1`) only for an app shown
|
|
552
|
+
inside another site's frame, such as the playground's Codespaces preview; the
|
|
553
|
+
browser must reach the app over HTTPS.
|
|
554
|
+
|
|
555
|
+
Other attributes with fixed, overridable defaults: `views_root`
|
|
556
|
+
(`"app/views"`), `public_root` (`"public"`), `layout`
|
|
433
557
|
(`"layouts/application"`), `log_level` (`:info`), `session_cookie_name`,
|
|
434
558
|
`session_max_age` (2 weeks), `session_secure` (`true` in production, which
|
|
435
|
-
marks the session cookie `Secure`; `false` elsewhere
|
|
436
|
-
`
|
|
559
|
+
marks the session cookie `Secure`; `false` elsewhere, though `SameSite=None`
|
|
560
|
+
and `Partitioned` add `Secure` anyway), `pool_size` (4), `static_files`/`csrf`
|
|
561
|
+
(`true`), `max_render_depth` (12 renders open at once: the page and its
|
|
562
|
+
partials; raise it for partials that legitimately recurse deeper; it must be
|
|
563
|
+
at least 1, or boot fails). Upgrade note: render nesting, unlimited before, is
|
|
564
|
+
now capped at 12 by default (the page is depth 1, so partials can nest 11
|
|
565
|
+
levels; the layout renders after the page and does not nest), and an app with
|
|
566
|
+
a deeper tree (threaded comments, a category menu) gets a template error until
|
|
567
|
+
it sets `max_render_depth`.
|
|
437
568
|
|
|
438
569
|
**Deployment:** `cybertrain build` produces `dist/`: the binary `dist/NAME`
|
|
439
570
|
with the views embedded, `dist/public/` (static assets, or let a reverse
|
|
@@ -458,7 +589,7 @@ systemd, Caddy with HTTPS, redeploys, rollbacks and backups.
|
|
|
458
589
|
App tests are plain Spinel programs under `test/`, like the framework's own:
|
|
459
590
|
|
|
460
591
|
```ruby
|
|
461
|
-
|
|
592
|
+
require_relative "support/blog_test" # the app, BLOG and Cybertrain::Test
|
|
462
593
|
|
|
463
594
|
test "title must be present" do
|
|
464
595
|
article = Article.new(body: "a body long enough to pass length")
|
|
@@ -473,29 +604,55 @@ full HTTP flows, `Cybertrain::Test::Client` drives an app in-process (no
|
|
|
473
604
|
socket) with a cookie jar, so sessions and CSRF behave as behind a browser:
|
|
474
605
|
|
|
475
606
|
```ruby
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
client.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
607
|
+
require_relative "support/blog_test" # the app, BLOG and Cybertrain::Test
|
|
608
|
+
|
|
609
|
+
test "POST /articles creates an article and redirects to it" do
|
|
610
|
+
client = Cybertrain::Test::Client.new(BLOG)
|
|
611
|
+
client.get("/articles/new")
|
|
612
|
+
res = client.post("/articles", { "authenticity_token" => BlogTest.form_token(client),
|
|
613
|
+
"article[title]" => "Hello Rails",
|
|
614
|
+
"article[body]" => "I am on Rails! This is my first article." })
|
|
615
|
+
assert_redirected_to res, BlogTest.article_path(Article.last)
|
|
616
|
+
assert_response res, :see_other
|
|
617
|
+
end
|
|
485
618
|
```
|
|
486
619
|
|
|
487
|
-
|
|
488
|
-
`test/support/blog_test.rb`)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
620
|
+
Both snippets start from the example's helper,
|
|
621
|
+
[`examples/blog/test/support/blog_test.rb`](examples/blog/test/support/blog_test.rb):
|
|
622
|
+
copy it into an app's `test/support/`, where files are required, not run.
|
|
623
|
+
It migrates a fresh `storage/test.sqlite3`, boots the app as `BLOG`, loads
|
|
624
|
+
`Cybertrain::Test` and its client, and reads the CSRF token out of the last
|
|
625
|
+
rendered form (`BlogTest.form_token`). End each test file with
|
|
626
|
+
`Cybertrain::Test.run!`; `examples/blog/test/articles.rb` and `comments.rb`
|
|
627
|
+
show the full pattern.
|
|
628
|
+
|
|
629
|
+
Run with `cybertrain spin test`: it compiles each `test/*.rb` into its own
|
|
630
|
+
program and diffs its output (stdout and stderr together) against
|
|
631
|
+
`test/<name>.rb.expected`. Regenerate with `cybertrain spin test --regen
|
|
632
|
+
test/<name>.rb`; FFI/database tests can't run under CRuby, so their snapshot
|
|
633
|
+
comes from the compiled binary instead: `rm -f build/test/<name>; cybertrain
|
|
634
|
+
spin test test/<name>.rb; [ -x build/test/<name> ] && ./build/test/<name> >
|
|
635
|
+
test/<name>.rb.expected 2>&1` (`cybertrain spin test` exits non-zero while the
|
|
636
|
+
snapshot is stale, so it is followed by `;`, not `&&`; the `[ -x ]` check skips
|
|
637
|
+
the capture when the compile failed, so the old snapshot is kept; in the
|
|
638
|
+
framework checkout, `script/regen-snapshot test/<name>.rb` does exactly this and
|
|
639
|
+
prints the diff stat).
|
|
496
640
|
|
|
497
641
|
## Learn more
|
|
498
642
|
|
|
643
|
+
- [Homepage](https://saeki-mototsune.github.io/CyberTrain/),
|
|
644
|
+
[tutorial](https://saeki-mototsune.github.io/CyberTrain/tutorial.html) and
|
|
645
|
+
[playground](https://saeki-mototsune.github.io/CyberTrain/playground.html) —
|
|
646
|
+
the source lives in [site/](site/), published by
|
|
647
|
+
[.github/workflows/pages.yml](.github/workflows/pages.yml); the logo files and
|
|
648
|
+
brand notes are in [site/assets/brand/](site/assets/brand/BRAND.md).
|
|
649
|
+
- [API reference](https://saeki-mototsune.github.io/CyberTrain/api/) — every
|
|
650
|
+
class and method an application uses (controllers, params, session and
|
|
651
|
+
flash, models and relations, routes, migrations, configuration, tests),
|
|
652
|
+
including what `spin run gen` generates. It is built by YARD
|
|
653
|
+
(`script/api-docs`, into `site/api/`) from the doc comments marked
|
|
654
|
+
`@api public` in `cybertrain/` and from [docs/api/](docs/api/); a method
|
|
655
|
+
without that tag is internal and stays out. CI fails on any YARD warning.
|
|
499
656
|
- [docs/design.md](docs/design.md) — the design record (Japanese): every
|
|
500
657
|
decision, what was rejected and why, and the Spinel constraints behind it.
|
|
501
658
|
- [docs/template-language.md](docs/template-language.md) — the full template
|
|
@@ -516,8 +673,17 @@ before the gem is pushed:
|
|
|
516
673
|
1. Bump `Cybertrain::VERSION` (`cybertrain/version.rb`) and `version` in
|
|
517
674
|
`spin.toml` together (CI checks they match); spin caches a git
|
|
518
675
|
dependency by that version.
|
|
519
|
-
2. Merge to `main`, then tag and push: `git tag v0.
|
|
520
|
-
3. `gem build cybertrain.gemspec && gem push cybertrain-0.
|
|
676
|
+
2. Merge to `main`, then tag and push: `git tag v0.2.1 && git push origin v0.2.1`.
|
|
677
|
+
3. `gem build cybertrain.gemspec && gem push cybertrain-0.2.1.gem`.
|
|
678
|
+
4. The tag also publishes the playground image
|
|
679
|
+
`ghcr.io/saeki-mototsune/cybertrain-playground:X.Y.Z` (and `latest`) through
|
|
680
|
+
[.github/workflows/playground-image.yml](.github/workflows/playground-image.yml);
|
|
681
|
+
check that run.
|
|
682
|
+
|
|
683
|
+
`Cybertrain::SPINEL_TAG` (also in `cybertrain/version.rb`) is the Spinel
|
|
684
|
+
release `cybertrain setup` installs. It must equal `SPINEL_TAG` in
|
|
685
|
+
[.github/workflows/ci.yml](.github/workflows/ci.yml), which CI checks, so
|
|
686
|
+
change the two together when the framework moves to another Spinel release.
|
|
521
687
|
|
|
522
688
|
## License
|
|
523
689
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Cybertrain
|
|
2
|
+
# The one definition of "a name usable as the application's package name":
|
|
3
|
+
# `cybertrain build`/`server` (CLI::Build.app_name, reading spin.toml) and
|
|
4
|
+
# the development rebuilder (Dev::Rebuilder.new, which shells out with the
|
|
5
|
+
# name Application.new(name:) was given) both ask here, so the rules
|
|
6
|
+
# cannot drift apart. Application.new itself does not: production never
|
|
7
|
+
# builds. A leaf file with no requires: the CLI (CRuby) and the framework
|
|
8
|
+
# (Spinel) both load it. String methods only, no Regexp, because it
|
|
9
|
+
# compiles under Spinel (see Dev::Rebuilder).
|
|
10
|
+
module AppName
|
|
11
|
+
# "" when name works as a `spin build` target and as the build/bin/<name>
|
|
12
|
+
# path, else the reason (a String on every path, NOTES rules 10/34).
|
|
13
|
+
# Shell safety is not this method's job: both callers quote the name when
|
|
14
|
+
# they build a command ("my-app" and "MyApp" are fine for `spin build`),
|
|
15
|
+
# so only what breaks the target or the path is refused.
|
|
16
|
+
def self.problem(name)
|
|
17
|
+
# An empty name would become an explicit "" argument to spin.
|
|
18
|
+
return "cannot be empty" if name.empty?
|
|
19
|
+
# "." and ".." name build/bin/. and build/bin/.., which exist already
|
|
20
|
+
# (the latter is build/) and only fail later, in assemble's cp.
|
|
21
|
+
return "cannot be '.' or '..'" if name == "." || name == ".."
|
|
22
|
+
# Quoting leaves a leading "-" bare ("-" is in quote_arg's safe set),
|
|
23
|
+
# and even quoted `spin build '-x'` is parsed by spin as an option, not
|
|
24
|
+
# as the package name. Whether spin accepts `--` before the target is
|
|
25
|
+
# unverified, hence a refusal.
|
|
26
|
+
return "cannot start with '-' (spin would read it as an option)" if name.start_with?("-")
|
|
27
|
+
# A "/" would put the binary in a subdirectory of build/bin.
|
|
28
|
+
return "cannot contain '/'" if name.include?("/")
|
|
29
|
+
# Whitespace splits the target in the places that do not quote it, and
|
|
30
|
+
# a newline ends a command line.
|
|
31
|
+
return "cannot contain whitespace" if name.include?(" ") || name.include?("\t") || name.include?("\n") || name.include?("\r")
|
|
32
|
+
|
|
33
|
+
""
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|