Skip to content

Context Processors ​

Introduction ​

Most pages show something that isn't theirs: the signed-in user's name in the header, a menu item only some people may use, the message "Saved" after a form, the current path in a breadcrumb. Passing those down as props through every layout and component is tedious, and calling auth.Current(ctx) in each one computes the same thing again and again.

A context processor is a function that computes such values once per request. Each exported field of the struct it returns becomes a template variable that any template of the app reads by name:

html
<header>
  if user != nil {
    <span>{user.Name}</span>
  }
  <small>{request.Path}</small>
</header>

user and request aren't props, fields or locals here. They are variables of the processors main lists, typed like any Go value: user is an *auth.Identity, a typo is a compile error with a suggestion, and the editor completes user. and says where user comes from.

Listing Processors ​

Every processor an app uses is listed once, in main, with web.ContextProcessors. Nothing registers itself on import, and the framework has no list of its own: a processor not in the list doesn't run, and its variables don't exist in templates.

go
package main

import (
	"log"

	"example.com/app/pages"
	"example.com/app/users"
	"github.com/vuka-lang/ui/v2"
	"github.com/vuka-lang/web/v2"
	"github.com/vuka-lang/web/v2/auth"
)

func main() {
	app := web.New(
		pages.Routes(),
		web.ContextProcessors(
			web.RequestContext,
			auth.Context,
			web.MessagesContext,
			web.CSRFContext,
			ui.PageContext,
			users.Context,
		),
	)
	log.Fatal(app.Run(""))
}

vuka new writes this list with the built-in processors, so they are visible from the start; remove auth.Context and user and perms are gone from templates.

The built-in processors are ordinary processors, written as yours are:

ProcessorVariables
web.RequestContextrequest: the path, query, parameters and route
auth.Contextuser (*auth.Identity, nil for an anonymous visitor), perms (auth.Perms, with Has), actor (the real user while impersonating, else nil)
web.MessagesContextmessages ([]web.Message: Level, Text): what web.Flash added, each shown once
web.CSRFContextcsrfToken: a token for a form posted from another origin, checked with web.ValidCSRFToken
ui.PageContextpage (ui.PageHeader): Title, Crumbs of the page being rendered

request is a web.RequestInfo: Path, Query (url.Values), Params (the path parameters by name), Name (the route's handler) and Pattern.

Writing a Processor ​

A processor is a plain function of a package of the app (not of main): func(ctx context.Context, deps…) (S, error), S a struct. The parameters after ctx come from dependency injection, as a handler's do. Each exported field of S is a variable, its name the field's in lower camel case: Me is me, CSRFToken is csrfToken.

go
// users/context.go
package users

type User struct {
	ID   string
	Name string
}

type Service struct{ db *sql.DB }

func (s *Service) ByID(ctx context.Context, id string) (*User, error) {
	u := &User{ID: id}
	err := s.db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = ?", id).Scan(&u.Name)
	return u, err
}

type Vars struct{ Me *User }

func Context(ctx context.Context, s *Service) (Vars, error) {
	id := auth.Current(ctx)
	if id == nil {
		return Vars{}, nil
	}
	u, err := s.ByID(ctx, id.ID)
	return Vars{Me: u}, err
}

List it in main — users.Context — and every template reads me:

html
if me != nil {
  <a href="/account">{me.Name}</a>
}

A processor runs only when a template of the render reads one of its variables, at most once per request or live event; every later read shares its result. A processor decides what an anonymous visitor gets: here me is nil.

NOTE

Two processors giving the same variable name fail the build, naming both (E0421): no list resolves a name by its order.

Reading Variables in Templates ​

A bare name in a shape's markup, in its <script lang="go">, or in a .vuka file reads a variable when it names nothing else there: no field, local, parameter, package or declaration of the package. The compiler reads the web.ContextProcessors call in main, as it reads folder routes, so the variable has its type:

html
for _, m := range messages {
  <p class={m.Level}>{m.Text}</p>
}
<form method="post" action="https://pay.example.com/checkout">
  <input type="hidden" name="csrf" value={csrfToken} />
</form>

Markup reads a variable through the render's ctx. A function of a .vuka file, or a method of a shape's script, reads one when it takes ctx context.Context (E0423 when it doesn't):

vuka
func (h *Header) greeting(ctx context.Context) string {
	if me == nil {
		return "Welcome"
	}
	return "Hello, " + me.Name
}

A name a letter or two from a variable's is a typo: E0424 suggests the variable and its processor.

Where Names Are Read ​

Every package of the module that a package main imports, directly or through others, reads variables by name: pages, layouts, components, modules and live components alike. The folder pages under pages/ — route groups such as (admin) and parameter folders such as [id] included — count as imported by each main passing pages.Routes(), the generated route.go importing them.

A processor's own package, and every package it imports, reads none of its variables by name: the read would import the processor's package back, an import cycle. They read the other processors' variables, and the explicit web.Var where they must. A package reading a processor's variables is compiled after the processor's package; when processors' packages and their readers need each other that way, no order works and E0428 names the processor and the packages between.

A package imported by two mains — cmd/web and cmd/admin, say — reads a name only when every main that lists processors gives it, with one type; otherwise E0422 names the main lacking it. A main that lists none, such as a command running migrations, isn't counted.

Packages of other modules, such as a component library, can't see the app's main, so they read nothing by name. They read a variable explicitly, and vuka vet checks the read against the app's list (E0427):

go
me := web.Var[*users.User](ctx, "me")

One Spelling in Vuka Files ​

A vuka file reads what a processor gives through its variable, in markup and in Go alike: perms.Has("x") rather than auth.Can(ctx, "x"), user rather than auth.Current(ctx), request.Path rather than web.Request(ctx).URL.Path. Markup tracks the variable, and in an event or Mount it is as current as the call. An event checks its permission the same way, or declares it with @auth.Requires:

html
<script lang="go">
func (d *Index) Approve(ctx context.Context) error {
	if !perms.Has("orders.approve") {
		return web.Forbidden
	}
	return nil
}

@auth.Requires("orders.ship")
func (d *Index) Ship(ctx context.Context) error { return nil }
</script>

if perms.Has("orders.approve") {
  <button onClick={Approve}>Approve</button>
}
WhereRead
.vuka.html markup and script, .vuka filesperms.Has("x"), user, actor, request
plain .go packages: services, jobs, librariesauth.Can(ctx, "x"), auth.Current(ctx), web.Request(ctx), or @auth.Requires

vuka vet reports the direct calls in vuka files (W0402) — saying to list the processor when main doesn't — and the editor offers the rewrite; from 3.0 it is a compile error there.

Names a Template Declares ​

A field, a prop or a local named like a variable wins, as in Go's scoping: the template reads it. That is how a caller passes a value of its own to a component that otherwise shows the variable. vuka vet says so (W0401), so a field named user by accident doesn't hide the signed-in user unnoticed.

Reading a Variable in Go ​

Go code reads a variable with web.Var, naming its type:

go
me := web.Var[*users.User](ctx, "me")

A bare name in a template compiles to exactly this call. web.Var returns the zero value when the read fails — no such variable, another type, the processor's error — and logs why; web.VarErr returns the error:

go
me, err := web.VarErr[*users.User](ctx, "me")
if err != nil {
	return err
}
fmt.Println("signed in as", me.Name)

Live Pages and Layouts ​

On a live page, each read is recorded against the component that made it, and so is what the processor itself reads: the URL for request, the identity and its permissions for auth.Context. When one of those changes, only the components that read the variable render again.

A layout that stays while the visitor moves between pages can show request.Path or page.Title in its markup: that part renders again on each navigation and the layout stays. A layout that stores a variable in its Load, which runs once, reloads instead (W0703).

An event that changes what a processor computed — the user renamed themselves — says so, and the processor runs again on the next read; the components showing me render again if it changed:

go
web.RefreshVars(ctx, "me")

NOTE

Template variables are read-only. URL state changes in events, with web.Query.Set(ctx, "tab", key) or a field bound with @ui.URL("tab"); request then follows.

Giving Other Values ​

web.WithVars gives a processor's variables other values below a context: the struct is the processor's result type, and the processor doesn't run there. A part of a page shows another user's card this way:

go
other := &users.User{ID: "7", Name: "Grace"}
ctx = web.WithVars(ctx, users.Vars{Me: other})

In tests, viewtest.With(users.Vars{Me: u}) (package github.com/vuka-lang/web/v2/viewtest) is an app option doing the same for every page, and viewtest.Context(ctx, users.Vars{Me: u}) for one context.

Rendering Outside a Request ​

An email, a job or a unit test renders with a context that has no request and no processors. Reading a variable there fails — web.VarErr says there is no template variable here — and web.Var returns the zero value. Give the values the template reads:

go
u := &users.User{ID: "1", Name: "Ada"}
ctx = web.WithVars(context.Background(), users.Vars{Me: u})
ctx = web.WithVars(ctx, web.RequestVars{Request: web.RequestInfo{Path: "/orders"}})
html, err := ui.String(ctx, OrderShipped{Order: 42})
if err != nil {
	return err
}
fmt.Println(html)

Guarding a Processor ​

A processor whose variables only a signed-in visitor may read is listed with the gates of a route:

go
opt := web.ContextProcessors(
	web.Auth(users.Context, auth.Required),
)

A read by an anonymous visitor then sends them down the sign-in path — a redirect to the login page, or 401 — instead of running the processor.

What templates show by permission is covered in permissions in templates.

The Browser ​

No processor's variables reach islands or the page's scripts unless the processor is listed with web.Browser:

go
opt := web.ContextProcessors(
	web.Browser(web.RequestContext),
	web.Browser(web.Personal(users.Context)),
)

A processor reading the signed-in visitor gives values that are theirs alone. web.Browser refuses it unless it is also marked web.Personal (E0426), and a permission list (auth.Perms) never reaches the browser.

In the Editor ​

vuka lsp completes the variables a file may read, and their fields after a dot; hover shows a variable's type and the processor it comes from (from users.Context (users/context.go)); go to definition jumps to the processor and to its entry in web.ContextProcessors. Variables have a semantic token type of their own, vukaContextVar. See editors.

Diagnostics ​

Code
E0421two processors give one variable
E0422a variable not every main gives
E0423a variable read without a ctx
E0424a typo for a variable
E0425a list entry that is no processor
E0426personal values sent to the browser
E0427a web.Var read the app doesn't give
E0428processors' packages and their readers needing each other
W0401a name shadowing a variable
W0402auth.Can, auth.Current or web.Request where perms, user or request would do
W0403a control hidden by a permission its event doesn't check

Released under the MIT License. Vuka builds on Go and its tools, which are made by the Go Authors. Vuka is not affiliated with Google or the Go project. Go is a trademark of Google LLC.