CARLOS rastrillo docs

๐Ÿค– rastrillo

amadan.net/rastrillo/rastrillo

The root package: the process entrypoints, the platform contract, localization, fingerprinted assets, the agent vocabulary, and the manifest types.

The shape of an app is the guide to the entrypoints, and Deploying to what they implement.

Entrypoints

func Run(opts Options) error
func Resolve(opts Options) (Options, error)
func Serve(opts Options) error
func Handler(opts Options) (http.Handler, func() error, error)

Run resolves the platform's activation argv and calls Serve. Use it when you let the framework own the database.

When your app opens its own database, use Resolve and Serve instead. Run re-parses argv and repopulates Options.DBPath, so Serve would open a second connection to the file db.Open already owns. Resolve does the same argv and $STATE_DIRECTORY work without serving.

Handler is Serve minus the listener: the whole framework chrome as an http.Handler, for test harnesses. Call the returned cleanup when you are done.

OpenDB is the corrected SQLite opener, exported for tests. In a new app use db.Open, which returns the two-pool handle.

BuildVersion is the version string GET /api/version reports, overridden at build time; it defaults to "dev".

Options

The one configuration struct. The fields worth knowing:

Mux or Router โ€” exactly one must be set. Router builds the mux after the database opens, and is handed the *sql.DB Serve opened, which is how an app puts the framework-opened handle in its per-request Ctx. Serve owns that handle and closes it when Serve returns; do not retain it past that.

Wrap โ€” where your middleware goes: sessions, CSRF, panic pages, authorization. It runs inside the framework's chrome, so GET /healthz and GET /api/version are answered outside it (platform probes never traverse your middleware) and locale-prefix stripping happens before it (your middleware sees the paths your routes match on). Returning nil is a boot error.

DBPath โ€” opens SQLite with the pragma ordering and connection settings that have to be right. Blank it before Serve when your app opened its own handle.

Migrations โ€” applied in order at boot, idempotently. This is the older additive-only mechanism. In a new app use migrate instead.

Socket and Addr โ€” the platform's activation contract. Both empty, Serve checks for a systemd-activated listener (LISTEN_FDS) before falling back to :8080.

Locales, DefaultLocale, LocaleFS โ€” the locale set and its catalogs.

CSP โ€” swaps the baseline content-security policy. The framework sets baseline security headers outermost, and your own Set or Del wins.

NextDue โ€” answers the activator's GET /api/next-due scheduled-wake poll. Unset, the route does not exist.

Sidecar โ€” the app's sidecar pass, run in a loop when the platform spawns <binary> sidecar run. See Agents and tools.

ErrorPage โ€” your own error page, for a failure your app never saw. The framework recovers a panicking handler outermost of all โ€” outside the security headers, so outside your middleware too โ€” logs the stack with a reference, and calls this to render the body at 500. Unset, the response is a plain "Something went wrong.". http.ErrAbortHandler goes straight back up, and a panic after the first byte still leaves a broken page: the status is long gone by then, exactly as in net/http.

type ErrorPageFunc func(w http.ResponseWriter, r *http.Request, status int, ref string)
func NewRef() string

Wire the same function to Ctx.ErrorPage and the 500 a handler answers looks identical to the 500 a panic answers. ui's error-page partial is the body; the callback owns the status code as well, so it calls WriteHeader(status) itself.

ref is what NewRef mints: six lowercase base32 characters over four random bytes, shown on the page and logged beside the error. It is not an id and nothing is stored under it โ€” its whole job is to join what the user saw to what you grep for. The alphabet has no 0, 1, 8 or 9, so a reference read down a phone line cannot be heard as an O, an l or a B. view.Fail mints one too; NewRef is exported for a hand-written handler doing the same job.

Ctx and RenderFunc

type Ctx struct {
	DB     *sql.DB
	Logger *slog.Logger
	Assets *Assets
	Actor  Actor
	Render RenderFunc

	ErrorPage ErrorPageFunc
}

Passed to every generated action: your own wiring, built once by your ctx factory. Per-request state does not live here โ€” identity is sessions.Current(r), locale is LocaleFrom(r).

Render is the manifest system's seam. A generated action cannot call an app-private helper, so it calls ctx.Render โ€” and nil-checks it, answering a logged 500 instead of a nil-pointer panic if you forget to wire it.

ErrorPage is the same seam for the unhappy path: it is what view.Fail, view.NotFound and view.Forbidden render through. Nil is legal, and the helpers answer plain text.

Localization

func NewLocales(codes []string, def string, base Catalog, fsys fs.FS) (*Locales, error)
func T(r *http.Request, key string) string
func Tf(r *http.Request, key string, args ...any) string
func LocaleFrom(r *http.Request) string
func BaseCatalog() Catalog
func BaseCatalogs() map[string]Catalog
func BaseLocales() []string
func BaseKeys() []string
func IsBaseKey(key string) bool
func Dir(locale string) string
func LocaleItems(r *http.Request) []LocaleItem
type LocaleItem struct {
	Code    string
	Name    string
	Href    string
	Current bool
}
const LocaleCookie = "rastrillo_locale"
const LocaleSwitchPath = "/_locale"

T and Tf are the request-scoped lookups actions call; Tf interpolates {name} placeholders. Lookup falls back through the requested locale, the default locale, the framework's catalog for that locale, the framework's English, and finally the key itself, so a missing translation stays visible on the page instead of blank.

Catalog is a flat map[string]string. BaseCatalog carries rastrillo/ui's own English strings and is wired into every served app automatically; your entry for the same key wins. BaseCatalogs, BaseLocales and BaseKeys are the twelve shipped catalogs, their codes, and the rastrillo.ui.* key set an unshipped locale has to translate before generate --check passes; IsBaseKey reports whether one key is the framework's. Dir is the <html dir> value for a locale, "rtl" or "ltr".

LocaleItems builds the language switcher's data for a request โ€” one LocaleItem per declared locale, empty for a one-locale app โ€” and the locale-menu partial renders it as a form posting to LocaleSwitchPath.

Locales is the resolved set: Locales.Codes, Locales.Default, Locales.Has, Locales.FrameworkHas, Locales.T, Locales.Tf, Locales.Middleware, which strips the path prefix before the app's mux sees it, and Locales.SwitchHandler, the POST /_locale route that writes LocaleCookie and redirects โ€” Serve mounts it whenever Options.Locales is set, one locale or twelve.

Localization has the resolution order and the caveats.

Assets

func NewAssets(fsys fs.FS) *Assets
func (a *Assets) Path(name string) string
func (a *Assets) Handler() http.Handler

Fingerprints static files so they cache forever and still update on an ordinary reload. Path maps a file to a URL carrying its content hash; Handler serves it with an immutable Cache-Control. Mount at GET /static/ with no StripPrefix. See Assets.

Icons

func Icon(slug string) template.HTML
func IconSlugs() []string

Icon renders a vendored icon by slug, and an unknown slug renders nothing instead of panicking a page mid-response. IconSlugs is the eleven-slug vocabulary, which is Rastrillo's own rather than any vendor's โ€” Icons explains why that matters.

Agents

type Tool struct{ /* Description, Access, Args, Confirm */ }
type ToolDef struct{ /* the registry entry generate emits */ }
type Access int
const ( ToolRead Access = iota; ToolWrite )

An action opts in with var Tool = rastrillo.Tool{...}. ToolRead observes and never changes state. ToolWrite changes state, so it requires a confirm sentence and an explicitly confirmed call. Access.String is "read" or "write".

type Actor struct {
	Human bool
	Name  string
}
func WithActor(r *http.Request, a Actor) *http.Request
func ActorFromContext(ctx context.Context) (Actor, bool)

Every action's caller is attributed, never anonymous, so an audit trail can say who did what. Actor.String is the audit form, "human" or "agent:<name>", and it is what eventlog stores on every appended event โ€” so a stream says who did what without importing this package.

Agents and tools is the guide.

Manifest types

The Go mirror of a manifest/*.toml resource, for tooling that builds one programmatically. Declaring in TOML is the normal path โ€” Manifests is the guide.

type Resource struct{ /* Name, Route, Store, Scope, List, Form */ }
func (r Resource) Validate() error

Resource.Validate refuses a field colliding with the fixed columns every generated store emits โ€” id, created_at, updated_at, and owner for a scoped resource โ€” instead of producing a confusing table.

List holds Column values and Filter values; Form holds Field values.

The enumerations:

Type Values
Kind Text, Textarea, Money
StoreKind Exclusive (one SQL table), Mergeable (an eventlog stream per record)
ScopeKind Unscoped (the zero value), UserScoped (owner-filtered by session subject)

Read this page as markdown โ€” exact, unstyled, and cheap for an agent to fetch.