You are reading the docs for rws 18.30 The latest release is 18.34. Read this page for the latest release.

Generators (rws gen)

rws gen model, route, migration and test — code for a project in the conventional layout, written against the exact API of the rws release you run, never overwriting a file.

Since 18.29.0 rws gen writes the four things an app gets more of — a model, a resource's routes, a migration, a test — into a project in the conventional layout. What it writes is pinned to the API of the rws release you run (CI generates into every starter and runs the generated tests), and it never overwrites a file: running a generator twice changes nothing the second time, and a file that is in the way is a conflict, shown as a diff. There is no rws new — new projects start from a starter.

rws gen model Post title:String views:i64 published:bool 'note:Option<String>'
rws gen route posts            # the model's fields; or: rws gen route posts title:String
rws gen test posts
rws gen migration add_index_to_posts
cargo test

Use the rws binary of the release the project depends on (cargo install rust-web-server --version <x.y.z>, or any of the installation packages): its output names that release in each file's header and is written against its API.

The same generators are tools of the dev MCP endpoint (gen_model, gen_route, gen_migration, gen_test), and a library module, rust_web_server::generator.

What each generator writes

Command Creates Adds a line to
rws gen model <Name> field:type… src/models/<name>.rs, migrations/NNNN_create_<table>.sql and .down.sql src/models/mod.rs (pub mod post;, pub use post::Post;; created when missing), src/lib.rs (pub mod models;), Cargo.toml (the model-<db> feature)
rws gen route <resource> [field:type…] src/routes/<resource>.rs src/routes/mod.rs, app() in src/lib.rs (the routes are registered)
rws gen migration <name> migrations/NNNN_<name>.sql and .down.sql —
rws gen test <resource> tests/<resource>.rs —

The paths are those of rws.app.toml ([paths] models, routes, migrations, tests, lib), so a project that keeps its routes elsewhere gets them there. A line is added only where it can be placed safely — beside the other mod lines, inside the existing register(…) call of app(), in a one-line rust-web-server = { … } dependency; otherwise the plan ends with a note: saying what to add by hand.

Models

rws gen model Post title:String views:i64 published:bool 'note:Option<String>'
// src/models/post.rs
//! `Post`: the `posts` table (migrations/0001_create_posts.sql).
//!
//! Generated by `rws gen model Post title:String views:i64 published:bool note:Option<String>` (rws 18.29.0).

use rust_web_server::Model;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Model)]
#[table(name = "posts")]
pub struct Post {
    #[primary_key(auto_increment)]
    pub id: i64,
    pub title: String,
    pub views: i64,
    pub published: bool,
    pub note: Option<String>,
}

#[cfg(test)]
mod tests {
    // maps_to_the_posts_table: the table, columns and primary key;
    // round_trips_through_sqlite (SQLite only): the migrations applied to an
    // in-memory database, then save, find_by_id, delete_by_id.
}
  • The name may be Post, BlogPost or blog_post: the module is snake_case (blog_post.rs), the type CamelCase, the table the snake_case plural (blog_posts). Every model gets id: i64, auto-increment.
  • Field types are a fixed list: i64, i32, f64, bool, String, and Option<T> of those (a nullable column). Anything else is refused with that list.
  • The database is --db sqlite|postgres|mysql, else the model-* feature the project's Cargo.toml already enables, else SQLite; the feature is added when missing. The migration's column types follow it:
Rust SQLite PostgreSQL MySQL
id INTEGER PRIMARY KEY AUTOINCREMENT BIGSERIAL PRIMARY KEY BIGINT AUTO_INCREMENT PRIMARY KEY
i64 INTEGER BIGINT BIGINT
i32 INTEGER INTEGER INT
f64 REAL DOUBLE PRECISION DOUBLE
bool INTEGER BOOLEAN BOOLEAN
String TEXT TEXT TEXT

Columns are NOT NULL unless the field is an Option.

  • The migration is numbered after the highest NNNN_ in migrations/; the down file drops the table.
  • To list the model in the dev MCP endpoint's models tool, add .model::<models::Post>() to your DevTools (the plan's note says so).

Routes

rws gen route posts title:String views:i64 [--methods list,show,create,update,remove] [--prefix /api]

writes typed handlers — #[get("/posts")] list, #[get("/posts/{id}")] show, #[post("/posts")] create (201), #[put("/posts/{id}")] update, #[delete("/posts/{id}")] remove — a Post and a NewPost (the request body) type, and

/// Add these routes to the app.
pub fn register(app: AppWithState<crate::Store>) -> AppWithState<crate::Store> {
    app.route(list).route(show).route(create).route(update).route(remove)
}

The app type and its state come from pub fn app(…) -> AppWithState<S> (or AsyncAppWithState<S>) in src/lib.rs, and app()'s existing routes::…::register(…) call is wrapped: routes::posts::register(routes::todos::register(App::with_state(…))). A project whose lib.rs has no such app() — the mcp starter builds an McpServer — is refused.

Without fields, the routes take the fields of src/models/<singular>.rs when there is one, else name: String. The handlers keep the items in memory, in the module (static STORE): they work and are tested as they are; replace the store with the app's state or the model's repository when the data must outlast the process. Unknown ids answer 404, ids that aren't numbers 400, bodies that aren't JSON of the right shape 400 — problem details, as every rws error.

Migrations

rws gen migration add_index_to_posts

writes migrations/0002_add_index_to_posts.sql and .down.sql, each holding a statement that changes nothing (SELECT 1;) under a comment, so the files apply as they are until you replace it. DbPool::migrate splits a file at semicolons — keep them out of comments (migrations).

Tests

rws gen test posts [--app-expr 'app(Store::memory())']

reads the routes src/routes/posts.rs declares (#[get("…")] and the like) and the fields of its NewPost, and writes tests/posts.rs: a round trip through every route there (create, show, list, update, remove, then 404), and the errors (404 for an unknown id, 400 for a non-numeric id and for a body that isn't JSON) — through TestClient, no socket. The test builds the app with app(); when app takes arguments, with what the existing tests in tests/ pass to TestClient::new, or --app-expr.

Conflicts, dry runs, JSON

$ rws gen model Post title:String          # after the one above
conflict   src/models/post.rs
unchanged  src/models/mod.rs
…
--- a/src/models/post.rs
+++ b/src/models/post.rs
@@ -11,9 +11,6 @@
     #[primary_key(auto_increment)]
     pub id: i64,
     pub title: String,
-    pub views: i64,
…
rws gen: not written — a file above exists with other content (the diff is what the generator would write). Move it aside or merge by hand.
  • A file the generator creates is never replaced. If it exists with exactly what the generator would write, it is unchanged; with anything else it is a conflict, the diff goes from the file to what would be written, and nothing of the plan is written. Re-running the same command is safe.
  • --dry-run prints the plan with every diff and writes nothing.
  • --json prints the plan for agents: {"command":"rws gen model Post title:String","status":"written","written":true,"changes":[{"path":"src/models/post.rs","action":"create","diff":"--- /dev/null\n+++ b/src/models/post.rs\n…"}],"notes":[…]} — status is written, planned (dry run), unchanged or conflict; errors are {"status":"usage"|"refused","error":"…"}.
  • --dir <project> runs against another directory (default: the working directory, which must hold the project's Cargo.toml).

Exit codes: 0 written (or nothing to do), 1 a conflict or a project the generator can't work with (no app(), no route module for gen test), 2 a usage error (a bad name or type, an unknown option).

Names are identifiers — letters, digits and single _, starting with a letter, at most 64 characters, not a Rust keyword — so no generated path leaves the project; a resource whose type would clash with a name the generated code imports (paths → Path, values → Value) is refused.

From Rust

use rust_web_server::generator::{plan, Field, Generate};

let request = Generate::Model {
    name: "Post".into(),
    fields: vec![Field::parse("title:String")?],
    db: None,
};
let plan = plan(".", &request)?;    // reads only
print!("{}", plan.render(true));    // one line per file, then the diffs
if !plan.has_conflicts() {
    plan.apply(".")?;                // writes; refuses if a file changed since
}

Plan has the changes (FileChange { path, kind, content, original, diff }, kind one of Create, Edit, Unchanged, Conflict), the notes, and to_json(written); generator::run(args, out, err) is the command line.

Edit this page on GitHub