glacier-web-cli 0.4.1

Command-line tool for creating and running Glacier applications.
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
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
---
title: Getting Started with Glacier
description: Install Glacier, create a blog, generate posts with the scaffold, then add a validation and comments, testing as you go.
category: Start Here
order: 1
---

This guide takes you from nothing to a small working blog: posts you can
create, edit and delete, with comments underneath. Along the way it shows
how a Glacier app is laid out, what the generators write for you, and how
the tests that come with them run.

After reading this guide, you will know:

- How to install Glacier and create a new application.
- How a Glacier app is laid out, and where your code goes.
- How to generate a model, controller, views, routes and tests with the
  scaffold generator, and what each generated file does.
- How to run the app and its tests.
- How to add a validation, change a view, and add a second model that
  belongs to the first.

## Guide Assumptions

This guide is for people starting a first Glacier app. It doesn't assume
you know Rust well, but you will need a Rust toolchain installed, and
you'll get further if you can read a little Rust.

Glacier is young. This guide sticks to what works today, and says so where
a feature isn't built yet.

## What is Glacier?

Glacier is a batteries-included web framework for Rust, inspired by Rails,
Phoenix and Laravel. It gives you a router, controllers, compiled HTML
templates, an ORM for SQLite and Postgres, migrations, background jobs,
sessions, flash messages, CSRF protection and a test framework, with
generators that write the usual pieces for you.

Controllers, models and tests are written in `.glacier`, a small
`do … end` syntax that each app's build script compiles to ordinary
Rust, line for line. Routes and migrations are plain Rust,
and you can write Rust anywhere `.glacier` is used. [The .glacier
Language](glacier_language.html) covers the syntax; this guide only uses
what the generators write and a few small changes to it.

Because everything is compiled, mistakes that other frameworks find at
runtime are build errors in Glacier: a typo in a template expression, a
link to a route that doesn't exist, or a misspelled column in a query.
When `glacier build` (or `glacier test` or `glacier server`) fails, the
error points at the line in your `.glacier` file.

## Creating a New Glacier Project

### Installing Glacier

You need Rust 1.94 or later, with `cargo`. If you don't have it,
[rustup](https://rustup.rs) installs it.

These guides describe Glacier 0.4.1. Install the `glacier` command from
crates.io, where it's published as `glacier-web-cli`:

```sh
cargo install glacier-web-cli
```

This builds the `glacier` command and puts it on your `PATH` (in
`~/.cargo/bin`). Check that it works:

```console
$ glacier --version
glacier 0.4.1
```

The apps it creates depend on the framework from crates.io, at the same
version as the command: `glacier-web` and, for the build script,
`glacier-dialect`. Cargo downloads them the first time an app builds.

### Creating the Blog Application

`glacier new` creates an application in a new directory. Run it where you
keep your projects:

```console
$ glacier new blog
      create  blog/Cargo.toml
      create  blog/.gitignore
      create  blog/AGENTS.md
      create  blog/.mcp.json
      create  blog/build.rs
      create  blog/src/main.rs
      create  blog/config/default.toml
      create  blog/config/development.toml
      create  blog/config/test.toml
      create  blog/config/production.toml
      create  blog/src/lib.rs
      create  blog/src/views/layouts/app.html
      create  blog/src/views/pages/home.html
      create  blog/public/app.css
      create  blog/src/app.glacier
      create  blog/src/controllers/pages_controller.glacier
      create  blog/tests/glacier.rs
      create  blog/tests/controllers/pages_controller.test.glacier
      create  blog/src/seeds.glacier
      create  blog/src/routes.glacier

Done! Next:

    cd blog
    glacier server

$ cd blog
```

An app name may contain letters, digits, `_` and `-`, and must start with a
letter.

This guide builds an app with HTML pages. For an app that only serves
JSON, `glacier new` takes `--api`: the app it creates has no views,
stylesheet or CSRF protection, answers errors in JSON, and its generators
write JSON controllers. [JSON APIs](json_apis.html) covers it.

### The Application's Layout

A new app is an ordinary Cargo package. Here is what each part is for:

| File or folder | What it's for |
| --- | --- |
| `Cargo.toml` | The package: depends on `glacier-web` (used in code as `glacier`) and, for the build script, `glacier-dialect` |
| `build.rs` | Compiles every `.glacier` file before each build |
| `.gitignore` | Leaves out `target/`, the SQLite databases in `db/` and the compiled `.glacier/` folder |
| `config/` | Settings: `default.toml` for every environment, then `development.toml`, `test.toml` and `production.toml` |
| `public/` | Static files served as they are, starting with `app.css` |
| `src/app.glacier` | The app: its own settings, such as middleware (the rest is added for you) |
| `src/main.rs`, `src/lib.rs` | One line each, never edited: `glacier::serve!()` starts the server, `glacier::app!()` builds the app from everything under `src/` |
| `src/routes.glacier` | The route table |
| `src/controllers/` | Controllers, one `<name>_controller.glacier` file each |
| `src/models/` | Models, one file each (none yet) |
| `db/migrations/` | Database migrations, one file each, beside `db/schema.sql` (none yet) |
| `src/views/` | HTML templates: `layouts/app.html` wraps every page, and each controller has a folder |
| `tests/` | Tests in `.glacier`, all found by `tests/glacier.rs` |

Two more folders appear once you build and run the app: `.glacier/`, where
the build script writes the Rust compiled from your `.glacier` files, and
`db/`, which holds the SQLite database. You never edit either.

`src/app.glacier` is the app itself. Its configuration, database,
migrations, seeds, static files and routes are added for you, so it
holds only the settings of its own, here two middleware:

```glacier
# The app. Its config, database, migrations, seeds, jobs, static files and
# routes are added for you; these are the settings of its own.
app do
  alias glacier.middleware.MethodOverride
  alias glacier.middleware.Logger

  # Before the logger, so logs show the overridden method.
  middleware MethodOverride
  middleware Logger
end
```

`src/lib.rs` and `src/main.rs` are one line each, and you never edit
them: `glacier::app!()` finds every file under `src/` and builds the app,
and `glacier::serve!()` reads the configuration, connects to the database
and serves it:

```rust
// The app: every module under src/, its migrations in db/, and the
// settings in src/app.glacier.
glacier::app!();
```

```rust
// Serves the app (or runs a command: `blog migrate`, `blog seed`).
glacier::serve!();
```

The configuration is plain TOML. `config/default.toml` sets the address the
server listens on, and its comment shows how environment variables
override any setting:

```toml
# Settings shared by every environment. `config/<env>.toml` overrides these,
# and environment variables override both: GLACIER__SERVER__PORT=8080.

[server]
host = "127.0.0.1"
port = 4000
```

In development the database is a SQLite file. `config/development.toml`
also holds a `secret_key_base`, which signs and encrypts the session
cookie; `glacier new` writes a random one. In production you set both
through the environment, as `config/production.toml` explains:

```toml
# Set secrets through the environment, never in this file:
#
#     GLACIER__SECRET_KEY_BASE=$(glacier secret)
#     GLACIER__DATABASE__URL=postgres://user:password@host/blog

[server]
host = "0.0.0.0"
```

The app runs in development unless the `GLACIER_ENV` environment variable
says `test` or `production`.

## Hello, Glacier!

### Starting the Server

From inside the `blog` directory, start the server:

```sh
glacier server
```

The first build compiles Glacier and its dependencies, so it takes a
while; later builds are quick. When the app is up, its log says it is
listening on `http://127.0.0.1:4000 (development)`. Open
<http://127.0.0.1:4000> in a browser to see the home page.

`glacier server` watches `src/`, `config/` and `Cargo.toml`. When you save
a change it rebuilds and restarts the app. If the rebuild fails, it shows
the errors and keeps the previous version running until you fix them.
Files in `public/` are served from disk, so changing them needs no
rebuild. Press Ctrl-C to stop the server.

### The Home Page

The home page comes from three pieces. The route table in `src/routes.glacier`
sends `GET /` to the `home` action of the `Pages` controller (you'll see
the whole file once the scaffold has added to it). The controller is in
`src/controllers/pages_controller.glacier`:

```glacier
module PagesController do
  implements Controller

  # Renders src/views/pages/home.html inside src/views/layouts/app.html.
  def home do
    render title: "Home"
  end
end
```

`render` picks the template by convention, from the controller and action
names: `src/views/pages/home.html`. That template is plain HTML:

```html
<h1>blog is running on Glacier</h1>
<p>This page is <code>src/views/pages/home.html</code>, rendered by <code>Pages.home</code> in <code>src/controllers/</code>.</p>
<p>Add routes in <code>src/routes.rs</code>, then <code>glacier generate migration create_posts</code> to start on a model.</p>
```

and `render` places it inside the layout, `src/views/layouts/app.html`,
passing it the `title`:

```html
{% props title: String, content: Rendered %}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ title }} · blog</title>
    <link rel="stylesheet" href="/app.css">
  </head>
  <body>
    {%- if let Some(notice) = flash("notice") %}
    <p class="flash">{{ notice }}</p>
    {%- end %}
    {{ content }}
  </body>
</html>
```

A template that takes values starts with `{% props … %}`, which declares them, with
Rust types. `{{ … }}` outputs a value, escaped for HTML, and `{% if %}`,
`{% for %}` and `{% end %}` control what is shown. Templates are compiled,
so a mistake in one is a build error. [Layouts and
Rendering](views.html) has the details.

`glacier new` also wrote a test for this page:

```glacier
test "shows the home page" do
  res = get "/"
  assert res.status == :ok
  assert res.body =~ "is running on Glacier"
end
```

## Generating a Scaffold (updated)

A blog needs posts. The scaffold generator writes everything for a
resource at once: a migration, a model, a controller with all seven
actions, their views and routes, and tests. Run it in the `blog` directory:

```console
$ glacier generate scaffold Post title:string body:text:optional published:boolean
      create  db/migrations/m20261004100354_create_posts.glacier
      create  src/models/post.glacier
      create  tests/factories/post.glacier
      create  tests/models/post.test.glacier
      update  tests/glacier.rs
      create  src/controllers/posts_controller.glacier
      create  src/views/posts/index.html
      create  src/views/posts/show.html
      create  src/views/posts/new.html
      create  src/views/posts/edit.html
      create  src/views/posts/_form.html
      create  tests/controllers/posts_controller.test.glacier
      update  tests/glacier.rs
      update  src/routes.glacier

Next: glacier db migrate, then visit /posts
```

Each field is `name:type`. The types are `string`, `text`, `integer`,
`bigint`, `float`, `boolean` and `references`, and `:optional` makes a
`string` or `text` field nullable, and `:unique` makes a field unique,
validated and indexed (`email:string:unique`). Every model also gets an `id` and the
`inserted_at` and `updated_at` timestamps, so you don't list those.

`g` is short for `generate`. There are smaller generators too: `glacier
generate model` writes only the migration, model, their tests and a factory, and
`glacier generate controller Reports summary` writes a controller with the
actions you name, their views, routes and tests.

### Running the Migration

The migration creates the `posts` table. Apply it:

```console
$ glacier db migrate
migrated  m20261004100354_create_posts
```

`glacier db status` lists the migrations and whether each has run, and
`glacier db rollback` reverts the last one. If you start the server in
development with migrations still pending, it logs a warning that tells
you to run `glacier db migrate`. [Migrations](migrations.html) covers them
in depth.

Now start the server again (or let the running one restart) and visit
<http://127.0.0.1:4000/posts>. You can list, create, show, edit and delete
posts.

## What the Scaffold Generated (updated)

None of the generated code is hidden in the framework: it's ordinary code
in your app, for you to read and change.

### The Migration

```glacier
migration do
  create_table :posts do
    string :title, null: false
    text :body
    boolean :published, null: false, default: false
    timestamps
  end
end
```

A migration says what to change, and `glacier db migrate` runs the ones
that haven't run, oldest first by the version in their names. They live
in `db/migrations/`, beside the schema in `db/schema.sql`; there's no list
to keep.

### The Model

```glacier
module Post do
  implements Model

  field title: String, required
  field body: Option<String>
  field published: bool
end
```

A model declares its fields with their Rust types. `String` is a required
column; `Option<String>` may be empty (`NULL`). `required` is a validation:
saving a post with a blank title fails, and the post carries the error
message. There's no list to add it to: every model in `src/models/` is
found when the app builds.

Models load and save themselves: `Post.find(id)!` loads one,
`Post.order(inserted_at: :desc)!` loads them all in order, and
`post.save()!` writes one. You'll see these in the controller next.
[Models](models.html) and [Querying](querying.html) cover
models fully.

### The Controller

```glacier
module PostsController do
  implements Controller

  def index do
    posts = Post.order(inserted_at: :desc)!
    render posts, title: "Posts"
  end

  def show do
    post = Post.find(params[:id])!
    render post, title: "Post"
  end

  def new do
    render post: Post.new(), title: "New post"
  end

  def create do
    post = Post.build(params)
    if post.save()! do
      redirect post, notice: "Post created."
    else
      render :new, post, title: "New post", status: 422
    end
  end

  def edit do
    post = Post.find(params[:id])!
    render post, title: "Edit post"
  end

  def update do
    post = Post.find(params[:id])!
    if post.update(params)! do
      redirect post, notice: "Post updated."
    else
      render :edit, post, title: "Edit post", status: 422
    end
  end

  def destroy do
    Post.find(params[:id])!.delete()!
    redirect Post.collection_path(), notice: "Post deleted."
  end
end
```

Each `def` is an action. `params` holds the request's parameters: the
submitted form, and the parts of the URL, so `params[:id]` is the `:id`
from `/posts/:id`.

A `!` after a call waits for it and passes any error up, so
`Post.find(params[:id])!` either gives you the post or ends the action
with an error; for a post that doesn't exist (or an id that isn't a
number), that error is a `404 Not Found`.

`render` renders the action's template, here `src/views/posts/index.html`
and so on, passing what you give it as the template's props.
`render :new, post, …` renders a different template of the same
controller, which is how `create` shows the form again with its errors and
a `422` status. `redirect` sends the browser elsewhere: given a post, it
redirects to that post's page, and `notice:` sets a flash message that the
layout shows on the next page.

`Post.build(params)` makes a new post from the form, converting each
field to its type, and `post.save()!` validates and inserts it, giving
`true` or `false`. `post.update(params)!` does the same for an
existing post.

### The Routes

The generator added one line to `src/routes.glacier`:

```glacier
# The route table. It generates `draw()` for the app and a `*_path()`
# helper for every named route, so links can't point at missing routes.

routes do
  # HTML pages: security headers + CSRF protection.
  pipeline browser = Pipeline.browser()

  scope "/", through: :browser do
    get "/", PagesController.home, as: :home
    resources "/posts", PostsController
  end
end
```

`resources "/posts", PostsController` declares the seven routes of a resource,
and `glacier routes` lists them:

```console
$ glacier routes
GET     /                Pages#home
GET     /posts           Posts#index
GET     /posts/:id       Posts#show
GET     /posts/new       Posts#new
POST    /posts           Posts#create
GET     /posts/:id/edit  Posts#edit
PATCH   /posts/:id       Posts#update
PUT     /posts/:id       Posts#update
DELETE  /posts/:id       Posts#destroy
```

The route table also generates a path helper for each named route:
`posts_path()`, `new_post_path()`, `post_path(id)` and
`edit_post_path(id)`, along with `home_path()` from `as home`. Templates
use these helpers to build links, so a link can't point at a route that
doesn't exist: that would be a build error. The routes sit inside a
`scope "/", through: :browser`, so they pass through the `browser` pipeline,
which adds security headers and checks CSRF tokens on form submissions.
[Routing](routing.html) explains the rest.

### The Views

The index lists the posts:

```html
{% props posts: Array<Post> %}
<h1>Posts</h1>

{% if posts.is_empty() -%}
<p>No posts yet.</p>
{%- else -%}
<table>
  <thead>
    <tr><th>Title</th><th>Body</th><th>Published</th><th></th></tr>
  </thead>
  <tbody>
  {%- for post in posts %}
    <tr>
      <td>{{ post.title }}</td>
      <td>{{ post.body }}</td>
      <td>{{ post.published }}</td>
      <td>
        {{ link_to("Show", post) }}
        {{ link_to("Edit", edit_post_path(post.id)) }}
      </td>
    </tr>
  {%- end %}
  </tbody>
</table>
{%- end %}

<p><a href="{{ new_post_path() }}">New post</a></p>
```

`{% props posts: Array<Post> %}` says this template takes a list of posts,
which is what `render posts` in the `index` action passes.
`link_to("Show", post)` links to a post's own page.

The new and edit pages share a form, `_form.html`. A file whose name
starts with `_` is a partial, rendered from another template:

```html
{% props post: Post %}
{{ form_for(&post) }}
  <p>
    <label>Title<br><input name="title" value="{{ post.text("title") }}"></label>
    {%- for message in post.errors_on("title") %}
    <small class="error">Title {{ message }}</small>
    {%- end %}
  </p>
  <p>
    <label>Body<br><textarea name="body">{{ post.text("body") }}</textarea></label>
    {%- for message in post.errors_on("body") %}
    <small class="error">Body {{ message }}</small>
    {%- end %}
  </p>
  <p>
    <label>Published<br><input type="hidden" name="published" value="false"><input type="checkbox" name="published" value="true"{% if post.text("published") == "true" %} checked{% end %}></label>
    {%- for message in post.errors_on("published") %}
    <small class="error">Published {{ message }}</small>
    {%- end %}
  </p>
  <button>Save</button>
</form>
```

`form_for(&post)` opens the right form for the post: `POST /posts` for a
new one, and `PATCH /posts/:id` for one that's already saved, with the
CSRF token included. `post.text("title")` gives the value to show in the
field, and `post.errors_on("title")` the validation errors for it.
`new.html` renders the form with `{{ Form { post }.render() }}`:

```html
{% props post: Post %}
<h1>New post</h1>

{{ Form { post }.render() }}

<p><a href="{{ posts_path() }}">Back</a></p>
```

### The Tests and the Factory

The scaffold wrote tests for the model and the controller. The model
tests check that a valid post saves and that the title is required:

```glacier
# Tests for the Post model, generated by `glacier generate`.

test "saves a valid post" do
  post = insert(Post)
  assert Post.count()! == 1
  assert post.title == "Example title"
end

test "requires title" do
  saved = Post.create(params_for(Post, title: ""))!
  assert errors_on(saved) =~ "title can't be blank"
end
```

The controller tests make requests to the app, as a browser would, and
check the responses:

```glacier
# Tests for /posts, generated by `glacier generate scaffold`.

# Load a form first, as a browser would, for its CSRF token.
setup do
  get "/posts/new"
end

test "lists posts" do
  insert(Post)
  res = get "/posts"
  assert res.status == :ok
  assert res.body =~ "Example title"
end

test "creates a post" do
  res = post "/posts", params_for(Post)
  assert res.status == :see_other
  assert get(res.location).body =~ "Post created."
end

test "rejects a post without title" do
  res = post "/posts", params_for(Post), title: ""
  assert res.status == :unprocessable_entity
  assert res.body =~ "can&#39;t be blank"
end

test "updates a post" do
  post = insert(Post)
  res = patch post, params_for(Post), title: "Updated"
  assert res.status == :see_other
  assert get(post).body =~ "Updated"
end

test "deletes a post" do
  post = insert(Post)
  res = delete post
  assert res.status == :see_other
  assert get(post).status == :not_found
end
```

Both use the factory in `tests/factories/post.glacier`, which gives valid
fields for a post. `insert(Post)` saves a post with them, and
`params_for(Post)` gives them as form fields; either takes overrides, such
as `title: ""`:

```glacier
# Valid fields for a Post, used by `insert(Post)` and
# `params_for(Post)` in tests. Override any of them per call:
# `insert(Post, title: …)`.

factory Post do
  title: "Example title"
  body: "Example body"
  published: true
end
```

Every test gets a fresh database with the migrations applied, so tests
don't affect each other. The generator listed the new files in
`tests/glacier.rs`, which compiles all the tests into one test binary.

## Running the Tests

Run the tests with `glacier test`:

```console
$ glacier test
running 8 tests
test models::post::requires_title ... ok
test controllers::pages_controller::shows_the_home_page ... ok
test controllers::posts_controller::rejects_a_post_without_title ... ok
test models::post::saves_a_valid_post ... ok
test controllers::posts_controller::lists_posts ... ok
test controllers::posts_controller::creates_a_post ... ok
test controllers::posts_controller::updates_a_post ... ok
test controllers::posts_controller::deletes_a_post ... ok

test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
```

(Cargo's build lines and its summaries of the empty unit-test targets are
cut from this output.)

Each test is named after its folder, file and description:
`controllers::posts_controller::lists_posts`. Anything after `glacier test`
is passed on to `cargo test`, so `glacier test posts_controller` runs only
the tests whose names contain `posts_controller`. `cargo test` runs the
same tests; `glacier test` also points failures at your `.glacier` lines.
[Testing](testing.html) covers writing tests.

## Adding a Validation

Short titles make a poor blog, so require at least five characters. A
validation goes on the field's line in `src/models/post.glacier`; change
the title line to:

```glacier
field title: String, required, length(min: 5)
```

`length(min: 5)` also takes `max:`, or both. Saving a post with a shorter
title now fails, and the form shows "Title is too short (minimum is 5
characters)" beneath the field. A blank title still reports only "can't be
blank".

Add tests for both to `tests/models/post.test.glacier`:

```glacier
test "requires a title of at least five characters" do
  saved = Post.create(params_for(Post, title: "Hi"))!
  assert errors_on(saved) =~ "title is too short (minimum is 5 characters)"
end

test "reports only that a blank title is blank" do
  saved = Post.create(params_for(Post, title: ""))!
  errors = errors_on(saved)
  assert errors =~ "title can't be blank"
  refute errors =~ "too short"
end
```

`Post.create(…)!` validates and inserts a post in one step, and
`errors_on(saved)` lists its errors with their fields, so the tests can
check the messages. `refute` is the opposite of `assert`.

## Adding Comments (updated)

Next, let readers comment on posts. A comment belongs to a post, so it
needs its own model.

### Generating the Comment Model

Only the model is needed this time, since comments will be shown and
added on the post's page:

```console
$ glacier generate model Comment commenter:string body:text post:references
      create  db/migrations/m20261004100649_create_comments.glacier
      create  src/models/comment.glacier
      create  tests/factories/comment.glacier
      create  tests/models/comment.test.glacier
      update  tests/glacier.rs

Next: glacier db migrate
```

`post:references` adds a `post_id` column, which points at the `posts`
table, and a `belongs_to :post` line in the model:

```glacier
module Comment do
  implements Model

  field commenter: String, required
  field body: String, required
  belongs_to :post
end
```

Before migrating, make one change to the migration. A comment's `post_id`
is a foreign key, so the database won't delete a post that still has
comments. `on_delete_cascade()` tells it to delete the comments along with
the post:

```glacier
migration do
  create_table :comments do
    string :commenter, null: false
    text :body, null: false
    references :post, null: false, on_delete: :cascade
    timestamps
  end
end
```

NOTE: This guide deletes a post's comments in the database, with
`on_delete_cascade()`. An association can do it instead, with `has_many
:comments, dependent: :delete_all` (or `:destroy`, which runs each
comment's callbacks); see [Associations](associations.html).

Now run the migration:

```console
$ glacier db migrate
migrated  m20261004100649_create_comments
```

The comment factory makes a post for each comment it inserts, unless the
test gives a `post_id`:

```glacier
# Valid fields for a Comment, used by `insert(Comment)` and
# `params_for(Comment)` in tests. Override any of them per call:
# `insert(Comment, commenter: …)`.

factory Comment do
  commenter: "Example commenter"
  body: "Example body"
  post_id: insert(Post).id
end
```

### Associating the Models

Tell the post about its comments by adding `has_many :comments` to
`src/models/post.glacier`, which now reads:

```glacier
module Post do
  implements Model

  field title: String, required, length(min: 5)
  field body: Option<String>
  field published: bool
  has_many :comments
end
```

Associations in Glacier are never loaded behind your back. To use a post's
comments, ask for them in the query with `preload`; reading
`post.comments` without it panics with a message telling you to add one.
That way an accidental N+1 query shows up the first time the code runs,
rather than as a slow page later.

NOTE: Glacier doesn't load an association the first time you use it,
and association methods such as `post.comments.create(…)`
aren't available yet. Preload an association to read it, and set
`post_id` to link a new comment to its post.

### A Route for Comments

Comments are created from the post's page, at a URL under the post. Add
this route to `src/routes.glacier`, after the posts:

```glacier
post "/posts/:post_id/comments", CommentsController.create, as: :post_comments
```

`:post_id` is captured from the URL, and `as post_comments` names the route,
which generates a `post_comments_path(post_id)` helper.

NOTE: The route is written out in full here. The route table can also nest
resources, which generates the same kind of path and helper; see
[Routing](routing.html#nested-resources).

### The Comments Controller

Create `src/controllers/comments_controller.glacier`:

```glacier
module CommentsController do
  implements Controller

  def create do
    post = Post.find(params[:post_id])!
    comment = Comment.build(params)
    if comment.save()! do
      redirect post, notice: "Comment added."
    else
      redirect post, notice: "A comment needs your name and some text."
    end
  end
end
```

There's no list to add it to: every controller in `src/controllers/` is
found when the app builds.

`params[:post_id]` reads `:post_id` from the URL. Finding the post first
means a comment on a post that doesn't exist is a `404 Not Found`. `params` includes the parameters from the
URL as well as the form, so `Comment.build(params)` gets the
comment's `post_id` as well as its commenter and body. Either way the
action returns to the post's page, with a flash message saying what
happened.

### Showing Comments on the Post

The `show` action needs to load the post's comments. In
`src/controllers/posts_controller.glacier`, change it to:

```glacier
def show(ctx) do
  id = ctx.id()?
  post = Post.where(id: id).preload(:comments).one()!
  title = post.title.clone()
  render post, title: title
end
```

`Post.where(id: id).preload(:comments).one()!` loads the post and its
comments, in two queries. `one()` returns the record, or a `404 Not Found`
if there's none, just as `find` does. The page title is now the post's
title.

The id is read into a variable first because keyword arguments such as
`id: id` compile into a closure, and `?` can't be used inside one. The
title is copied before `render`, because `render post` hands the post
over to the template, after which it can't be read here (Rust's
ownership rules). Get either wrong and the build fails with an error at
the line in this file; [The .glacier Language](glacier_language.html)
shows one.

Finally, change `src/views/posts/show.html` to use the title as its
heading, list the comments, and add a form for a new one:

```html
{% props post: Post %}
<h1>{{ post.title }}</h1>

<dl>
  <dt>Body</dt>
  <dd>{{ post.body }}</dd>
  <dt>Published</dt>
  <dd>{{ post.published }}</dd>
</dl>

<p>
  <a href="{{ edit_post_path(post.id) }}">Edit</a>
  <a href="{{ posts_path() }}">Back</a>
</p>
{{ form_tag(post.to_path(), "delete") }}<button>Delete</button></form>

<h2>Comments</h2>
{% for comment in &post.comments %}
<p><strong>{{ comment.commenter }}</strong>: {{ comment.body }}</p>
{% end %}

{{ form_tag(post_comments_path(post.id), "post") }}
  <p><label>Your name<br><input name="commenter"></label></p>
  <p><label>Comment<br><textarea name="body"></textarea></label></p>
  <button>Add comment</button>
</form>
```

`form_tag(path, "post")` opens a form that submits to `path`, with the CSRF
token included; `post_comments_path(post.id)` is the helper from the new
route. With the server running, open a post and add a comment.

### Testing Comments

Add a controller test for comments in
`tests/controllers/comments_controller.test.glacier`:

```glacier
# Tests for comments on a post.

alias Routes.post_comments_path
alias Routes.post_path

# Load a form first, as a browser would, for its CSRF token.
setup do
  get "/posts/new"
end

test "adds a comment to a post" do
  post = insert(Post)
  res = post post_comments_path(post.id), [commenter: "Ada", body: "Lovely post."]
  assert res.status == :see_other
  page = get(post).body
  assert page =~ "Comment added."
  assert page =~ "Lovely post."
end

test "rejects a comment without a body" do
  post = insert(Post)
  res = post post_comments_path(post.id), [commenter: "Ada", body: ""]
  assert res.status == :see_other
  assert get(post).body =~ "A comment needs your name and some text."
  assert Comment.count()! == 0
end

test "deleting a post deletes its comments" do
  comment = insert(Comment)
  res = delete post_path(comment.post_id)
  assert res.status == :see_other
  assert Comment.count()! == 0
end
```

Route helpers aren't visible in tests until you name them with `alias`,
as the first lines do. One `alias Routes.…` line is in fact enough, because
it also brings in the rest of the routes module. A keyword list such as `[commenter: "Ada", body:
"Lovely post."]` is sent as form fields. The last test checks that
deleting a post deletes its comments, which is the
`on_delete_cascade()` from the migration at work.

List the new test file in `tests/glacier.rs`, next to the generated ones:

```rust
//! The app's `.glacier` tests and factories (`tests/**/*.glacier`),
//! compiled into one test binary; every `.glacier` file under tests/ is found.
//! Run them with `glacier test` or `cargo test`.

glacier::tests!();
```

and run the tests:

```console
$ glacier test
running 15 tests
test models::post::requires_a_title_of_at_least_five_characters ... ok
test models::post::requires_title ... ok
test models::post::reports_only_that_a_blank_title_is_blank ... ok
test models::comment::requires_commenter ... ok
test controllers::pages_controller::shows_the_home_page ... ok
test controllers::posts_controller::rejects_a_post_without_title ... ok
test models::comment::saves_a_valid_comment ... ok
test controllers::posts_controller::lists_posts ... ok
test controllers::posts_controller::creates_a_post ... ok
test controllers::posts_controller::deletes_a_post ... ok
test controllers::posts_controller::updates_a_post ... ok
test controllers::comments_controller::rejects_a_comment_without_a_body ... ok
test controllers::comments_controller::deleting_a_post_deletes_its_comments ... ok
test models::post::saves_a_valid_post ... ok
test controllers::comments_controller::adds_a_comment_to_a_post ... ok

test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
```

(Cargo's build lines and the empty unit-test targets are cut, as before.)

## What's Next

You now have a blog with posts and comments, and tests for both. From
here:

- [The .glacier Language]glacier_language.html explains the syntax
  you've been writing and how it compiles to Rust.
- [Models]models.html, [Associations]associations.html,
  [Querying]querying.html and [Migrations]migrations.html cover
  models and the database.
- [Routing]routing.html, [Controllers]controllers.html and
  [Layouts and Rendering]views.html cover the request side.
- [Testing]testing.html covers tests and factories.
- [Authentication]authentication.html adds sign up and sign in, and
  [JSON APIs]json_apis.html covers APIs and serializers.
- [Background Jobs]background_jobs.html covers work that runs outside
  a request, queued in the database.
- [Working with AI Agents]ai_agents.html connects a coding agent to
  your app through `glacier mcp`.