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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7660171ff57ac0e3f625dbffdef0a5a81de709bb80d2bcaf4e2b55be40034c24
4
- data.tar.gz: 467b4243e1b26cfa42b64e276f6f859d01c1c7f9bd96df015e1222b66b31dd5d
3
+ metadata.gz: '0800072e567ad58afad9d1e6531aeff2aa0e82d8d9c69f2091fb6d3f5e0a5d61'
4
+ data.tar.gz: 9e93d0e6dc888ad648ab6acef7d78f52822b72f11aa7435dfd4aeae601334e8a
5
5
  SHA512:
6
- metadata.gz: f56d35ff51a73b15c54376c145197847760d0238cf6599c68559d1c82c84f27998d65b0e53fd594ed7951f7d1b9d75ed3da7895571b4746e3ccb3b58e11bb7d1
7
- data.tar.gz: 49a0e1927cd913289307e6684065bf157a1c04c22a585bad2e38cd2826f523cf480aa8a2cea19cd2437fb21dd249dd21a4a3ad97e6c43e231a580f166c202aa6
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`, with `spinel` and `spin` on `PATH` (see `SPINEL_TAG`
29
- in [.github/workflows/ci.yml](.github/workflows/ci.yml)); untested against
30
- other versions.
31
- - A C toolchain (`cc`) — Spinel compiles every program to C.
32
- - Ruby 3.2+ and RubyGems, for the `cybertrain` command only (applications
33
- never run on CRuby).
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. Building Spinel itself also needs OpenSSL's headers
37
- (`libssl-dev`); cybertrain itself does not link OpenSSL.
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 --regen
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 itself is not in the gem. Its commands
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` creates an application in NAME, then runs `spin
65
- lock` and `spin run gen` in it.
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 migration` generates, applies pending migrations, and
69
- generates again (`spin run gen; spin run db -- migrate; spin run gen`).
70
- - `cybertrain db COMMAND...` runs any database command: `status`,
71
- `rollback [N]`, `schema:dump`, `create`.
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
- `migration`, `db`, `server` and `build` run inside an application; NAME is
79
- the `[package] name` in its `spin.toml`.
173
+ ```sh
174
+ export PATH="$HOME/.cybertrain/bin:$PATH"
175
+ ```
80
176
 
81
- `cybertrain new` points the app's `spin.toml` at the release matching the
82
- CLI — `cybertrain = { git = "https://github.com/saeki-mototsune/cybertrain",
83
- ref = "v0.1.1" }` — then runs `spin lock` (spin fetches the framework into
84
- its cache, `~/.cache/spin/packages/`, and pins the commit in `spin.lock`)
85
- and `spin run gen`, much as `rails new` runs `bundle install`. Nothing of
86
- the framework is copied into the app; commit `spin.toml` and `spin.lock`.
87
- `--skip-spin` leaves both steps for later, `--path DIR` depends on a local
88
- checkout instead, and `--version V` on an index version once cybertrain is
89
- on a `spin-index`.
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 migration`
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` and run `spin run gen`.
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 migration # spin run gen; spin run db -- migrate; spin run gen
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 schema, routes,
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 sends the server `SIGHUP`, whose handler stops listening
385
- and `execv`s the new binary on the same port and PID, invisible to a client
386
- mid-session. A failed build keeps serving the old binary and banners the
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, `SIGHUP`, `execv`) |
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
- Other attributes with fixed, overridable defaults: `host` (`"127.0.0.1"`),
432
- `views_root` (`"app/views"`), `public_root` (`"public"`), `layout`
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), `pool_size` (4),
436
- `static_files`/`csrf` (`true`).
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
- require "cybertrain/test"
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
- require "cybertrain/test/client"
477
-
478
- client = Cybertrain::Test::Client.new(BLOG)
479
- client.get("/articles/new")
480
- res = client.post("/articles", "article[title]" => "Hello",
481
- "article[body]" => "I am on Rails!",
482
- "authenticity_token" => token) # from the rendered form
483
- assert_redirected_to res, "/articles/1"
484
- assert_response res, :see_other
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
- `examples/blog/test/articles.rb`/`comments.rb` (setup in
488
- `test/support/blog_test.rb`) show the full pattern, including token
489
- extraction, against a freshly migrated `storage/test.sqlite3`.
490
-
491
- Run with `spin test`: it compiles each `test/*.rb` into its own program and
492
- diffs stdout against `test/<name>.rb.expected`. Regenerate with `spin test
493
- --regen test/<name>.rb`; FFI/database tests can't run under CRuby, so their
494
- snapshot comes from the compiled binary instead: `spin test test/<name>.rb &&
495
- ./build/test/<name> > test/<name>.rb.expected`.
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.1.1 && git push origin v0.1.1`.
520
- 3. `gem build cybertrain.gemspec && gem push cybertrain-0.1.1.gem`.
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