oficina / slides Public

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

Plain git — no account needed to clone.

Download
README.md

manifest/ — the declarative path

Manifest-driven and code-driven are equal, optional paths: declare the resources that are pure CRUD, hand-write the ones that aren't, per resource, in one app. One TOML file here declares a whole screen set — List → Show → Edit/New plus the confirm-page delete flow — generated as real committed code under gen/ by rastrillo generate:

name = "tickettypes" route = "/admin/tickettypes"

[list] columns = [{ field = "Name", kind = "text" }, { field = "Price", kind = "money" }] search = true

[form] basics = [{ name = "Name", required = true }, { name = "Price", kind = "money" }]

The vocabulary covers one flat table per resource, three field kinds, no relations. Add scope = "user" and every generated query is owner-filtered by the session subject — someone else's row answers 404 — for resources a user owns; mount those routes behind sessions.Require. Add store = "mergeable" and the resource lives as event-log streams (tombstone deletes, derive-on-read) instead of a sqlc table — it needs no sqlc tool. Relations or a custom flow: hand-write it.

Adding the first manifest to this app:

  1. go get -tool github.com/sqlc-dev/sqlc/cmd/sqlc (once — the generated store is sqlc input).
  2. rastrillo generate writes gen/ — committed, never hand-edited.
  3. Embed the generated templates. This file goes at the MODULE ROOT, not beside the hand templates: a go:embed pattern cannot contain "..", and gen/templates is not under internal/slides. Call it genassets.go —

package slidesassets

import "embed"

//go:embed gen/templates var GenTemplatesFS embed.FS

rastrillo generate only ever clears and rewrites gen/actions/, never a file sitting beside gen/, so this survives regeneration.

  1. Write the Ctx.Render seam. A generated action cannot call an app-private helper, so it calls Ctx.Render with a page name that is always "<resource>/list", "<resource>/show" or "<resource>/form". This is the whole adapter — internal/slides/genrender.go:

package slides

import ( "bytes" "fmt" "html/template" "io/fs" "net/http" "path" "strings"

slidesassets "slides" "slides/gen/locales"

"amadan.net/rastrillo/rastrillo" "amadan.net/rastrillo/rastrillo/ui" )

// One template per generated screen, keyed "<resource>/<page>". var genPages = buildGenPages()

func buildGenPages() map[string]template.Template { sub, err := fs.Sub(slidesassets.GenTemplatesFS, "gen/templates") if err != nil { panic(err) // embedded at compile time } dirs, err := fs.ReadDir(sub, ".") if err != nil { panic(err) } out := map[string]template.Template{} for , d := range dirs { if !d.IsDir() { continue } files, err := fs.Glob(sub, d.Name()+"/*.html") if err != nil { panic(err) } for , f := range files { base := template.Must(template.New("layout"). Funcs(ui.Funcs(ui.WithT(genT))). Funcs(template.FuncMap{"asset": assets.Path}). ParseFS(ui.Templates(), "*.html")) base = template.Must(base.ParseFS(appFS, "templates/layout.html")) key := d.Name() + "/" + strings.TrimSuffix(path.Base(f), ".html") out[key] = template.Must(base.ParseFS(sub, f)) } } return out }

// The generated templates call (T "resource.<name>.…"); those keys // live in gen/locales, not in ui's default catalog. A missing key // renders as itself, so a typo shows on the page instead of // blanking a sentence. func genT(key string, args ...any) string { if v, ok := locales.BaseCatalog[key]; ok { return v } return key }

func GenRender(ctx *rastrillo.Ctx, w http.ResponseWriter, name string, status int, data any) { t, ok := genPages[name] if !ok { genFail(ctx, w, "rendering "+name, fmt.Errorf("no such page template")) return } var buf bytes.Buffer if err := t.ExecuteTemplate(&buf, "layout", data); err != nil { genFail(ctx, w, "rendering "+name, err) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(status) buf.WriteTo(w) }

func genFail(ctx *rastrillo.Ctx, w http.ResponseWriter, what string, err error) { if ctx != nil && ctx.Logger != nil { ctx.Logger.Error("slides: "+what, "err", err) } http.Error(w, "something went wrong", http.StatusInternalServerError) }

assets and appFS are render.go's, already in this package. The generated screens go through the same layout.html the hand pages use, because a generated content template defines "content" exactly as a hand one does. When a generated screen needs this request's flash or signed-in state, make GenRender a func(r) returning a rastrillo.RenderFunc closed over r and call it from the ctx factory — RenderFunc carries no *http.Request of its own.

  1. Apply the generated store's migrations at boot, or the table the screens read does not exist. In internal/slides/migrations.go:

import ( "fmt"

bookmarksstore "slides/gen/store/bookmarks" )

func genSchemaSet() *migrate.Set { s := new(migrate.Set) for i, stmt := range bookmarksstore.Migrations { s.Add(migrate.Migration{ID: fmt.Sprintf("bookmarksgen/%04dinit", i+1), SQL: stmt}) } return s }

var genSchema = genSchemaSet()

var BootSchema = migrate.Merge(genSchema, Schema)

genSchema goes in BootSchema and never in Schema: Schema is what rastrillo migration check diffs against Models, and a generated table in it reads as one Models forgot.

The ledger trap, stated because it bites later rather than now: the generated Migrations are rewritten verbatim from the TOML on every rastrillo generate, and migrate.Apply treats applied SQL as immutable. Reshape a manifest after the app has booted once and the next boot refuses rather than silently reapplying it — rastrillo migration baseline is the recovery.

  1. Mount the generated router beside the chi router in internal/slides/app.go, and wire its Ctx:

gmux := gen.Router(func(http.Request) rastrillo.Ctx { writer, _ := d.G.DB() return &rastrillo.Ctx{DB: writer, Logger: logger, Actor: rastrillo.Actor{Human: true}, Render: GenRender} }) mux.Handle("/admin/", gmux)

  1. Add rastrillo generate --check to the Makefile's ci target.

Drop to a .go manifest in this directory (package manifest, a var X = rastrillo.Resource{...}) the moment you need a function value — a custom Column.Render, say. To take any single generated screen over by hand, write your own action file at the same computed path under actions/ — the generator skips that one from then on.