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

tango

package module
v0.7.0 Latest Latest
Warning

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

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

README

tanGO

Go Reference CI codecov

tanGO is a small, explicit web framework for Go, inspired by Django's ergonomics but built from plain Go structs and interfaces. Write models as structs, get typed migrations, a server-rendered admin and a JSON API from the same code. Scaffolding writes plain Go you own: nothing is generated at runtime, boot never scans your filesystem, and there is no hidden configuration.

Status: pre-1.0, released as v0.x. The Covered API in Versioning and compatibility is what a release promises not to break; a minor release may still break it, only after the deprecation window, and says so in the changelog. Anything marked "planned" or "future" in the docs is not built yet. Read Limitations before depending on tanGO for anything beyond what it says it does.

Quick start

go install github.com/angvp/tango/cmd/tango@latest
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).

To add tanGO to an existing module instead, run go get github.com/angvp/tango. The tango CLI is optional but recommended: it wraps go run/go build with a few conventions the framework expects your main.go to implement.

The tanGO admin listing posts, with search and per-row edit and delete

The server-rendered admin over two models, from examples/api-with-admin.

A route in six lines

The fastest way to see a complete application is examples/jsonapi: a 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. Its 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!"}

tango newapp greetings scaffolds an app stub like this one and prints the line that installs it: adding it to InstalledApps in project/project.go is one line you write yourself, because tanGO never edits your code for you. tango shell opens a Go prompt on your models and database for looking at, or fixing, a few rows.

What you get

  • Explicit lifecycle: Config, App and Registry; nothing boots implicitly.
  • Routing: named routes with reverse lookup, built on chi internally (chi is never part of your imports), and Context helpers for JSON, request binding and redirects.
  • Models from structs: a handful of tags (tango:"pk", tango:"unique", tango:"index", tango:"fk=Other") and no schema DSL.
  • Persistence: db.Store for CRUD, queries and raw SQL on SQLite, PostgreSQL or MySQL, with foreign keys, two-layer validation and cascade delete. See relationships and admin foreign keys.
  • Typed Go migrations: tango makemigrations, migrate and migrate down.
  • Admin: server-rendered list, create, edit and delete with pagination, search, filters and ordering, behind a session-cookie login with CSRF protection and a rate-limited login. Accounts are managed with tango admin create/resetpassword/deactivate.
  • Batteries, each optional: accounts with password reset, email verification and a JSON mode for single-page apps; SMTP mail; storage with local-disk and S3 stores; cache with in-process, Redis and Memcached stores; WebSocket rooms; a token-bucket rate limiter.
  • OpenAPI 3.1 generated from your routes, with Swagger UI (openapi.New, or openapi.Dev to serve it only while developing).
  • A CLI (tango run/check/makemigrations/migrate/newproject/newapp/tui/shell/admin) that wraps ordinary go build and 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.

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, OpenAPI documentation, background jobs, accounts, mail, uploads, caching, rate limiting, realtime, 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.
  • 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.

Contributing

Bug reports and focused pull requests are welcome; CONTRIBUTING.md covers setup and what a change needs, and SECURITY.md covers reporting a vulnerability.

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 (the Redis and Memcached adapter tests need a server and skip without TANGO_TEST_REDIS_URL and TANGO_TEST_MEMCACHE_ADDR): 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, -tango-openapi 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 ErrNoOpenAPI = errors.New("tango: no installed app provides an OpenAPI document; add openapi.New to InstalledApps")

ErrNoOpenAPI is returned by DumpOpenAPI when no installed app provides an OpenAPI document.

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 DumpOpenAPI added in v0.5.0

func DumpOpenAPI(config Config) ([]byte, error)

DumpOpenAPI boots config's registry and returns the OpenAPI document of the first installed app that implements OpenAPIProvider. It is the convention behind the "-tango-openapi" flag.

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 DBPool added in v0.5.0

type DBPool struct {
	MaxOpen     int           // most open connections; 0 is unlimited
	MaxIdle     int           // most idle connections kept for reuse
	MaxLifetime time.Duration // longest a connection is reused
	MaxIdleTime time.Duration // longest a connection may sit idle
}

DBPool sets the limits of the database connection pool. A zero field leaves that setting as the *sql.DB has it (for database/sql: unlimited open connections, 2 idle, no lifetime limits).

type DescribedRoute added in v0.5.0

type DescribedRoute struct {
	Method string
	// Pattern is the full mounted path pattern, with chi placeholders such
	// as {id} or {id:[0-9]+}.
	Pattern string
	// Name is the fully-qualified route name, or "".
	Name string
	// Prefix is the first path segment of the prefix the route was included
	// under, or "".
	Prefix string
	// Doc is the route's RouteDoc, or nil.
	Doc *RouteDoc
}

DescribedRoute is one mounted route as tools see it.

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. When the logger discards Info events it does no work for the request. 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 OpenAPIProvider added in v0.5.0

type OpenAPIProvider interface {
	OpenAPI() ([]byte, error)
}

OpenAPIProvider is implemented by an installed app that can produce the project's OpenAPI document; the openapi package's app does. It is the capability behind `tango openapi`.

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 RouteDoc added in v0.5.0

type RouteDoc struct {
	// Summary is one line naming what the route does.
	Summary string
	// Description is longer text; Markdown is allowed.
	Description string
	// Tags group the route. Without any, the route is tagged with the first
	// segment of the prefix it was included under.
	Tags []string
	// Body is the request body's type, sent as JSON.
	Body any
	// Query is a struct whose fields are the query parameters. A field is
	// named by its `query:"name"` tag, or its lower-cased Go name, and is
	// required when its tag says `query:"name,required"`.
	Query any
	// Responses maps an HTTP status to the response body's type.
	Responses map[int]any
	// Bearer marks the route as needing an `Authorization: Bearer` token.
	Bearer bool
	// Deprecated marks the route as deprecated.
	Deprecated bool
	// Hidden leaves the route out of generated documentation.
	Hidden bool
}

RouteDoc describes one route for tools that read the route table, such as the openapi package. Every field is optional; a route without a RouteDoc is still described by its method, path and name.

Body, Query and the values of Responses are sample values whose Go types describe the shape: pass a zero value or a pointer to one (for example Pet{} or []Pet{}), never a pointer-to-pointer. A nil Responses value means the response has no body.

type RouteOption

type RouteOption func(*Route)

RouteOption customizes a Route during construction.

func Doc added in v0.5.0

func Doc(doc RouteDoc) RouteOption

Doc attaches a RouteDoc to a Route declaration.

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) Described added in v0.5.0

func (rr *RouteRegistry) Described() []DescribedRoute

Described returns every route included so far, in inclusion order, for tools that document or audit the route table. The returned values are copies.

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 WithDBPool added in v0.5.0

func WithDBPool(pool DBPool) ServeOption

WithDBPool sets the database pool limits ServeContext applies to its *sql.DB instead of the default. Fields left zero are not touched, so WithDBPool(DBPool{}) keeps the database/sql behavior as it is.

Without this option ServeContext raises the idle connection limit to 32 when the application has not bounded the pool itself with SetMaxOpenConns; a pool the application bounded is left alone.

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.
i18ntest
Package i18ntest is test support for code that uses the i18n package.
Package i18ntest is test support for code that uses the i18n package.
internal/testhook
Package testhook lets i18n/i18ntest reach the package's global state without exporting a reset from i18n itself.
Package testhook lets i18n/i18ntest reach the package's global state without exporting a reset from i18n itself.
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.
i18nbridge
Package i18nbridge lets packages that already import i18n ask which registered catalog a request selected, without i18n exporting a helper for it.
Package i18nbridge lets packages that already import i18n ask which registered catalog a request selected, without i18n exporting a helper for it.
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.
sqlbind
Package sqlbind writes one query with numbered placeholders ($1, $2, …) for every dialect, so tanGO's own raw SQL (migrations, tests) does not have to be duplicated per placeholder style.
Package sqlbind writes one query with numbered placeholders ($1, $2, …) for every dialect, so tanGO's own raw SQL (migrations, tests) does not have to be duplicated per placeholder style.
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 openapi describes an app's HTTP routes as an OpenAPI 3.1 document.
Package openapi describes an app's HTTP routes as an OpenAPI 3.1 document.
swaggerui
Package swaggerui serves Swagger UI, embedded in the binary, for the openapi package:
Package swaggerui serves Swagger UI, embedded in the binary, for the openapi package:
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, PostgreSQL or MySQL.
Package testdb gives each test a fresh database for the run's Test dialect, so one test suite runs unchanged against SQLite, PostgreSQL or MySQL.

Jump to

Keyboard shortcuts

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