cargo-rustango 0.57.11

`cargo rustango new <name>` — project scaffolder for the rustango web framework.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
//! File body templates for `cargo rustango new`.
//!
//! Templates are plain `const &str` (or builder fns when they need
//! interpolation). Keeping them in-source means the binary has zero
//! runtime filesystem dependency and `cargo install cargo-rustango`
//! ships everything in one artifact. CI snapshot-tests the generated
//! output by running `cargo check` on each template.

use super::{Backend, Template};

// ---------------- Cargo.toml ----------------

pub fn cargo_toml(
    name: &str,
    template: Template,
    backend: Backend,
    features: &[String],
    rustango_path: Option<&str>,
) -> String {
    let rustango_dep = template.rustango_dep(rustango_path, features);
    let default_backend = backend.feature();

    // `rustango::jobs::Job` is an async trait, so a project that
    // implements one needs `#[async_trait]`. rustango depends on
    // async-trait itself (behind `_async_trait`) but does not re-export
    // the attribute macro, so the project must name it directly.
    //
    // Without this, `manage make:job` emits a file that does not
    // compile, and the only remedy is a comment inside it — generated
    // code carrying its own bug report. `batteries` pulls `jobs` in, so
    // the check covers the templates that get jobs implicitly as well as
    // an explicit `--features jobs`.
    let wants_jobs = template.base_features().iter().any(|f| *f == "batteries")
        || features
            .iter()
            .any(|f| f == "jobs" || f == "jobs-postgres" || f == "batteries");
    let async_trait_dep = if wants_jobs {
        "\n# `rustango::jobs::Job` is an async trait — implementing one needs this.\n\
         async-trait = \"0.1\""
    } else {
        ""
    };
    format!(
        r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2021"

# Empty `[workspace]` table makes this project standalone: if a parent
# directory has its own workspace `Cargo.toml`, cargo would otherwise
# refuse to build (see "current package believes it's in a workspace
# when it's not"). This declaration severs that link without taking on
# any workspace members. Delete it if you intentionally want the
# project to be a member of a parent workspace.
[workspace]

[dependencies]
# `rustango` re-exports every proc-macro (Model / Form / ViewSet /
# Serializer / embed_migrations / main), so you do NOT need to depend
# on `rustango-macros` directly. Use `rustango::Model` etc.
rustango = {rustango_dep}
tokio = {{ version = "1", features = ["macros", "rt-multi-thread", "sync", "signal", "net"] }}
axum = {{ version = "0.8", default-features = false, features = ["tokio", "http1", "json", "form", "query"] }}
tower = {{ version = "0.5", features = ["util"] }}
serde = {{ version = "1", features = ["derive"] }}
serde_json = "1"
chrono = {{ version = "0.4", default-features = false, features = ["serde", "clock"] }}
tracing = "0.1"
tracing-subscriber = {{ version = "0.3", features = ["env-filter"] }}
dotenvy = "0.15"{async_trait_dep}

[dev-dependencies]
tokio = {{ version = "1", features = ["macros", "rt-multi-thread"] }}

# The `#[derive(Model)]` FromRow / LoadRelated impls are
# `#[cfg(feature = "...")]`-gated on THIS crate's features, so a backend
# feature must be enabled here for them to compile. `default` picks the
# one `cargo run` uses; switch with e.g.
# `cargo run --no-default-features --features sqlite`.
[features]
default = ["{default_backend}"]
postgres = ["rustango/postgres"]
sqlite = ["rustango/sqlite"]
mysql = ["rustango/mysql"]
"#
    )
}

// ---------------- .env.example ----------------

pub fn env_example(name: &str, backend: Backend) -> String {
    let url = backend.compose_url(name);
    // SQLite is a file in the bind mount, so there is no host to swap.
    let host_note = match backend.service() {
        Some(svc) => format!(
            "# Defaults suit `docker compose up -d`; the database name matches
# docker-compose.yml. Running cargo on the host instead of in the rust
# container? Change `{svc}` -> `localhost` below."
        ),
        None => "# SQLite needs no database server — the file lives beside the
# project and is created on first `cargo run -- migrate`."
            .to_owned(),
    };
    format!(
        "# Copy to .env and edit for your environment; src/main.rs loads it at startup.
{host_note}
DATABASE_URL={url}
RUSTANGO_BIND=0.0.0.0:8080

# Apex domain the operator console is served on.
RUSTANGO_APEX_DOMAIN=localhost

# Session signing key — 32 bytes of base64. Anything else is discarded
# silently and a key is generated into ./var/ instead, so a project that
# looks configured is not. Leave it commented in development; set a real
# one in production and keep it out of source control:
#   RUSTANGO_SESSION_SECRET=$(openssl rand -base64 32)
# RUSTANGO_SESSION_SECRET=

# Log filter, standard RUST_LOG syntax. Default is `info,sqlx=warn`.
# Production sets this in the orchestrator rather than here.
# RUST_LOG=info,my_app=debug
"
    )
}

// ---------------- .gitignore ----------------

/// What a generated project must never commit.
///
/// `var/` is the one that matters and was missing. When
/// `RUSTANGO_SESSION_SECRET` is unset, the server generates a signing
/// key and writes it to `./var/.rustango_session.key` — plus
/// `.rustango_tenant_session.key` and `.rustango_operator_session.key`
/// on a tenancy project. So `cargo run` followed by `git add .`
/// committed a session signing key, on the very first run, in a
/// three-line `.gitignore` that did not mention the directory.
///
/// `*.db*` for the same reason in a different register: a
/// sqlite-backend project otherwise commits its development database.
/// (The generated `.dockerignore` already excluded those and this file
/// did not, which is the asymmetry that made it easy to miss.)
pub const GITIGNORE: &str = "/target
/.env

# Runtime state. The server writes generated session signing keys here
# when RUSTANGO_SESSION_SECRET is unset — never commit them.
/var/

# Local databases (sqlite backend, test fixtures).
*.db
*.db-wal
*.db-shm
*.db-journal

*.log
";

// ---------------- rust-toolchain.toml ----------------

/// Point rustup at `stable` in the new project so users on macOS who
/// have Homebrew's older `rust` binary on PATH (currently 1.86) don't
/// get the "rustc 1.86.0 is not supported by the following packages"
/// error when they `cd` into the project. rustup reads this file and
/// uses the named toolchain inside the project regardless of which
/// cargo they invoked.
///
/// Deliberately **not** an exact version. rustango's MSRV lives in
/// `Cargo.toml` (`rust-version`), which is a floor — a generated app
/// has no reason to be held *at* that floor, and pinning one would
/// leave every project born on an ageing compiler. A stale exact pin
/// also breaks IDE support: rust-analyzer refuses toolchains much
/// older than itself, so VS Code warns that the channel is too old
/// and silently drops to no IDE services.
///
/// `components` is the second half of that fix — it makes the editor
/// use the toolchain's own rust-analyzer rather than the one bundled
/// with the extension. Mirrors this repo's own `rust-toolchain.toml`.
pub const RUST_TOOLCHAIN: &str = "[toolchain]
channel = \"stable\"
components = [\"rustfmt\", \"clippy\", \"rust-analyzer\"]
";

// ---------------- docker-compose.yml ----------------

/// Bundle a working Postgres + Rust hot-reload dev stack out of the
/// box (#86). The `rust` service runs `cargo watch -x run` against
/// the bind-mounted source tree; the three named volumes
/// (`cargo-target`, `cargo-registry`, `cargo-git`) preserve
/// incremental build state across container restarts so a fresh
/// `docker compose up` doesn't trigger a full from-scratch rebuild.
///
/// Users who prefer running cargo on the host can simply ignore the
/// `rust` service and run the postgres service standalone with
/// `docker compose up -d postgres`.
pub fn docker_compose(name: &str, backend: Backend) -> String {
    let db_service = match backend {
        Backend::Postgres => format!(
            r#"  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: rustango
      POSTGRES_PASSWORD: rustango
      POSTGRES_DB: {name}_dev
    # Without this the data lives in the container's writable layer, so
    # `docker compose down` deletes your development database and `up`
    # hands back an empty one, with nothing said either way (#1309).
    volumes:
      - postgres-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U rustango -d {name}_dev"]
      interval: 2s
      timeout: 2s
      retries: 20

"#
        ),
        Backend::Mysql => format!(
            r#"  mysql:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: rustango
      MYSQL_DATABASE: {name}_dev
      MYSQL_USER: rustango
      MYSQL_PASSWORD: rustango
    # Without this the data lives in the container's writable layer, so
    # `docker compose down` deletes your development database and `up`
    # hands back an empty one, with nothing said either way (#1309).
    volumes:
      - mysql-data:/var/lib/mysql
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u rustango -prustango"]
      interval: 2s
      timeout: 2s
      retries: 30

"#
        ),
        // A file in the bind mount — nothing to run.
        Backend::Sqlite => String::new(),
    };
    // Declared only when the matching service exists — compose errors on a
    // volume that nothing mounts, and SQLite has no service at all.
    let db_volume = match backend {
        Backend::Postgres => "  postgres-data:\n",
        Backend::Mysql => "  mysql-data:\n",
        Backend::Sqlite => "",
    };
    let (skip_hint, depends_on) = match backend.service() {
        Some(svc) => (
            format!(
                "  # Skip it and run cargo on the host with\n  \
                 # `docker compose up -d --no-deps {svc}`."
            ),
            format!("    depends_on:\n      {svc}:\n        condition: service_healthy\n"),
        ),
        // Nothing else to boot, so skipping it means not using compose.
        None => (
            "  # Skip it by running `cargo run` on the host — with SQLite\n  \
             # there is nothing else compose needs to start."
                .to_owned(),
            String::new(),
        ),
    };
    format!(
        r#"services:
{db_service}  # Hot-reload Rust dev container. `cargo watch -x run` rebuilds and
  # restarts the binary on every source edit.
{skip_hint}
  rust:
    # Dockerfile.dev, not Dockerfile: the plain one is the deployable
    # release image, which copies the source in and would defeat the
    # bind mount below.
    build:
      context: .
      dockerfile: Dockerfile.dev
{depends_on}    env_file:
      - .env
    volumes:
      - ./:/app
      - cargo-target:/app/target
      - cargo-registry:/usr/local/cargo/registry
      - cargo-git:/usr/local/cargo/git
    environment:
      CARGO_TARGET_DIR: /app/target
      CARGO_INCREMENTAL: "1"
    command: bash -c "cargo watch -x run"
    ports:
      - "8080:8080"

volumes:
{db_volume}  cargo-target:
  cargo-registry:
  cargo-git:
"#
    )
}

// ---------------- Dockerfile ----------------

/// Companion to [`docker_compose`] — a thin Rust dev image with
/// `cargo-watch` preinstalled. The image stays small because all
/// project sources land via the bind mount; this image only needs
/// the toolchain + cargo-watch installed.
///
/// Tracks the latest Rust 1.x release, matching the `stable` channel
/// in `rust-toolchain.toml` — see [`RUST_TOOLCHAIN`] for why neither
/// is pinned to an exact version. Pin both together (`rust:1.90` +
/// `channel = "1.90"`) if you need byte-reproducible dev images.
///
/// This is `Dockerfile.dev`. The plain `Dockerfile` is
/// [`dockerfile_prod`] — see its docs for why a project needs both.
pub fn dockerfile_dev() -> &'static str {
    "FROM rust:1\n\
     \n\
     WORKDIR /app\n\
     \n\
     RUN cargo install cargo-watch\n"
}

/// The image you actually deploy.
///
/// For a long time the only generated Dockerfile was [`dockerfile_dev`],
/// which builds nothing: it installs a toolchain and waits for a bind
/// mount. That is right for the hot-reload loop and wrong for every
/// other purpose — it runs the **debug** profile, needs the source tree
/// mounted at run time, and runs as root. A generated project could be
/// developed in Docker but not shipped in it.
///
/// So: multi-stage, release profile, sources copied in rather than
/// mounted, and a runtime stage that carries only the binary. The two
/// stages must agree on libc, hence `bookworm` on both — a
/// `bookworm`-built binary will not start on `bullseye`.
///
/// `--locked` is deliberate. A deploy image is exactly where an
/// unnoticed dependency bump should fail the build rather than ship.
pub fn dockerfile_prod(name: &str) -> String {
    format!(
        r#"# The deployable image. `Dockerfile.dev` is the hot-reload one
# docker-compose.yml uses; this one is for shipping.
#
#   docker build -t {name}:latest .
#   docker run --rm -p 8080:8080 -e DATABASE_URL=... {name}:latest
#
# Pick a non-default backend at build time:
#   docker build --build-arg FEATURES=sqlite --build-arg NO_DEFAULT=1 .

FROM rust:1-bookworm AS builder
ARG FEATURES=""
ARG NO_DEFAULT=""
WORKDIR /src
COPY . .
# Incremental compilation only grows the cache in a one-shot image build.
ENV CARGO_INCREMENTAL=0
RUN set -eux; \
    flags="--release --locked"; \
    if [ -n "$NO_DEFAULT" ]; then flags="$flags --no-default-features"; fi; \
    if [ -n "$FEATURES" ]; then flags="$flags --features $FEATURES"; fi; \
    cargo build $flags; \
    mkdir -p /out; \
    cp target/release/{name} /out/

# Same Debian release as the builder: the binary links the builder's glibc.
FROM debian:bookworm-slim
RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates curl \
 && rm -rf /var/lib/apt/lists/*
COPY --from=builder /out/{name} /usr/local/bin/{name}
# Migrations and settings are read at run time, so they travel with the
# binary rather than being baked into it.
COPY migrations /app/migrations
COPY config /app/config
# Never root. The numeric id keeps the ownership stable if the image is
# rebuilt on a host whose useradd picks a different one.
RUN useradd --uid 10001 --create-home app && chown -R 10001 /app
USER 10001
WORKDIR /app
EXPOSE 8080
# `.with_health()` mounts /health; drop this line if you removed it.
HEALTHCHECK --interval=5s --timeout=3s --start-period=20s --retries=12 \
  CMD curl -fsS http://127.0.0.1:8080/health || exit 1
CMD ["{name}"]
"#
    )
}

// ---------------- .dockerignore ----------------

/// Without this, `docker build .` sends the whole working tree to the
/// daemon — `target/` included, which is gigabytes, and `.env`, which is
/// secrets. Both then land in the image.
///
/// It matters for [`dockerfile_dev`] too: that image mounts the source
/// rather than copying it, but the build context is still uploaded.
pub const DOCKERIGNORE: &str = "\
# Build output — the image builds its own, and this is the single
# biggest thing that would otherwise be uploaded to the daemon.
target/
**/target/

# Secrets. `.env.example` is the committed template and is safe.
.env
.env.*
!.env.example

# Local databases — a SQLite file here would be baked into the image.
*.db
*.db-wal
*.db-shm
*.db-journal

# Nothing in the image needs these.
.git/
.gitignore
node_modules/
**/node_modules/
";

// ---------------- README.md ----------------

pub fn readme(name: &str, template: Template, backend: Backend) -> String {
    let template_label = match template {
        Template::Api => "api (bare ORM + axum, no admin)",
        Template::Fullstack => "fullstack (ORM + auto-admin)",
        Template::Tenant => "tenant (multi-tenancy + operator console)",
    };
    let backend_label = backend.feature();
    let (up_line, host_path) = match backend.service() {
        Some(svc) => (
            format!("docker compose up -d                 # boots {svc} + rust + cargo-watch"),
            format!(
                r#"### B. Cargo on the host (Docker just for {svc})

```sh
cp .env.example .env                 # then change `{svc}` -> `localhost` in DATABASE_URL
docker compose up -d {svc}        # only the DB
cargo run -- migrate                 # apply pending migrations
cargo run                            # boot the HTTP server
cargo run -- --help                  # full verb list (makemigrations, startapp, etc.)
```"#
            ),
        ),
        None => (
            "docker compose up -d                 # boots rust + cargo-watch".to_owned(),
            r#"### B. Cargo on the host (no Docker at all)

SQLite is a file next to the project, so nothing else has to be running.

```sh
cp .env.example .env
cargo run -- migrate                 # creates the database file and applies migrations
cargo run                            # boot the HTTP server
cargo run -- --help                  # full verb list (makemigrations, startapp, etc.)
```"#
                .to_owned(),
        ),
    };
    format!(
        r#"# {name}

Generated with `cargo rustango new {name}` — template `{template_label}`,
backend `{backend_label}`.

## Run locally — two paths

### A. All-in-Docker (default; hot-reload via cargo-watch)

```sh
cp .env.example .env
{up_line}
docker compose run --rm rust cargo run -- migrate
# server lives at http://localhost:8080 — edits to src/ trigger rebuild
```

The `rust` service runs `cargo watch -x run` against the bind-mounted
source tree. Three named volumes preserve incremental build state
across container restarts so a fresh `up` doesn't recompile from
scratch.

{host_path}

Either way: `cargo run` (no args) is `runserver`. Every other
management verb (`makemigrations`, `migrate`, `startapp`, `check`, …)
flows through the same binary via `rustango::manage::Cli` — see
`src/main.rs`.

## Project layout

```text
src/
  main.rs         — Cli::new().api(urls::api()).run() boots both server + verbs
  models.rs       — every #[derive(Model)] lives here
  views.rs        — request handlers
  urls.rs         — pub fn api() -> Router aggregator

migrations/       — JSON migration files (committed to git)
```

Adding a new model is one struct in `models.rs`; the auto-admin sees
it immediately. See <https://github.com/ujeenet/rustango> for the full
feature list.
"#
    )
}

// ---------------- src/main.rs ----------------

/// Per-template `main.rs` — api/fullstack get the simple sqlx pool +
/// `urls::router(pool)` shape; tenant gets `rustango::server::Builder`
/// which auto-mounts the operator console at the apex and the tenant
/// admin at every subdomain via host-based dispatch. Without Builder,
/// the tenant template would scaffold a server that 404s on `/admin`
/// because nothing wires the auto-admin or operator console in — the
/// v0.8.1 Builder does that work.
///
/// Emits `src/main.rs` — the binary, which uses the library target
/// written by [`lib_rs`].
///
/// `name` is the package name; the crate name Rust sees replaces `-`
/// with `_`. Interpolated with `replace` rather than `format!` because
/// these templates are Rust source full of braces.
pub fn main_rs(template: Template, name: &str) -> String {
    let body = match template {
        Template::Api => MAIN_RS_API,
        Template::Fullstack => MAIN_RS_FULLSTACK,
        Template::Tenant => MAIN_RS_TENANT,
    };
    body.replace("{crate_name}", &crate_name(name))
}

/// The identifier Rust uses for a package: hyphens become underscores.
pub fn crate_name(package_name: &str) -> String {
    package_name.replace('-', "_")
}

/// `src/lib.rs` — where the app actually lives.
///
/// A generated project had only a binary, and that made
/// `manage make:worker` produce something that could not work: a file
/// under `src/bin/` is **its own crate**, so the `crate::jobs::…` its
/// template suggested referred to the worker itself, and there was no
/// path from it to the app's models or job types. Every project that
/// wanted a worker had to notice this and restructure by hand.
///
/// With a library target the binary and every `src/bin/*.rs` share one
/// crate: `use my_app::jobs::SendReceipt;` works from both, and the
/// worker registers exactly the types the server dispatches — which is
/// not a nicety. A worker that registers a *different* set picks up
/// rows it has no handler for and returns without unlocking them,
/// stranding work where `pending_count()` cannot see it.
pub fn lib_rs(name: &str) -> String {
    let krate = crate_name(name);
    format!(
        "//! `{krate}` — the application, as a library.
//!
//! Everything lives here rather than in `src/main.rs` so that binaries
//! under `src/bin/` can reach it. A file in `src/bin/` is its own
//! crate: `crate::` there is that binary, not this project, so a
//! worker generated by `manage make:worker` could not otherwise see
//! your models or job types.
//!
//! From `src/main.rs` or any `src/bin/*.rs`:
//!
//! ```ignore
//! use {krate}::urls;
//! use {krate}::models::SomeModel;
//! ```
//!
//! `manage startapp <name>` adds its `pub mod <name>;` line here.

pub mod models;
pub mod urls;
pub mod views;
"
    )
}

const MAIN_RS_API: &str = "//! Project entrypoint — `Cli::run()` is the unified dispatcher
//! that handles `cargo run` (runserver) AND `cargo run -- migrate` /
//! `makemigrations` / `startapp` / etc. from one binary. No
//! `src/bin/manage.rs` needed.
//!
//! Logging is auto-configured by `#[rustango::main]` —
//! `tracing_subscriber::fmt` with env-filter, default
//! `info,sqlx=warn`. Override with `RUST_LOG` (see `.env.example`)
//! or replace the macro with a hand-rolled subscriber in front of
//! `Cli::new()` for JSON / file-rotation / OTel export.

// The app's modules live in the library target (src/lib.rs) so that
// binaries under src/bin/ — a worker from `manage make:worker`, say —
// can reach the same models and jobs this one does. A `src/bin/*.rs`
// file is its own crate, so `crate::` there is NOT this project.
use {crate_name}::urls;

#[rustango::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let _ = dotenvy::dotenv();
    rustango::manage::Cli::new()
        .api(urls::api())
        .with_welcome() // friendly `/` on first run; drop once you have a root handler
        // NOTE: no .with_settings_from_env() here. It is gated on the
        // config feature, and this template is deliberately bare: ORM +
        // axum + the manage dispatcher. Adding config alone does not
        // help either — the settings layers it switches on also need
        // session, signals, access_log, allowed_hosts and body_limit,
        // which together are batteries, which is the fullstack template.
        //
        // So the config/*.toml this scaffolder writes are inert in an api
        // project. Pre-existing, and worth fixing properly — either stop
        // writing them here, or give the template the features to read
        // them — but not by making a minimal template heavy.
        .run()
        .await
}
";

const MAIN_RS_FULLSTACK: &str = "//! Project entrypoint — `Cli::run()` is the unified dispatcher
//! that handles `cargo run` (runserver) AND `cargo run -- migrate` /
//! `makemigrations` / `startapp` / etc. from one binary. No
//! `src/bin/manage.rs` needed. The auto-admin is not wired up for
//! you: add an `admin_router(pool)` helper to `src/urls.rs` and nest
//! it under `/admin` (see the getting-started guide, Step 11).
//!
//! Logging is auto-configured by `#[rustango::main]` —
//! `tracing_subscriber::fmt` with env-filter, default
//! `info,sqlx=warn`. Override with `RUST_LOG` (see `.env.example`)
//! or replace the macro with a hand-rolled subscriber in front of
//! `Cli::new()` for JSON / file-rotation / OTel export.

// See the note in the api template: the app's modules live in the
// library target so `src/bin/*.rs` binaries can reach them.
use {crate_name}::urls;

#[rustango::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let _ = dotenvy::dotenv();
    rustango::manage::Cli::new()
        .api(urls::api())
        .with_welcome() // friendly `/` on first run; drop once you have a root handler
        .with_health() // /health + /ready endpoints for load balancers
        // Loads config/*.toml for the RUSTANGO_ENV tier (default `dev`),
        // then RUSTANGO__* env overrides. Without it the files the
        // scaffolder writes are inert.
        .with_settings_from_env()
        .run()
        .await
}
";

const MAIN_RS_TENANT: &str = r##"//! Tenant project entrypoint — HTTP server serving both the operator
//! console and per-tenant apps via subdomain routing.
//!
//! Logging is auto-configured by `#[rustango::main]` —
//! `tracing_subscriber::fmt` with env-filter, default
//! `info,sqlx=warn`. Override with `RUST_LOG` (see `.env.example`)
//! or replace the macro with a hand-rolled subscriber in front of
//! `Cli::new()` for JSON / file-rotation / OTel export.

// See the note in the api template: the app's modules live in the
// library target so `src/bin/*.rs` binaries can reach them.
use {crate_name}::urls;

#[rustango::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let _ = dotenvy::dotenv();
    rustango::manage::Cli::new()
        .tenancy()
        .api(urls::api())
        .with_welcome() // friendly `/` on first run; drop once you have a root handler
        .with_health() // /health + /ready hit the registry pool
        .run()
        .await
}
"##;

// MANAGE_RS_TENANT removed in v0.16 — tenant projects now use the
// same single-binary `Cli::new().tenancy().run()` shape as every
// other template; src/main.rs is the one entrypoint.

// ---------------- src/models.rs ----------------

pub fn models_rs(template: Template) -> String {
    let header = "//! Project models — every #[derive(Model)] lives here.
//!
//! Adding a struct here makes it admin-visible automatically: the
//! macro populates the `inventory` registry that
//! `rustango::admin::router(pool)` walks.

use rustango::sql::Auto;
use rustango::Model;

// `#[derive(Model)]` registers this struct at *runtime* through `inventory`,
// which rustc cannot see — so fields only the framework reads look dead to it.
// Delete this line once your own code reads them.
#[allow(dead_code)]
#[derive(Model, Debug, Clone)]
#[rustango(table = \"item\", display = \"name\")]
pub struct Item {
    #[rustango(primary_key)]
    pub id: Auto<i64>,
    #[rustango(max_length = 64)]
    pub name: String,
    pub active: bool,
}
";

    if matches!(template, Template::Tenant) {
        format!(
            "{header}
// Tenancy registry models (Org, Operator, User) come along automatically
// with the `tenancy` feature — you don't need to redefine them here.
"
        )
    } else {
        header.to_owned()
    }
}

// ---------------- src/views.rs ----------------

pub const VIEWS_RS: &str = "//! Project views — the request handlers this project mounts.

use axum::response::Html;

pub async fn index() -> Html<&'static str> {
    Html(
        \"<!doctype html>\\n\\
         <title>rustango app</title>\\n\\
         <h1>Hello from Rustango!</h1>\\n\\
         <p>The auto-admin (if enabled) is at <a href=\\\"/admin\\\">/admin</a>.</p>\",
    )
}

pub async fn healthz() -> &'static str {
    \"ok\"
}
";

// ---------------- src/urls.rs ----------------

pub fn urls_rs(template: Template) -> String {
    match template {
        Template::Api => {
            // Stateless aggregator. `manage startapp <name>` auto-
            // patches a `.merge(crate::<name>::urls::api())` line
            // after `Router::new()` so additional apps compose
            // cleanly. Handlers that need the pool can read it from
            // request extensions (`Extension<PgPool>`) — main.rs
            // attaches it via `.layer(Extension(pool))`.
            "//! Project URL routing (template: api — no admin).
//!
//! `Router::new()` is the auto-mount anchor — `manage startapp`
//! inserts `.merge(crate::<name>::urls::api())` lines here.

use axum::routing::get;
use axum::Router;

use crate::views;

pub fn api() -> Router<()> {
    Router::new()
        .route(\"/\", get(views::index))
        .route(\"/healthz\", get(views::healthz))
}
"
            .to_owned()
        }
        Template::Fullstack => {
            // #1210/#1211 — this used to also emit an `admin_router(pool)`
            // helper. Nothing generated called it (the generated `main.rs`
            // never nested it), so it was dead code — a warning in every
            // fresh project — and it was the only generated line naming
            // `PgPool`, hard-wiring Postgres into a project whose manifest
            // offers sqlite and mysql.
            //
            // Removing it means a fullstack project has NO admin until the
            // author adds one. There is no `Cli` auto-mount for the
            // single-tenant admin (that exists only for `tenancy`), so
            // getting-started Step 11 has to spell the helper out — see
            // `examples/getting_started_blog/src/urls.rs`, which defines it
            // by hand and is covered by `tests/admin_smoke.rs`.
            "//! Project URL routing (template: fullstack — ORM + admin).
//!
//! `Router::new()` in `api()` is the auto-mount anchor —
//! `manage startapp` inserts `.merge(crate::<name>::urls::api())`
//! lines here.
//!
//! The admin is NOT wired up for you. Add an `admin_router(pool)` helper
//! here and nest it under `/admin` from `main.rs` — getting-started
//! Step 11 spells it out. Take `rustango::sql::Pool` so the helper names
//! no driver and the project still builds on all three backends.

use axum::routing::get;
use axum::Router;

use crate::views;

pub fn api() -> Router<()> {
    Router::new()
        .route(\"/\", get(views::index))
        .route(\"/healthz\", get(views::healthz))
}
"
            .to_owned()
        }
        Template::Tenant => {
            // Multi-tenant: main.rs uses `rustango::server::Builder`,
            // which expects a stateless `Router<()>` and injects the
            // `TenantContext` extension itself so handlers can use
            // `rustango::extractors::Tenant`. The user's routes mount
            // on every tenant subdomain alongside the auto-admin.
            "//! Project URL routing (template: tenant).
//!
//! `Builder::api(...)` mounts these routes on every tenant
//! subdomain alongside the auto-admin. Handlers can take
//! `rustango::extractors::Tenant` to resolve the current tenant +
//! get a tenant-scoped `&mut PgConnection`. Example:
//!
//! ```ignore
//! pub async fn list_items(mut t: rustango::extractors::Tenant)
//!     -> Result<axum::Json<Vec<crate::models::Item>>, axum::http::StatusCode> {
//!     let rows = crate::models::Item::objects()
//!         .fetch_on(t.conn()).await
//!         .map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
//!     Ok(axum::Json(rows))
//! }
//! ```

use axum::routing::get;
use axum::Router;

use crate::views;

pub fn api() -> Router<()> {
    Router::new()
        .route(\"/\", get(views::index))
        .route(\"/healthz\", get(views::healthz))
}
"
            .to_owned()
        }
    }
}
// The framework no longer ships hardcoded bootstrap migration JSON.
// `cargo rustango new --template tenant` seeds an empty `system/
// migrations/` folder; `cargo run -- makemigrations` generates the
// framework's own tables from the compiled models, and
// `cargo run -- migrate` applies them.

// ---------------- Tiered settings files (#87) ----------------
//
// Every fresh project ships with four config files: `default.toml`
// for shared knobs + one `<env>_settings.toml` per tier (dev /
// staging / prod). The runtime picks the tier from `RUSTANGO_ENV`
// (default `dev`). Sensible-by-default values mean a freshly-
// scaffolded `cargo run` works without any TOML edits; production
// deploys override the prod tier with their own values.

/// `config/default.toml` — shared values that don't depend on tier.
/// Intentionally sparse — every section is optional and defaults to
/// the section's `Default` impl. Tier files override here.
pub fn config_default_toml(name: &str, backend: Backend) -> String {
    let example_url = backend.host_url(name);
    format!(
        r##"# {name} — shared defaults across every tier
# (`config/default.toml` is loaded first; `config/<RUSTANGO_ENV>_settings.toml`
# overrides on top.) Every section is optional — uncomment + edit
# what you need.

# [database]
# url           = "{example_url}"
# pool_min_size = 2
# pool_max_size = 20

# [admin]
# allowed_tables   = []        # empty = every registered model
# read_only_tables = []

# [server]
# bind                  = "127.0.0.1:8080"
# request_timeout_secs  = 30
# max_body_bytes        = 2097152      # 2 MiB

# [auth]
# argon2_memory_kib  = 19456    # OWASP 2024 floor
# argon2_iterations  = 2
# lockout_threshold  = 5
# lockout_duration_secs = 900

# [auth.jwt]
# access_ttl_secs   = 900       # 15 min
# refresh_ttl_secs  = 604800    # 7 days
# issuer            = "{name}"

# [brand]
# name           = "{name}"
# tagline        = ""
# primary_color  = "#2c6fb0"
# theme_mode     = "auto"       # auto | light | dark

# [security]
# headers_preset       = "strict"
# hsts_max_age_secs    = 31536000     # 1 year
# cors_allowed_origins = []

# [routes]
# legacy_preset = false
# # Per-field overrides: login_url / admin_url / audit_url / static_url /
# # brand_url / change_password_url / impersonation_handoff_url

# [audit]
# retention_days = 90

# [logging]                        # needs `Cli::with_logging()` AND
#                                  # `#[rustango::main(logging = false)]` —
#                                  # see config/prod_settings.toml for why
# level             = "info,sqlx=warn"   # RUST_LOG syntax; RUST_LOG still wins
# format            = "pretty"           # pretty | json | compact
# with_line_numbers = false
# file_dir          = "/var/log/{name}"  # set to also write a rolling file
# file_rotation     = "daily"            # daily | hourly | minutely | never

# [mcp]                            # Model Context Protocol server (feature = "mcp")
# prefix                = "/mcp"   # URL prefix the MCP router mounts under
# token_ttl_secs        = 900      # agent access-token lifetime (15 min)
# enable_sse            = true     # serve the GET {{prefix}} SSE stream
# allowed_origins       = []       # CORS allow-list (empty = same-origin only)
# rate_limit_per_minute = 0        # per-IP cap (0/unset = unlimited)
# max_tools_listed      = 0        # tools/list page size (0/unset = unlimited)
"##
    )
}

/// `config/dev_settings.toml` — local development. Loose defaults:
/// short JWT TTLs (so token-rotation bugs surface early), no HSTS
/// (so http→https rebinds don't lock the browser), debug-friendly
/// settings.
pub fn config_dev_settings_toml(name: &str, backend: Backend) -> String {
    let url = backend.host_url(name);
    format!(
        r##"# {name} — local development tier
# Loaded when RUSTANGO_ENV=dev (the default when unset).

[database]
# Matches docker-compose.yml and .env.example. Host is `localhost`, not
# the compose service name: this tier runs the app on the host against
# the container.
url = "{url}"

[server]
bind = "127.0.0.1:8080"

[security]
# Drop strict headers in dev so http<->https rebinds don't lock the
# browser into HSTS.
headers_preset    = "dev"
hsts_max_age_secs = 0

[brand]
# Make the dev tier visually distinguishable from prod.
tagline = "(dev)"
"##
    )
}

/// `config/staging_settings.toml` — production-like but pointed at
/// a separate database, with shorter retention.
pub fn config_staging_settings_toml(name: &str, backend: Backend) -> String {
    let staging_url = match backend {
        Backend::Postgres => format!("postgres://staging-host/{name}_staging"),
        Backend::Mysql => format!("mysql://staging-host/{name}_staging"),
        Backend::Sqlite => format!("sqlite://./{name}_staging.db?mode=rwc"),
    };
    format!(
        r##"# {name} — staging tier
# Loaded when RUSTANGO_ENV=staging. Production-shape security
# headers, but pointed at a separate database with shorter retention
# so QA volume doesn't bleed into prod analytics.

# [database]
# url = "{staging_url}"

[server]
bind = "0.0.0.0:8080"

[security]
headers_preset    = "strict"
hsts_max_age_secs = 31536000

[brand]
tagline = "(staging)"

[audit]
retention_days = 30
"##
    )
}

/// `config/prod_settings.toml` — production. Strict defaults; expects
/// real values (DATABASE_URL, secret_key, etc.) supplied via env
/// vars or out-of-band secret management. The TOML purposefully
/// leaves the database url commented — operators set `DATABASE_URL`,
/// which is the variable every pool actually reads.
pub fn config_prod_settings_toml(name: &str) -> String {
    format!(
        r##"# {name} — production tier
# Loaded when RUSTANGO_ENV=prod. Strict-by-default; sensitive values
# (database url, secret key) come from the environment or your secrets
# manager — leaving them out of source control.

# The URL comes from the `DATABASE_URL` environment variable, which is
# what every pool reads. Set it from your secrets manager.
#
# Pool sizes can be pinned here. If you do, uncomment the header *with*
# the keys: a key left uncommented under a commented-out `[database]`
# lands at the TOML root, where it is silently ignored.
# [database]
# pool_min_size = 5
# pool_max_size = 50

[server]
bind                 = "0.0.0.0:8080"
request_timeout_secs = 30

[security]
headers_preset    = "strict"
hsts_max_age_secs = 31536000

[audit]
retention_days = 365

# Read by `Cli::with_logging()`, which this project does NOT call. To make
# this section live: add `.with_logging()`, and change `src/main.rs` to
# `#[rustango::main(logging = false)]` so the macro's own subscriber steps
# aside — `tracing` keeps the first one installed, so without that the
# settings here are read and discarded (#1465).
# `RUST_LOG` overrides `level` either way, and works today.
# [logging]
# level  = "info"
# format = "json"     # one object per event, for a log aggregator
"##
    )
}