oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1# manifest/ — the declarative path
2
3Manifest-driven and code-driven are equal, optional paths: declare the
4resources that are pure CRUD, hand-write the ones that aren't, per
5resource, in one app. One TOML file here declares a whole screen set —
6List → Show → Edit/New plus the confirm-page delete flow — generated
7as real committed code under gen/ by `rastrillo generate`:
8
9 name = "ticket_types"
10 route = "/admin/ticket_types"
11
12 [list]
13 columns = [{ field = "Name", kind = "text" }, { field = "Price", kind = "money" }]
14 search = true
15
16 [form]
17 basics = [{ name = "Name", required = true }, { name = "Price", kind = "money" }]
18
19The vocabulary covers one flat table per resource, three field kinds,
20no relations. Add `scope = "user"` and every generated query is
21owner-filtered by the session subject — someone else's row answers
22404 — for resources a user owns; mount those routes behind
23`sessions.Require`. Add `store = "mergeable"` and the resource lives as
24event-log streams (tombstone deletes, derive-on-read) instead of a
25sqlc table — it needs no sqlc tool. Relations or a custom flow:
26hand-write it.
27
28Adding the first manifest to this app:
29
301. `go get -tool github.com/sqlc-dev/sqlc/cmd/sqlc` (once — the
31 generated store is sqlc input).
322. `rastrillo generate` writes gen/ — committed, never hand-edited.
333. Embed the generated templates. This file goes at the MODULE ROOT,
34 not beside the hand templates: a go:embed pattern cannot contain
35 "..", and gen/templates is not under internal/slides. Call it
36 genassets.go —
37
38 package slidesassets
39
40 import "embed"
41
42 //go:embed gen/templates
43 var GenTemplatesFS embed.FS
44
45 `rastrillo generate` only ever clears and rewrites gen/actions/,
46 never a file sitting beside gen/, so this survives regeneration.
47
484. Write the Ctx.Render seam. A generated action cannot call an
49 app-private helper, so it calls Ctx.Render with a page name that is
50 always "<resource>/list", "<resource>/show" or "<resource>/form".
51 This is the whole adapter — internal/slides/genrender.go:
52
53 package slides
54
55 import (
56 "bytes"
57 "fmt"
58 "html/template"
59 "io/fs"
60 "net/http"
61 "path"
62 "strings"
63
64 slidesassets "slides"
65 "slides/gen/locales"
66
67 "amadan.net/rastrillo/rastrillo"
68 "amadan.net/rastrillo/rastrillo/ui"
69 )
70
71 // One template per generated screen, keyed "<resource>/<page>".
72 var genPages = buildGenPages()
73
74 func buildGenPages() map[string]*template.Template {
75 sub, err := fs.Sub(slidesassets.GenTemplatesFS, "gen/templates")
76 if err != nil {
77 panic(err) // embedded at compile time
78 }
79 dirs, err := fs.ReadDir(sub, ".")
80 if err != nil {
81 panic(err)
82 }
83 out := map[string]*template.Template{}
84 for _, d := range dirs {
85 if !d.IsDir() {
86 continue
87 }
88 files, err := fs.Glob(sub, d.Name()+"/*.html")
89 if err != nil {
90 panic(err)
91 }
92 for _, f := range files {
93 base := template.Must(template.New("layout").
94 Funcs(ui.Funcs(ui.WithT(genT))).
95 Funcs(template.FuncMap{"asset": assets.Path}).
96 ParseFS(ui.Templates(), "*.html"))
97 base = template.Must(base.ParseFS(appFS, "templates/layout.html"))
98 key := d.Name() + "/" + strings.TrimSuffix(path.Base(f), ".html")
99 out[key] = template.Must(base.ParseFS(sub, f))
100 }
101 }
102 return out
103 }
104
105 // The generated templates call (T "resource.<name>.…"); those keys
106 // live in gen/locales, not in ui's default catalog. A missing key
107 // renders as itself, so a typo shows on the page instead of
108 // blanking a sentence.
109 func genT(key string, args ...any) string {
110 if v, ok := locales.BaseCatalog[key]; ok {
111 return v
112 }
113 return key
114 }
115
116 func GenRender(ctx *rastrillo.Ctx, w http.ResponseWriter, name string, status int, data any) {
117 t, ok := genPages[name]
118 if !ok {
119 genFail(ctx, w, "rendering "+name, fmt.Errorf("no such page template"))
120 return
121 }
122 var buf bytes.Buffer
123 if err := t.ExecuteTemplate(&buf, "layout", data); err != nil {
124 genFail(ctx, w, "rendering "+name, err)
125 return
126 }
127 w.Header().Set("Content-Type", "text/html; charset=utf-8")
128 w.WriteHeader(status)
129 buf.WriteTo(w)
130 }
131
132 func genFail(ctx *rastrillo.Ctx, w http.ResponseWriter, what string, err error) {
133 if ctx != nil && ctx.Logger != nil {
134 ctx.Logger.Error("slides: "+what, "err", err)
135 }
136 http.Error(w, "something went wrong", http.StatusInternalServerError)
137 }
138
139 assets and appFS are render.go's, already in this package. The
140 generated screens go through the same layout.html the hand pages
141 use, because a generated content template defines "content" exactly
142 as a hand one does. When a generated screen needs this request's
143 flash or signed-in state, make GenRender a func(r) returning a
144 rastrillo.RenderFunc closed over r and call it from the ctx factory
145 — RenderFunc carries no *http.Request of its own.
146
1475. Apply the generated store's migrations at boot, or the table the
148 screens read does not exist. In internal/slides/migrations.go:
149
150 import (
151 "fmt"
152
153 bookmarksstore "slides/gen/store/bookmarks"
154 )
155
156 func genSchemaSet() *migrate.Set {
157 s := new(migrate.Set)
158 for i, stmt := range bookmarksstore.Migrations {
159 s.Add(migrate.Migration{ID: fmt.Sprintf("bookmarks_gen/%04d_init", i+1), SQL: stmt})
160 }
161 return s
162 }
163
164 var genSchema = genSchemaSet()
165
166 var BootSchema = migrate.Merge(genSchema, Schema)
167
168 genSchema goes in BootSchema and never in Schema: Schema is what
169 `rastrillo migration check` diffs against Models, and a generated
170 table in it reads as one Models forgot.
171
172 The ledger trap, stated because it bites later rather than now: the
173 generated Migrations are rewritten verbatim from the TOML on every
174 `rastrillo generate`, and migrate.Apply treats applied SQL as
175 immutable. Reshape a manifest after the app has booted once and the
176 next boot refuses rather than silently reapplying it —
177 `rastrillo migration baseline` is the recovery.
178
1796. Mount the generated router beside the chi router in
180 internal/slides/app.go, and wire its Ctx:
181
182 gmux := gen.Router(func(*http.Request) *rastrillo.Ctx {
183 writer, _ := d.G.DB()
184 return &rastrillo.Ctx{DB: writer, Logger: logger,
185 Actor: rastrillo.Actor{Human: true}, Render: GenRender}
186 })
187 mux.Handle("/admin/", gmux)
188
1897. Add `rastrillo generate --check` to the Makefile's ci target.
190
191Drop to a .go manifest in this directory (package manifest, a
192`var X = rastrillo.Resource{...}`) the moment you need a function
193value — a custom Column.Render, say. To take any single generated
194screen over by hand, write your own action file at the same computed
195path under actions/ — the generator skips that one from then on.
196