Help us shape the future of Go! Take the 2026 Go Developer Survey

tango

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: MIT Imports: 28 Imported by: 0

README

tanGO

codecov

tanGO is a small, explicit web framework for Go, inspired by Django's ergonomics but built from plain Go structs and interfaces. Scaffolding writes plain Go you own. Nothing is generated at runtime, and boot never scans your filesystem; there's no reflection-heavy magic beyond what's needed to read your model tags, and no hidden configuration.

Status: early development. The public API is still settling. Behavior described in this README and the linked docs reflects what's implemented today; anything explicitly marked "planned" or "future" is not yet built. See Versioning and compatibility for what each release promises not to break, and Limitations before depending on tanGO for anything beyond experimentation.

What you get

  • An explicit app/registry lifecycle (Config, App, Registry) — nothing boots implicitly.
  • A URL dispatcher with named routes and reverse lookup, built on chi internally (chi itself is never part of your imports).
  • Context helpers for JSON responses, request binding, redirects, and raw request/response escape hatches.
  • A model metadata engine that reads plain Go structs and a handful of tags (tango:"pk", tango:"unique", tango:"index", tango:"fk=Other").
  • A minimal db.Store for CRUD and raw SQL against SQLite or PostgreSQL, with minimal ORM foundations: foreign key fields, two-layer validation, and cascade delete — see relationships and admin foreign keys.
  • Generated, typed Go migrations (tango makemigrations/migrate/migrate down) — no separate schema DSL.
  • A server-rendered HTML admin (list/create/edit/delete, pagination, search, filters, the ordering you configure) behind a session-cookie login, with CSRF protection and rate-limited login attempts — accounts are managed via tango admin create/resetpassword/deactivate.
  • A CLI (tango run/check/makemigrations/migrate/newproject/newapp/tui/shell/admin) that wraps ordinary go build/go run rather than replacing them.

Principles

If writing normal Go is simpler than invoking tanGO, write normal Go.

  • Prefer Go primitives before replacing them.
  • Prefer explicit structure over convention magic.
  • Treat JSON and API-only apps as first-class. HTML is optional.
  • The admin is a normal app, not a privileged subsystem.
  • No custom template language.
  • Third-party internals stay behind tanGO's abstractions.

See ADR 0001.

Install

go get github.com/angvp/tango

Install the tango CLI (optional, but recommended — it wraps go run/go build with a few conventions the framework expects your main.go to implement):

go install github.com/angvp/tango/cmd/tango@latest

The smallest runnable application

The fastest way to see a real one is examples/jsonapi: a complete, separate Go module whose one app answers a JSON route. Its main.go is written by hand, so it shows every line of a small application.

To start your own project instead:

tango newproject shop
cd shop
go run .

tango newproject scaffolds a go.mod, a main.go pre-wired for SQLite and the admin app, a project package where your apps are listed, and a shell program for tango shell, and it runs unchanged; run tango makemigrations, tango migrate and tango admin create <username> to create your first admin account, then open /admin/ (the project prints its address when it starts). tango newapp greetings then scaffolds an app stub and prints the line that installs it: adding it to InstalledApps in project/project.go is one line you write yourself — tanGO never edits your code for you. tango shell then opens a Go prompt on your models and database for looking at, or fixing, a few rows.

examples/jsonapi's app registers one named route:

func (App) Register(registry *tango.Registry) error {
	return registry.Routes().Include("/greetings/", tango.URLs{
		tango.Path("GET", "/{name}/", hello, tango.Name("hello")),
	})
}

func hello(ctx *tango.Context) error {
	name := ctx.Param("name")
	return ctx.JSON(200, map[string]string{"message": "Hello, " + name + "!"})
}

Run it:

cd examples/jsonapi
go run .
curl http://localhost:8000/greetings/World/
# {"message":"Hello, World!"}

Where to continue

  • Tutorial — build one application, a bulletin board, from an empty directory to a deployed container in ten parts: routing and JSON views, persistence and migrations, the HTML admin, foreign keys, server-rendered pages, accounts and forms, API tokens and rate limits, a live WebSocket feed, background jobs and graceful shutdown, and testing and deployment.
  • Guides — standalone, task-oriented references: project structure, application architecture, configuration, models and tags, routing, Context, persistence, migrations, admin, app checks, dialect setup, reusable apps, and relationships/admin foreign keys.
  • API reference — the supported public surface, linked to runnable examples. Generated package docs are also available via go doc or pkg.go.dev once published.
  • Examples, each with a README giving its purpose and commands:
    • jsonapi: the smallest JSON application, one hand-written app;
    • api-with-admin: a JSON API and the HTML admin over one set of models, linked by a foreign key;
    • notes-starter: a keepable starter with generated migrations, the admin and rate limiting;
    • jwt-api: a JSON route behind stateless bearer JWTs;
    • realtime-chat: WebSocket rooms with JWT auth and graceful shutdown;
    • accounts-mail: the accounts app's password reset and email verification, with emails printed to the terminal;
    • spa-accounts: the backend of a single-page app: accounts' JSON mode with bearer tokens, a host profile written in the registration transaction, a JSON field edited in the admin, and bounded varchar fields;
    • uploads: a document locker: storage.Upload reads one multipart file, the host keeps the generated key in its own model, and storage.Serve delivers it to its owner, on the local disk;
    • cache: cache.FetchJSON over a bounded in-process cache for a recomputable read, with a versioned prefix and a fail-open hook;
    • reusable-greetings and reusable-greetings-host: a reusable app with contributed migrations and static assets, and a host installing it;
    • board: the tutorial's finished application, deployable as a container.
  • Versioning and compatibility — what each release promises not to break, deprecations, and the changelog.
  • Limitations — non-goals, security boundaries, and dialect differences.

Verifying the docs

Documentation in this repository is checked, not just written: every checked-in example under examples/ is built as its own module in CI, so a stale command or import path in a doc snippet gets caught rather than silently drifting from what actually runs. Run the same check locally before sending a documentation change:

go test ./...

scripts/check.sh runs everything CI runs: gofmt, vet, build, the tests and the offline link check (with lychee, or its pinned Docker image when lychee is not installed).

TestExamplesAreIndependentModulesThatCompile (at the repository root) builds every examples/* module and runs its generated -check flag; the migration-generation tests in internal/cli similarly go build a real generated migrations package rather than only inspecting its source text.

License

MIT — see LICENSE.

Documentation

Overview

Package tango is a Go web framework for database-backed applications, inspired by Django's productivity and kept explicit: no code generation at runtime, no hidden discovery, plain Go you can read top to bottom.

A project is a list of App values in a Config. Each App contributes its models, routes, admin registrations, checks, jobs and lifecycle components to a Registry when the project starts. A project's main function opens the database (see LoadDBConfigFromEnv), lets DispatchFlags handle the app-side flags the tango CLI relies on (-check, -migrate, -tango-dump-models, -tango-status and the admin account flags), and otherwise serves HTTP with ServeContext. Views are plain functions of a Context.

The packages beside this one provide the rest: github.com/angvp/tango/db stores models in SQLite or PostgreSQL, github.com/angvp/tango/migration applies the migrations `tango makemigrations` generates, github.com/angvp/tango/admin is a ready-made admin site, and github.com/angvp/tango/accounts, github.com/angvp/tango/auth, github.com/angvp/tango/realtime and github.com/angvp/tango/ratelimit cover what most applications need next.

The tutorial and guides are at https://tangoframework.com/docs/. What each release promises not to break, and how breaking changes are announced, is at https://tangoframework.com/docs/compatibility/.

Index

Constants

View Source
const (
	EventViewError                 = "tango.http.view_error"
	EventPanic                     = "tango.http.panic"
	EventAccessLog                 = "tango.http.access"
	EventJobFailed                 = "tango.scheduler.job_failed"
	EventRequestIDGenerationFailed = "tango.http.request_id_generation_failed"
)

Variables

View Source
var ErrDuplicateApp = errors.New("tango: duplicate app name")

ErrDuplicateApp is returned by Registry.Register when an app with the same Name() has already been registered.

View Source
var ErrDuplicateJob = errors.New("tango: duplicate job name")

ErrDuplicateJob is returned by Registry.RegisterJob when a Job with the same Name has already been registered.

View Source
var ErrDuplicateLifecycle = errors.New("tango: duplicate lifecycle name")

ErrDuplicateLifecycle is returned by Registry.RegisterLifecycle when a Lifecycle with the same Name has already been registered.

View Source
var ErrDuplicateRouteName = errors.New("tango: duplicate route name")

ErrDuplicateRouteName is returned when two routes resolve to the same fully-qualified name.

View Source
var ErrMalformedPattern = errors.New("tango: malformed route pattern")

ErrMalformedPattern is returned when a route pattern has unbalanced parameter placeholders (e.g. an unclosed "{").

View Source
var ErrMissingParam = errors.New("tango: missing route param")

ErrMissingParam is returned by Reverse when a pattern's placeholder has no corresponding entry in params.

View Source
var ErrRegistrationNotComplete = errors.New("tango: registration not complete")

ErrRegistrationNotComplete is returned by Handler and Reverser when Registry.RunRegistration has not yet completed.

View Source
var ErrReservedLifecycleName = errors.New("tango: reserved lifecycle name")

ErrReservedLifecycleName is returned by Registry.RegisterLifecycle when a Lifecycle's Name collides with the internal name ServeContext reserves for the Scheduler lifecycle it appends itself (see schedulerLifecycleName).

View Source
var ErrUnknownRouteName = errors.New("tango: unknown route name")

ErrUnknownRouteName is returned by Reverse for a route name that was never registered.

Functions

func Check

func Check(config Config) error

Check validates that the configured app registry can boot, compile routes, and pass every AppCheck contributed by installed apps.

func DispatchFlags

func DispatchFlags(config Config, sqlDB *sql.DB, dialect db.Dialect, migrations []migration.Migration) (bool, error)

DispatchFlags handles tanGO's app-side CLI flags. It returns handled=true when a flag path ran and the caller should exit after checking err.

func DumpModels

func DumpModels(config Config) ([]migration.Model, error)

DumpModels boots config's registry and returns its registered models in the dialect-agnostic shape `tango makemigrations` diffs against. This is the documented convention behind the "-tango-dump-models" flag: an app's main.go handling that flag calls DumpModels, JSON-encodes the result to stdout, and exits — mirroring the "-check" flag convention.

func LoadDBConfigFromEnv added in v0.1.0

func LoadDBConfigFromEnv() (db.DSN, error)

LoadDBConfigFromEnv parses TANGO_DB_DSN with db.ParseDSN and returns the dialect, database/sql driver name, and driver data source name to open the app's database with. It defaults to sqlite://app.db when TANGO_DB_DSN is unset. The app still imports and registers the driver itself.

It fails when the retired TANGO_DB_DIALECT is set, so a deployment that still relies on it stops at startup instead of opening the wrong database.

func LoadEnvFile

func LoadEnvFile(path string) error

LoadEnvFile loads simple KEY=VALUE lines from path into the process environment without overriding variables that are already set.

func RequestIDFromContext added in v0.0.2

func RequestIDFromContext(ctx context.Context) (string, bool)

RequestIDFromContext returns the correlation ID installed by RequestID.

func Serve

func Serve(config Config, sqlDB *sql.DB, dialect db.Dialect) error

Serve builds and runs config's registry using sqlDB-backed persistence. dialect must match whatever dialect sqlDB was opened with — callers that also invoke DispatchFlags should pass the same dialect value to both.

Serve blocks forever: it delegates to ServeContext with a background context, which never cancels, so there is no caller-triggered shutdown path. Callers that want graceful shutdown call ServeContext directly with a cancelable context (e.g. one built from signal.NotifyContext).

func ServeContext added in v0.0.2

func ServeContext(ctx context.Context, config Config, sqlDB *sql.DB, dialect db.Dialect, opts ...ServeOption) error

ServeContext builds and runs config's registry the same way Serve does, but additionally starts and stops every registered Lifecycle around the HTTP server's own lifetime, and stops gracefully when ctx is canceled.

Startup runs each Lifecycle.Start (skipping nil) in registration order, each against the long-lived Application context. If the caller cancels ctx while a Start is running, the Application context is canceled promptly so a cooperative Start can observe it and return; ServeContext always waits for that Start to actually finish before rolling back, so no startup goroutine is ever leaked. If a Start fails, or ctx is canceled before every Start has run, every already-started component (Start nil counts as trivially started; the interrupted/failed one does not) is stopped in reverse order and the HTTP server is never started.

Once every component has started, ctx cancellation no longer touches the Application context directly — it stays live through HTTP draining (bounded by WithShutdownTimeout, forcing Server.Close if the drain deadline is hit) and is only canceled afterward, immediately before every started Lifecycle is stopped in reverse order against one shared, independently-timed context — a second budget from the drain phase, but one budget for the whole stop phase, not one per component.

Types

type AccessLogOption added in v0.0.2

type AccessLogOption func(*accessLogConfig)

AccessLogOption configures AccessLogger.

func WithAccessLogger added in v0.0.2

func WithAccessLogger(logger *slog.Logger) AccessLogOption

WithAccessLogger configures AccessLogger's logger. Nil uses slog.Default.

type App

type App interface {
	Name() string
	Register(*Registry) error
}

App is the narrow contract every tanGO application unit satisfies. This interface is stable: no breaking changes are planned, and apps are not expected to implement it by hand (use NewApp).

A distributable third-party app package should expose one exported App value or constructor at its package root, e.g.:

var App = tango.NewApp("widgets", registerWidgets)

or, if the app needs configuration:

func New(cfg Config) tango.App { ... }

Importers then reference it directly: InstalledApps: []tango.App{widgets.App} or InstalledApps: []tango.App{widgets.New(cfg)}. No other framework integration is required.

An App may optionally also implement Checker to contribute advisory checks; it is never required to.

An app that ships its own templates or static assets embeds and mounts them itself, via its own embed.FS and a route registered through Include/Routes() inside its Register callback — the same pattern the admin package uses. There is no separate framework-level mechanism for this.

func NewApp

func NewApp(name string, registerFn func(*Registry) error) App

NewApp constructs an App from a name and registration function.

type AppCheck

type AppCheck struct {
	Description string
	Err         error
}

AppCheck is one advisory finding contributed by an App. A nil Err means the check passed. Named AppCheck (not Check) to avoid colliding with the existing package-level Check(Config) error function.

type Checker

type Checker interface {
	Checks() []AppCheck
}

Checker is an optional capability an App may implement to contribute advisory checks surfaced by Registry.Checks (and, in turn, `tango check`). Implementing Checker is never required.

type Config

type Config struct {
	InstalledApps []App
	Addr          string
	Middleware    []Middleware
	// MiddlewareScope decides what Middleware wraps: each matched route,
	// or the whole router, Unmatched requests included. The zero value is
	// the framework's default, MiddlewareScopeRoutes.
	MiddlewareScope MiddlewareScope
	// NotFound, if set, answers requests whose path no route matches, in
	// place of the router's plain-text 404. MethodNotAllowed, if set,
	// answers requests whose path a route matches but not their method, in
	// place of the router's empty 405; tanGO sets the Allow header before
	// it runs. Both run like any View (an error gets the generic 500).
	// Under MiddlewareScopeAll they run inside Middleware and are observed
	// as route "(unmatched)"; under MiddlewareScopeRoutes they run outside
	// it and are not observed.
	NotFound         View
	MethodNotAllowed View
}

Config is the minimal application configuration.

func LoadConfigFromEnv

func LoadConfigFromEnv(opts ...ConfigOption) Config

LoadConfigFromEnv returns a Config populated from environment variables: Addr is TANGO_ADDR, defaulting to :8000. WithPortFromEnv adds PORT as a fallback before the default.

type ConfigOption added in v0.2.0

type ConfigOption func(*envConfig)

ConfigOption changes how LoadConfigFromEnv reads the environment.

func WithPortFromEnv added in v0.2.0

func WithPortFromEnv() ConfigOption

WithPortFromEnv makes LoadConfigFromEnv fall back to the PORT variable that hosting platforms (Railway, Heroku, Cloud Run, ...) set: the address is TANGO_ADDR if set, else ":"+PORT if PORT is set, else :8000.

type Context

type Context struct {
	// contains filtered or unexported fields
}

Context wraps an HTTP request/response pair with high-value helpers. Escape hatches (Request, ResponseWriter) are available when the helpers below aren't enough.

func (*Context) Bind

func (c *Context) Bind(dst any) error

Bind decodes the request body as JSON into dst.

func (*Context) Context

func (c *Context) Context() context.Context

Context returns the wrapped request's standard context.Context. Context does not itself implement context.Context.

func (*Context) HTML

func (c *Context) HTML(status int, tmpl *template.Template, name string, data any) error

HTML renders the named template within tmpl (via tmpl.ExecuteTemplate) against data, writing it as an HTML response with the given status code. The render is buffered: nothing is written to the response until execution fully succeeds, so a template error is returned like any other view error (a clean framework-generated 500) instead of a response that already committed status and headers followed by a truncated or malformed body. tmpl must already be parsed; HTML has no opinion on template parsing, caching, or file layout.

func (*Context) JSON

func (c *Context) JSON(status int, payload any) error

JSON writes payload as a JSON response body with the given status code.

func (*Context) Logger added in v0.0.2

func (c *Context) Logger() *slog.Logger

Logger returns the request-scoped structured logger.

func (*Context) Param

func (c *Context) Param(name string) string

Param returns a named path parameter's value, or "" if absent.

func (*Context) Query

func (c *Context) Query(name string) string

Query returns a named URL query parameter's value, or "" if absent.

func (*Context) Redirect

func (c *Context) Redirect(url string) error

Redirect writes an HTTP redirect response to url.

func (*Context) Request

func (c *Context) Request() *http.Request

Request returns the wrapped *http.Request.

func (*Context) ResponseWriter

func (c *Context) ResponseWriter() http.ResponseWriter

ResponseWriter returns the wrapped http.ResponseWriter.

type IncludeOption

type IncludeOption func(*includeConfig)

IncludeOption customizes one Include call.

func WithMiddleware

func WithMiddleware(middleware ...Middleware) IncludeOption

WithMiddleware attaches Middleware to every Route mounted by one Include call. Middleware runs before *Context is constructed; the first middleware passed is outermost.

type Job added in v0.0.2

type Job struct {
	Name     string
	Interval time.Duration
	Run      func(context.Context) error
}

Job is one named, interval-based unit of recurring work. RegisterJob records Jobs declaratively; execution is owned by ServeContext's scheduler.

type Lifecycle added in v0.0.2

type Lifecycle struct {
	Name  string
	Start func(context.Context) error
	Stop  func(context.Context) error
}

Lifecycle is a named component with optional startup and shutdown hooks, run by ServeContext around the HTTP server's own lifetime. Start and Stop are each optional (nil is skipped), but at least one of them must be set.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware wraps a raw net/http handler before a *tango.Context exists. Use it for cross-cutting HTTP concerns such as recovery, logging, request IDs, CORS, or compression. Context-aware behavior such as auth redirects belongs in a View wrapper instead.

func AccessLogger added in v0.0.2

func AccessLogger(opts ...AccessLogOption) Middleware

AccessLogger logs one structured event for each request it wraps. As global middleware, it sees Unmatched requests (the router's own 404 and 405) only under MiddlewareScopeAll.

func MaxBodySize added in v0.2.0

func MaxBodySize(n int64) Middleware

MaxBodySize returns Middleware that caps request bodies at n bytes. A request whose Content-Length already exceeds n gets 413 before the View runs. Otherwise the body is limited with http.MaxBytesReader: a View that reads past the limit gets an *http.MaxBytesError, which, if the View returns it unhandled, is answered with 413 instead of the generic 500. A View that handles the error itself keeps control of its response.

When more than one body limit applies to a request, the most restrictive wins: middleware runs outermost first, so a limit closer to the View can keep or lower an outer one, never raise it. Leave a route that needs larger bodies outside the stricter limit, for example in its own route group. n must be positive; MaxBodySize panics otherwise.

func Recoverer

func Recoverer(opts ...RecoveryOption) Middleware

func RequestID added in v0.0.2

func RequestID(opts ...RequestIDOption) Middleware

RequestID attaches a correlation ID to the request context and response.

type MiddlewareScope added in v0.2.0

type MiddlewareScope int

MiddlewareScope is what Config.Middleware wraps.

const (
	// MiddlewareScopeDefault is the framework's default scope, currently
	// MiddlewareScopeRoutes. A later release may change the default, with a
	// minor release's notice in the changelog; set a scope explicitly to
	// keep it.
	MiddlewareScopeDefault MiddlewareScope = iota
	// MiddlewareScopeRoutes wraps each matched route. Unmatched requests,
	// which the router answers itself with 404 or 405, pass no global
	// middleware and are neither logged nor counted.
	MiddlewareScopeRoutes
	// MiddlewareScopeAll wraps the whole router, so Unmatched requests pass
	// global middleware too. A request's route is resolved before global
	// middleware runs, from the request as it arrived: an Unmatched request
	// is reported as route "(unmatched)". Middleware that rewrites the method
	// or path so the router dispatches elsewhere is unsupported, and global
	// middleware sees no route parameters: the router hasn't dispatched yet.
	MiddlewareScopeAll
)

type Params

type Params map[string]string

Params holds substitution values for a reverse URL lookup.

type ProjectStatus

type ProjectStatus struct {
	RegistrationOK    bool   `json:"registrationOk"`
	RegistrationError string `json:"registrationError,omitempty"`
	DatabaseReachable bool   `json:"databaseReachable"`
	DatabaseError     string `json:"databaseError,omitempty"`
	MigrationsTotal   int    `json:"migrationsTotal"`
	MigrationsApplied int    `json:"migrationsApplied"`
	MigrationsPending int    `json:"migrationsPending"`
}

ProjectStatus is a snapshot of a project's registration, database, and migration state, produced by Status. It is the payload behind the documented "-tango-status" flag convention, which `tango tui` reads.

func Status

func Status(ctx context.Context, config Config, sqlDB *sql.DB, dialect db.Dialect, migrations []migration.Migration) ProjectStatus

Status reports config's registration status, sqlDB's reachability, and migrations' applied/pending counts, without writing to the database or filesystem. This is the documented convention behind the "-tango-status" flag: an app's main.go handling that flag calls Status, JSON-encodes the result to stdout, and exits — mirroring "-check" and "-tango-dump-models".

type RecoveryOption added in v0.0.2

type RecoveryOption func(*recoveryConfig)

RecoveryOption configures Recoverer.

func WithRecoveryLogger added in v0.0.2

func WithRecoveryLogger(logger *slog.Logger) RecoveryOption

WithRecoveryLogger configures Recoverer's logger. Nil uses slog.Default.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is the shared structure apps register into during boot.

func BuildRegistry

func BuildRegistry(config Config) (*Registry, error)

BuildRegistry registers Config.InstalledApps into a new Registry.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty, ready-to-use Registry.

func (*Registry) Admin

func (r *Registry) Admin() *adminregistry.Registry

Admin returns the admin sub-registry, creating it on first use.

func (*Registry) Checks

func (r *Registry) Checks() []AppCheck

Checks returns every advisory AppCheck contributed by installed apps that implement Checker, in InstalledApps order. Apps that do not implement Checker are skipped without error. Checks is only meaningful after RunRegistration has completed.

func (*Registry) Lifecycles added in v0.0.2

func (r *Registry) Lifecycles() []Lifecycle

Lifecycles returns every registered Lifecycle in registration order.

func (*Registry) Models

func (r *Registry) Models() *model.Registry

Models returns the model sub-registry, creating it on first use.

func (*Registry) Register

func (r *Registry) Register(app App) error

Register adds app to the registry. It returns an error wrapping ErrDuplicateApp if an app with the same Name() has already been registered, without invoking any app's Register callback.

func (*Registry) RegisterJob added in v0.0.2

func (r *Registry) RegisterJob(job Job) error

RegisterJob records job for execution when the application is served. It never invokes Run. Name must not be blank, Interval must be positive, Run must be non-nil, and Name must be unique within the Registry.

func (*Registry) RegisterLifecycle added in v0.0.2

func (r *Registry) RegisterLifecycle(lifecycle Lifecycle) error

RegisterLifecycle adds lifecycle to the registry. It rejects a blank (or whitespace-only) Name, rejects a Lifecycle with both Start and Stop nil, and returns an error wrapping ErrDuplicateLifecycle if a Lifecycle with the same Name has already been registered.

func (*Registry) Routes

func (r *Registry) Routes() *RouteRegistry

Routes returns the route sub-registry, creating it on first use.

func (*Registry) RunRegistration

func (r *Registry) RunRegistration() error

RunRegistration invokes each registered app's Register callback exactly once, in the order the apps were added, passing this Registry to each. It stops and returns the first error encountered, and does not invoke any subsequent app's callback.

func (*Registry) SetStore

func (r *Registry) SetStore(store *db.Store)

SetStore stores the persistence store for apps that need database access. It also wires this Registry's model registry into store (store.UseModels), so Delete cascades across every registered model's foreign keys with no extra call needed by app code — see db.Store.UseModels.

func (*Registry) Store

func (r *Registry) Store() (*db.Store, bool)

Store returns the configured persistence store, if one has been set.

type RequestIDOption added in v0.0.2

type RequestIDOption func(*requestIDConfig)

RequestIDOption configures RequestID.

func WithRequestIDLogger added in v0.0.2

func WithRequestIDLogger(logger *slog.Logger) RequestIDOption

WithRequestIDLogger configures RequestID's failure logger. Nil uses slog.Default.

func WithTrustedHeader added in v0.0.2

func WithTrustedHeader(name string) RequestIDOption

WithTrustedHeader allows RequestID to accept a validated ID from name. It panics when name is not a valid HTTP field name.

type Reverser

type Reverser interface {
	Reverse(name string, params Params) (string, error)
}

Reverser builds a URL for a fully-qualified route name.

type Route

type Route struct {
	// contains filtered or unexported fields
}

Route declares how an HTTP method and path pattern map to a View.

func Path

func Path(method, pattern string, view View, opts ...RouteOption) Route

Path constructs a Route declaration without executing its View.

func (Route) Method

func (r Route) Method() string

Method returns the route's HTTP method.

func (Route) Middleware

func (r Route) Middleware() []Middleware

Middleware returns the route-scoped Middleware attached to this Route.

func (Route) Name

func (r Route) Name() string

Name returns the route's optional name.

func (Route) Pattern

func (r Route) Pattern() string

Pattern returns the route's path pattern.

func (Route) View

func (r Route) View() View

View returns the route's view.

type RouteOption

type RouteOption func(*Route)

RouteOption customizes a Route during construction.

func Name

func Name(name string) RouteOption

Name attaches a name to a Route declaration.

func Use

func Use(middleware ...Middleware) RouteOption

Use attaches Middleware to one Route. Middleware runs before *Context is constructed; the first middleware passed is outermost.

type RouteRegistry

type RouteRegistry struct {
	// contains filtered or unexported fields
}

RouteRegistry is the sub-registry apps contribute routes to, mirroring the shape of Registry's other sub-APIs.

func (*RouteRegistry) Handler

func (rr *RouteRegistry) Handler() (http.Handler, error)

Handler compiles the full route tree, contributed by every app's Include call, into a plain http.Handler. It returns ErrRegistrationNotComplete if Registry.RunRegistration has not yet completed, and a non-nil error if two different Include calls produced the same fully-qualified route name.

func (*RouteRegistry) Include

func (rr *RouteRegistry) Include(prefix string, routes []Route, opts ...IncludeOption) error

Include mounts routes under prefix, namespace-qualifying any named route and normalizing trailing slashes so Include("/users", ...) and Include("/users/", ...) behave identically. It fails fast on a malformed pattern or a duplicate fully-qualified name within this call's routes, registering none of them in that case. Cross-call duplicate detection is deferred to Handler/Reverser compile time.

func (*RouteRegistry) IncludedNames

func (rr *RouteRegistry) IncludedNames() []string

IncludedNames returns the fully-qualified names of every named route included so far, in inclusion order. It exists to make Include's namespacing/normalization behavior testable before Handler/Reverser land.

func (*RouteRegistry) IncludedPatterns

func (rr *RouteRegistry) IncludedPatterns() []string

IncludedPatterns returns the mounted path pattern of every route included so far, in inclusion order.

func (*RouteRegistry) Reverser

func (rr *RouteRegistry) Reverser() (Reverser, error)

Reverser compiles the full route tree (the same compilation Handler performs) and returns a Reverser for building URLs from route names. It returns ErrRegistrationNotComplete if Registry.RunRegistration has not yet completed.

type ServeOption added in v0.0.2

type ServeOption func(*serveConfig)

ServeOption configures ServeContext.

func WithLogger added in v0.0.2

func WithLogger(logger *slog.Logger) ServeOption

WithLogger configures framework-owned structured logging.

func WithReadHeaderTimeout added in v0.2.0

func WithReadHeaderTimeout(d time.Duration) ServeOption

WithReadHeaderTimeout bounds how long a client may take to send a request's headers before the server closes the connection. It defaults to 10 seconds; 0 disables it. d must not be negative.

func WithRecorder added in v0.0.2

func WithRecorder(recorder observability.Recorder) ServeOption

WithRecorder configures automatic HTTP and scheduler metrics. A nil recorder is normalized to the no-op implementation.

func WithShutdownTimeout added in v0.0.2

func WithShutdownTimeout(d time.Duration) ServeOption

WithShutdownTimeout bounds each phase of ServeContext's graceful shutdown — draining in-flight HTTP requests, and running every registered Lifecycle.Stop hook — as two independent budgets, not one shared deadline. It defaults to 15 seconds. d must be positive.

type URLs

type URLs []Route

URLs is a plain list of Route values.

type View

type View func(*Context) error

View is the narrow handler contract every route dispatches to.

Directories

Path Synopsis
Package accounts is tanGO's first-party, optional, installable reusable app: a ready-made HTML email/password register/login/logout flow, built by composing github.com/angvp/tango/auth's primitives rather than adding to them.
Package accounts is tanGO's first-party, optional, installable reusable app: a ready-made HTML email/password register/login/logout flow, built by composing github.com/angvp/tango/auth's primitives rather than adding to them.
Package admin is tanGO's installable admin site: list, search, create, edit and delete pages for every model an application registers with it, behind a session-cookie login with CSRF protection and rate-limited login attempts.
Package admin is tanGO's installable admin site: list, search, create, edit and delete pages for every model an application registers with it, behind a session-cookie login with CSRF protection and rate-limited login attempts.
Package auth provides composable authentication primitives for tanGO applications.
Package auth provides composable authentication primitives for tanGO applications.
jwt
Package jwt provides explicit, stateless HS256 access-token primitives.
Package jwt provides explicit, stateless HS256 access-token primitives.
Package cache is a small, explicit cache for expensive reads that are safe to recompute.
Package cache is a small, explicit cache for expensive reads that are safe to recompute.
local
Package local is a bounded in-process cache.Store, for development, tests and single-process deployments.
Package local is a bounded in-process cache.Store, for development, tests and single-process deployments.
memcache module
redis module
Package cachetest holds the conformance suite every cache.Store must pass.
Package cachetest holds the conformance suite every cache.Store must pass.
cmd
tango command
Package db stores tanGO models in SQLite or PostgreSQL.
Package db stores tanGO models in SQLite or PostgreSQL.
Package i18n provides tanGO's small text-translation primitives.
Package i18n provides tanGO's small text-translation primitives.
internal
cli
clientip
Package clientip names the client behind an HTTP request, for the limiters that count per client.
Package clientip names the client behind an HTTP request, for the limiters that count per client.
clientip/clientiptest
Package clientiptest holds the cases every user of internal/clientip is tested against, so the public ratelimit.RemoteIPKey and the login limiters' resolver cannot disagree about what a request's client is.
Package clientiptest holds the cases every user of internal/clientip is tested against, so the public ratelimit.RemoteIPKey and the login limiters' resolver cannot disagree about what a request's client is.
migrationcompat
Package migrationcompat holds, under one directory per tanGO version, the migration files that version's own `tango makemigrations` wrote, and lists them for the Generated-file contract tests.
Package migrationcompat holds, under one directory per tanGO version, the migration files that version's own `tango makemigrations` wrote, and lists them for the Generated-file contract tests.
migrationtest
Package migrationtest builds a test's tables the way an app gets them: from the framework's own migrations, generated by migration.Diff and applied by migration.ApplyPending, in a database from testdb.Open.
Package migrationtest builds a test's tables the way an app gets them: from the framework's own migrations, generated by migration.Diff and applied by migration.ApplyPending, in a database from testdb.Open.
observabilitysafe
Package observabilitysafe isolates failures in host-supplied observability implementations from the application operation being observed.
Package observabilitysafe isolates failures in host-supplied observability implementations from the application operation being observed.
releasecheck
Package releasecheck decides whether a pushed tag may become a tanGO release.
Package releasecheck decides whether a pushed tag may become a tanGO release.
releasecheck/cmd/releasecheck command
Command releasecheck decides whether a pushed tag may become a tanGO release, and writes its release notes.
Command releasecheck decides whether a pushed tag may become a tanGO release, and writes its release notes.
shellcore
Package shellcore is the evaluator, formatter and built-in helpers behind the public shell package.
Package shellcore is the evaluator, formatter and built-in helpers behind the public shell package.
sqlident
Package sqlident quotes SQL identifiers (table, column and index names) for the SQL tanGO generates, so a model or field named after a reserved word ("user", "order", "group") works on every dialect.
Package sqlident quotes SQL identifiers (table, column and index names) for the SQL tanGO generates, so a model or field named after a reserved word ("user", "order", "group") works on every dialect.
storagekit
Package storagekit holds the few helpers every storage.Store implementation shares, so the adapters (storage/local, storage/s3 and storagetest's memory store) cannot drift apart on them.
Package storagekit holds the few helpers every storage.Store implementation shares, so the adapters (storage/local, storage/s3 and storagetest's memory store) cannot drift apart on them.
Package mail sends outgoing email.
Package mail sends outgoing email.
mailtest
Package mailtest provides a mail.Sender for tests: it records the messages it is given instead of delivering them.
Package mailtest provides a mail.Sender for tests: it records the messages it is given instead of delivering them.
Package migration applies and rolls back the schema migrations `tango makemigrations` generates.
Package migration applies and rolls back the schema migrations `tango makemigrations` generates.
Package model reads tanGO models: plain Go structs whose fields, through their tango:"…" struct tags, describe a table's columns, primary key, unique and indexed columns, and foreign keys.
Package model reads tanGO models: plain Go structs whose fields, through their tango:"…" struct tags, describe a table's columns, primary key, unique and indexed columns, and foreign keys.
Package observability defines tanGO's minimal backend-neutral metrics contract.
Package observability defines tanGO's minimal backend-neutral metrics contract.
Package ratelimit is a reusable, host-composable request rate limiter, independent of auth, admin, accounts, and realtime.
Package ratelimit is a reusable, host-composable request rate limiter, independent of auth, admin, accounts, and realtime.
Package realtime provides transport-neutral, single-process room coordination.
Package realtime provides transport-neutral, single-process room coordination.
websocket
Package websocket adapts realtime.Coordinator to an HTTP WebSocket upgrade, wrapping github.com/coder/websocket.
Package websocket adapts realtime.Coordinator to an HTTP WebSocket upgrade, wrapping github.com/coder/websocket.
Package shell is the interactive console behind `tango shell`: a Go interpreter booted with a project's configuration, database and model registry, without serving HTTP.
Package shell is the interactive console behind `tango shell`: a Go interpreter booted with a project's configuration, database and model registry, without serving HTTP.
Package storage keeps uploaded bytes outside the database.
Package storage keeps uploaded bytes outside the database.
local
Package local is a storage.Store on the local filesystem, for development, tests and single-host deployments with a persistent volume.
Package local is a storage.Store on the local filesystem, for development, tests and single-host deployments with a persistent volume.
gcs module
s3 module
Package storagetest holds the conformance suite every storage.Store must pass, and NewMemory, an in-memory Store for tests that need no disk and no credentials.
Package storagetest holds the conformance suite every storage.Store must pass, and NewMemory, an in-memory Store for tests that need no disk and no credentials.
Package testdb gives each test a fresh database for the run's Test dialect, so one test suite runs unchanged against SQLite or PostgreSQL.
Package testdb gives each test a fresh database for the run's Test dialect, so one test suite runs unchanged against SQLite or PostgreSQL.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL