Documentation
¶
Overview ¶
Package internal provides the core types and implementation for the Forge framework.
This package is internal and should not be used directly. Import "github.com/dmitrymomot/forge" instead, which re-exports the public API.
Core Types ¶
The package defines the fundamental types that users interact with:
- App: Orchestrates the application lifecycle, HTTP routing, and graceful shutdown
- Context: Provides request/response access, identity, RBAC, and helper methods
- Router: Interface handlers use to declare routes with HTTP methods and grouping
- Handler: Interface implemented by types that declare routes on a router
- HandlerFunc: Signature for individual route handlers that return errors
- Middleware: Wraps handlers to add cross-cutting concerns like auth or logging
- ErrorHandler: Custom error handling function for handler errors
- Permission: Named permission string for RBAC checks
- RolePermissions: Maps role names to their granted permissions
- RoleExtractorFunc: Extracts the current user's role from the request context
Context as context.Context ¶
Context embeds context.Context, so it can be passed directly to any function that expects a standard library context. The Deadline, Done, Err, and Value methods delegate to the underlying request context:
func (h *Handler) getUser(c forge.Context) error {
// Pass c directly to database calls, HTTP clients, etc.
user, err := h.repo.GetUser(c, userID)
if err != nil {
return err
}
return c.JSON(200, user)
}
Application Structure ¶
Create an application with New() and configure it using options:
app := internal.New(internal.AppConfig{},
internal.WithHandlers(authHandler, pageHandler),
internal.WithMiddleware(loggingMiddleware, panicMiddleware),
internal.WithHealthChecks(internal.HealthCheck("db", dbCheck)),
)
Handler Pattern ¶
Handlers implement the Handler interface and declare routes:
type AuthHandler struct {
repo *repository.Queries
}
func (h *AuthHandler) Routes(r internal.Router) {
r.GET("/login", h.showLogin)
r.POST("/login", h.handleLogin)
}
Handlers receive dependencies via constructor injection, not context helpers. This keeps handler logic explicit and testable.
Identity Methods ¶
Context provides convenience methods for checking user identity. These are shortcuts over the session system — they load the session lazily on first access and return safe defaults when no session is configured:
- UserID() string: Returns the authenticated user's ID, or empty string
- IsAuthenticated() bool: Returns true if a user is associated with the session
- IsCurrentUser(id string) bool: Returns true if the given ID matches the current user
Example:
func (h *Handler) showProfile(c internal.Context) error {
if !c.IsAuthenticated() {
return c.Redirect(302, "/login")
}
user, err := h.repo.GetUser(c, c.UserID())
if err != nil {
return err
}
return c.JSON(200, user)
}
Role-Based Access Control (RBAC) ¶
Configure RBAC with WithRoles, which accepts a permission map and a role extractor function. The role extractor is called lazily on the first Can() call and the result is cached for the lifetime of the request:
app := internal.New(internal.AppConfig{},
internal.WithRoles(
internal.RolePermissions{
"admin": {"users.read", "users.write", "billing.manage"},
"member": {"users.read"},
},
func(c internal.Context) string {
role, _ := c.SessionValue("role")
if s, ok := role.(string); ok {
return s
}
return ""
},
),
)
Check permissions in handlers with Can():
func (h *Handler) deleteUser(c internal.Context) error {
if !c.Can("users.write") {
return c.Error(403, "forbidden")
}
// proceed with deletion...
}
Retrieve the resolved role with Role():
func (h *Handler) dashboard(c internal.Context) error {
role := c.Role() // e.g. "admin", "viewer", ""
// ...
}
Can() and Role() return safe defaults if RBAC is not configured (false and empty string respectively). Both share the same lazy extraction and per-request caching — the role extractor is called at most once.
Request Handling ¶
Each request receives a Context with comprehensive helper methods:
func (h *AuthHandler) handleLogin(c internal.Context) error {
var form LoginForm
validationErrs, err := c.Bind(&form)
if err != nil {
return c.Error(http.StatusBadRequest, "invalid form")
}
if len(validationErrs) > 0 {
return c.Render(http.StatusUnprocessableEntity, loginTemplate)
}
// Process login...
return c.JSON(http.StatusOK, result)
}
Middleware ¶
Middleware wraps handlers to add cross-cutting concerns:
func LoggingMiddleware(next internal.HandlerFunc) internal.HandlerFunc {
return func(c internal.Context) error {
start := time.Now()
err := next(c)
duration := time.Since(start)
c.LogInfo("request processed", "duration", duration)
return err
}
}
Middleware can inspect/modify the request, short-circuit processing, or wrap the response. They have full access to the Context and can be route-specific or global.
Error Handling ¶
Errors returned from handlers trigger the ErrorHandler:
func customErrorHandler(c internal.Context, err error) error {
if statusCode := getStatusCode(err); statusCode > 0 {
return c.Error(statusCode, err.Error())
}
c.LogError("unhandled error", "error", err)
return c.Error(http.StatusInternalServerError, "internal server error")
}
Server Runtime ¶
Start the server with Run() or use Run() for multi-domain deployments:
// Single app
err := app.Run(internal.RunConfig{Address: ":8080"}, internal.WithRunLogger(log))
// Multi-domain
err := internal.Run(internal.RunConfig{Address: ":8080"},
internal.WithDomain("api.example.com", apiApp),
internal.WithDomain("*.example.com", tenantApp),
)
Features ¶
The Context provides helpers for common request patterns:
- JSON encoding/decoding
- Form binding with validation and sanitization
- Cookie management (plain, signed, encrypted, flash)
- Session management (load, create, authenticate, destroy)
- Identity shortcuts (UserID, IsAuthenticated, IsCurrentUser)
- Role-based access control (Can with lazy role extraction)
- Standard library context.Context compatibility
- HTMX-aware response rendering
- File upload/download with configurable storage
- Background job enqueueing
- Structured logging with request-scoped values
- Domain and subdomain extraction
- Custom context values
Design Principles ¶
- No magic: Explicit code, no reflection, no service containers
- Flat handlers: Business logic in handlers, extract to services only when shared
- Constructor injection: All dependencies visible in main.go
- No context helpers: Packages receive values via parameters
- Framework, not boilerplate: Provides utilities, not business logic
See the forge package documentation for the public API and usage examples.
Index ¶
- Variables
- func ContextValue[T any](c Context, key any) T
- func IsHTTPError(err error) bool
- func LoadConfig(dst any) error
- func Param[T ~string | ~int | ~int64 | ~float64 | ~bool](c Context, name string) T
- func Query[T ~string | ~int | ~int64 | ~float64 | ~bool](c Context, name string) T
- func QueryDefault[T ~string | ~int | ~int64 | ~float64 | ~bool](c Context, name string, defaultValue T) T
- func Run(cfg RunConfig, opts ...RunOption) error
- type App
- type AppConfig
- type CheckFunc
- type Component
- type Context
- type ErrorHandler
- type Extractor
- type ExtractorSource
- func FromBearerToken() ExtractorSource
- func FromCookie(name string) ExtractorSource
- func FromCookieEncrypted(name string) ExtractorSource
- func FromCookieSigned(name string) ExtractorSource
- func FromForm(name string) ExtractorSource
- func FromHeader(name string) ExtractorSource
- func FromParam(name string) ExtractorSource
- func FromQuery(name string) ExtractorSource
- func FromSession(key string) ExtractorSource
- type FingerprintMode
- type FingerprintStrictness
- type HTTPError
- func AsHTTPError(err error) *HTTPError
- func ErrBadRequest(message string, opts ...HTTPErrorOption) *HTTPError
- func ErrConflict(message string, opts ...HTTPErrorOption) *HTTPError
- func ErrForbidden(message string, opts ...HTTPErrorOption) *HTTPError
- func ErrInternal(message string, opts ...HTTPErrorOption) *HTTPError
- func ErrNotFound(message string, opts ...HTTPErrorOption) *HTTPError
- func ErrServiceUnavailable(message string, opts ...HTTPErrorOption) *HTTPError
- func ErrTooManyRequests(message string, opts ...HTTPErrorOption) *HTTPError
- func ErrUnauthorized(message string, opts ...HTTPErrorOption) *HTTPError
- func ErrUnprocessable(message string, opts ...HTTPErrorOption) *HTTPError
- func NewHTTPError(code int, message string) *HTTPError
- type HTTPErrorOption
- type Handler
- type HandlerFunc
- type HealthCheckOption
- type JWTClaimsKey
- type JobEnqueuer
- type JobManager
- type LanguageKey
- type Middleware
- type Option
- func WithCookieConfig(cfg cookie.Config) Option
- func WithCustomLogger(l *slog.Logger) Option
- func WithErrorHandler(h ErrorHandler) Option
- func WithHandlers(h ...Handler) Option
- func WithHealthChecks(checks ...HealthCheckOption) Option
- func WithJobEnqueuer(pool *pgxpool.Pool, opts ...job.EnqueuerOption) Option
- func WithJobWorker(pool *pgxpool.Pool, cfg job.Config, opts ...job.Option) Option
- func WithJobs(pool *pgxpool.Pool, cfg job.Config, opts ...job.Option) Option
- func WithLogger(component string, extractors ...logger.ContextExtractor) Option
- func WithMethodNotAllowedHandler(h HandlerFunc) Option
- func WithMiddleware(mw ...Middleware) Option
- func WithNotFoundHandler(h HandlerFunc) Option
- func WithRoles(permissions RolePermissions, extractor RoleExtractorFunc) Option
- func WithSSEKeepAlive(d time.Duration) Option
- func WithSession(store Store, opts ...SessionOption) Option
- func WithStaticFiles(pattern string, fsys fs.FS, subDir string) Option
- func WithStorage(s storage.Storage) Option
- type Permission
- type ResponseWriter
- func (w *ResponseWriter) Flush()
- func (w *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)
- func (w *ResponseWriter) OnBeforeWrite(fn func())
- func (w *ResponseWriter) Push(target string, opts *http.PushOptions) error
- func (w *ResponseWriter) Seal()
- func (w *ResponseWriter) Size() int64
- func (w *ResponseWriter) Status() int
- func (w *ResponseWriter) Unwrap() http.ResponseWriter
- func (w *ResponseWriter) Write(b []byte) (int, error)
- func (w *ResponseWriter) WriteHeader(code int)
- func (w *ResponseWriter) Written() bool
- type RoleExtractorFunc
- type RolePermissions
- type Router
- type RunConfig
- type RunOption
- func WithContext(ctx context.Context) RunOption
- func WithDomain(pattern string, app *App) RunOption
- func WithFallback(app *App) RunOption
- func WithRunLogger(l *slog.Logger) RunOption
- func WithShutdownHook(fn func(context.Context) error) RunOption
- func WithStartupHook(fn func(context.Context) error) RunOption
- type SSEEvent
- type Session
- func (s *Session) ClearDirty()
- func (s *Session) ClearNew()
- func (s *Session) DeleteValue(key string)
- func (s *Session) GetValue(key string) (any, bool)
- func (s *Session) IsAuthenticated() bool
- func (s *Session) IsDirty() bool
- func (s *Session) IsExpired() bool
- func (s *Session) IsNew() bool
- func (s *Session) MarkDirty()
- func (s *Session) SetValue(key string, val any)
- type SessionOption
- func WithMaxSessionsPerUser(max int) SessionOption
- func WithSessionCookieName(name string) SessionOption
- func WithSessionFingerprint(mode FingerprintMode, strictness FingerprintStrictness) SessionOption
- func WithSessionLogger(logger *slog.Logger) SessionOption
- func WithSessionTTL(ttl time.Duration) SessionOption
- func WithSessionTouchThreshold(threshold time.Duration) SessionOption
- type Store
- type TranslatorKey
- type ValidationErrors
Constants ¶
This section is empty.
Variables ¶
var ( // ErrSessionNotConfigured is returned when session functionality is used // but WithSession was not configured on the app. ErrSessionNotConfigured = errors.New("session: not configured") // ErrSessionNotFound is returned when a session does not exist. ErrSessionNotFound = errors.New("session: not found") // ErrSessionExpired is returned when a session has expired. ErrSessionExpired = errors.New("session: expired") // ErrSessionInvalidToken is returned when a session token is invalid. ErrSessionInvalidToken = errors.New("session: invalid token") // ErrSessionFingerprintMismatch is returned when session fingerprint validation fails. // This may indicate a session hijacking attempt. ErrSessionFingerprintMismatch = errors.New("session: fingerprint mismatch") )
Functions ¶
func ContextValue ¶
func IsHTTPError ¶
func LoadConfig ¶
LoadConfig parses environment variables into dst using struct tags. It loads .env from the working directory automatically via godotenv. Struct fields use `env:"KEY"`, `envDefault:"value"`, and `envSeparator:","` tags to declare their bindings.
func QueryDefault ¶
func QueryDefault[T ~string | ~int | ~int64 | ~float64 | ~bool](c Context, name string, defaultValue T) T
QueryDefault retrieves a typed query parameter with a default value. Returns defaultValue if the parameter is empty or cannot be parsed.
Types ¶
type App ¶
type App struct {
// contains filtered or unexported fields
}
App orchestrates the application lifecycle. It manages HTTP routing, middleware, and graceful shutdown. App is immutable after creation - all configuration is done via New().
func New ¶
New creates a new application with the given config and options. The App is immutable after creation.
func (*App) JobWorker ¶
func (a *App) JobWorker() *JobManager
JobWorker returns the job worker if configured, nil otherwise. This is used internally for multi-domain routing to collect workers.
func (*App) Router ¶
Router returns the underlying chi.Router for the App. This is used internally for composing multi-domain routing.
type AppConfig ¶
type AppConfig struct {
BaseDomain string `env:"BASE_DOMAIN"`
RequestTimeout time.Duration `env:"REQUEST_TIMEOUT"`
}
AppConfig holds externally configurable application settings.
type CheckFunc ¶
CheckFunc is the standard health check function signature. This matches existing healthcheck closures in pg, redis, cqrs, and jobs packages.
type Component ¶
Component is the interface for renderable templates. This is compatible with templ.Component.
type Context ¶
type Context interface {
context.Context
// Request returns the underlying *http.Request.
Request() *http.Request
// Response returns the underlying http.ResponseWriter.
Response() http.ResponseWriter
// Context returns the request's context.Context.
Context() context.Context
// Param returns the URL parameter value by name.
// Returns empty string if the parameter doesn't exist.
Param(name string) string
// Query returns the query parameter value by name.
// Returns empty string if the parameter doesn't exist.
Query(name string) string
// QueryDefault returns the query parameter value or a default.
QueryDefault(name, defaultValue string) string
// Form returns the form value by name.
// Calls ParseForm/ParseMultipartForm internally on first access.
// Returns empty string if the field doesn't exist.
Form(name string) string
// FormFile returns the first file for the given form key.
// Returns the file, its header, and any error.
FormFile(name string) (multipart.File, *multipart.FileHeader, error)
// UserID returns the authenticated user's ID from the session.
// Loads the session lazily on first call.
// Returns empty string if no session, no session manager, or no user.
UserID() string
// IsAuthenticated returns true if a user is associated with the session.
IsAuthenticated() bool
// IsCurrentUser returns true if the authenticated user's ID matches the given id.
IsCurrentUser(id string) bool
// Can returns true if the current user's role grants the given permission.
// Returns false if RBAC is not configured or the user has no matching permission.
// The role is extracted lazily and cached for the lifetime of the request.
Can(permission Permission) bool
// Role returns the current user's role string.
// The role is extracted lazily and cached for the lifetime of the request.
// Returns empty string if RBAC is not configured.
Role() string
// Domain returns the normalized domain from the request Host header.
// Strips port, handles IPv6, and converts to lowercase.
Domain() string
// Subdomain extracts the subdomain from the request.
// Uses the base domain configured via AppConfig.BaseDomain.
// Returns empty string if no base domain configured or host doesn't match.
Subdomain() string
// Header returns the request header value by name.
Header(name string) string
// SetHeader sets a response header.
SetHeader(name, value string)
// JSON writes a JSON response with the given status code.
JSON(code int, v any) error
// String writes a plain text response with the given status code.
String(code int, s string) error
// NoContent writes a response with no body.
NoContent(code int) error
// Redirect redirects to the given URL with the given status code.
// Handles both regular HTTP redirects and HTMX requests.
Redirect(code int, url string) error
// Error creates and returns an HTTPError without writing a response.
// The error should be returned from the handler to trigger the error handler.
Error(code int, message string, opts ...HTTPErrorOption) *HTTPError
// IsHTMX returns true if the request originated from HTMX.
IsHTMX() bool
// Render renders a component with the given status code.
// For HTMX requests: always uses HTTP 200 (HTMX requires 2xx for swapping).
// For regular requests: uses the provided status code.
// Compatible with templ.Component.
// Optional render options configure HTMX response headers.
Render(code int, component Component, opts ...htmx.RenderOption) error
// RenderPartial renders different components based on request type.
// For HTMX requests: renders partial with HTTP 200.
// For regular requests: renders fullPage with the provided status code.
// Optional render options configure HTMX response headers (only applied for HTMX requests).
RenderPartial(code int, fullPage, partial Component, opts ...htmx.RenderOption) error
// Bind binds form data, sanitizes, and validates into a struct.
// Returns validation errors separately from system errors.
Bind(v any) (ValidationErrors, error)
// BindQuery binds query parameters, sanitizes, and validates into a struct.
// Returns validation errors separately from system errors.
BindQuery(v any) (ValidationErrors, error)
// BindJSON binds JSON body, sanitizes, and validates into a struct.
// Returns validation errors separately from system errors.
BindJSON(v any) (ValidationErrors, error)
// Written returns true if a response has already been written.
Written() bool
// Logger returns the logger for advanced usage.
Logger() *slog.Logger
// LogDebug logs a debug message with optional attributes.
LogDebug(msg string, attrs ...any)
// LogInfo logs an info message with optional attributes.
LogInfo(msg string, attrs ...any)
// LogWarn logs a warning message with optional attributes.
LogWarn(msg string, attrs ...any)
// LogError logs an error message with optional attributes.
LogError(msg string, attrs ...any)
// Set stores a value in the request context.
// The value can be retrieved using Get or from c.Context().Value(key).
Set(key any, value any)
// Get retrieves a value from the request context.
// Returns nil if the key is not found.
Get(key any) any
// Cookie returns a plain cookie value.
Cookie(name string) (string, error)
// SetCookie sets a plain cookie.
SetCookie(name, value string, maxAge int)
// DeleteCookie removes a cookie.
DeleteCookie(name string)
// CookieSigned returns a signed cookie value.
// Returns cookie.ErrNoSecret if no secret is configured.
CookieSigned(name string) (string, error)
// SetCookieSigned sets a signed cookie.
// Returns cookie.ErrNoSecret if no secret is configured.
SetCookieSigned(name, value string, maxAge int) error
// CookieEncrypted returns an encrypted cookie value.
// Returns cookie.ErrNoSecret if no secret is configured.
CookieEncrypted(name string) (string, error)
// SetCookieEncrypted sets an encrypted cookie.
// Returns cookie.ErrNoSecret if no secret is configured.
SetCookieEncrypted(name, value string, maxAge int) error
// Flash reads and deletes a flash message.
// Returns cookie.ErrNoSecret if no secret is configured.
Flash(key string, dest any) error
// SetFlash sets a flash message.
// Returns cookie.ErrNoSecret if no secret is configured.
SetFlash(key string, value any) error
// Session returns the current session, auto-creating an anonymous session if needed.
// Returns ErrSessionNotConfigured if WithSession was not called.
Session() (*Session, error)
// InitSession creates a new session for this request.
// Idempotent - no-op if session already exists.
// Returns ErrSessionNotConfigured if WithSession was not called.
InitSession() error
// AuthenticateSession associates a user with the session and rotates the token.
// Auto-creates a session if one doesn't exist.
// Returns ErrSessionNotConfigured if WithSession was not called.
AuthenticateSession(userID string) error
// SessionValue retrieves a value from the session.
// Auto-creates an anonymous session if needed.
// Returns ErrSessionNotConfigured if WithSession was not called.
SessionValue(key string) (any, error)
// SetSessionValue stores a value in the session.
// Auto-creates an anonymous session if needed.
// Returns ErrSessionNotConfigured if WithSession was not called.
SetSessionValue(key string, val any) error
// DeleteSessionValue removes a value from the session.
// Auto-creates an anonymous session if needed.
// Returns ErrSessionNotConfigured if WithSession was not called.
DeleteSessionValue(key string) error
// DestroySession removes the session and clears the cookie.
// Returns ErrSessionNotConfigured if WithSession was not called.
DestroySession() error
// DestroyOtherSessions removes all sessions for the current user except this one.
// No-op if the session is not authenticated.
// Returns ErrSessionNotConfigured if WithSession was not called.
DestroyOtherSessions() error
// DestroyAllSessions removes all sessions for the given user.
// Returns ErrSessionNotConfigured if WithSession was not called.
DestroyAllSessions(userID string) error
// ListSessions returns all sessions for the given user.
// Returns ErrSessionNotConfigured if WithSession was not called.
ListSessions(userID string) ([]*Session, error)
// ResponseWriter returns the underlying ResponseWriter for advanced usage.
// Returns nil if not using the wrapped response writer.
ResponseWriter() *ResponseWriter
// Enqueue adds a job to the queue for background processing.
// Returns job.ErrSessionNotConfigured if WithJobs was not called.
// Returns job.ErrUnknownTask if the task name is not registered.
Enqueue(name string, payload any, opts ...job.EnqueueOption) error
// EnqueueTx adds a job to the queue within a transaction.
// The job is only visible after the transaction commits.
// Returns job.ErrSessionNotConfigured if WithJobs was not called.
// Returns job.ErrUnknownTask if the task name is not registered.
EnqueueTx(tx pgx.Tx, name string, payload any, opts ...job.EnqueueOption) error
// Storage returns the configured storage client.
// Returns storage.ErrSessionNotConfigured if WithStorage was not called.
Storage() (storage.Storage, error)
// Upload extracts the named form file from the request and stores it.
// Returns storage.ErrSessionNotConfigured if WithStorage was not called.
Upload(field string, opts ...storage.Option) (*storage.FileInfo, error)
// UploadFromURL downloads a file from sourceURL and stores it.
// Returns storage.ErrSessionNotConfigured if WithStorage was not called.
UploadFromURL(sourceURL string, opts ...storage.Option) (*storage.FileInfo, error)
// Download retrieves a file from storage.
// Returns storage.ErrSessionNotConfigured if WithStorage was not called.
Download(key string) (io.ReadCloser, error)
// DeleteFile removes a file from storage.
// Returns storage.ErrSessionNotConfigured if WithStorage was not called.
DeleteFile(key string) error
// FileURL generates a URL for accessing the file.
// Returns storage.ErrSessionNotConfigured if WithStorage was not called.
FileURL(key string, opts ...storage.URLOption) (string, error)
// T translates a key using the Translator stored in context by the I18n middleware.
// Returns the key itself if no translator is in context.
T(key string, placeholders ...i18n.M) string
// Tn translates a key with pluralization using the Translator stored in context.
// Returns the key itself if no translator is in context.
Tn(key string, n int, placeholders ...i18n.M) string
// Language returns the resolved language from the I18n middleware.
// Returns an empty string if no translator is in context.
Language() string
// FormatNumber formats a number using locale-specific separators.
// Falls back to fmt.Sprintf if no translator is in context.
FormatNumber(n float64) string
// FormatCurrency formats a currency amount using locale-specific formatting.
// Falls back to fmt.Sprintf if no translator is in context.
FormatCurrency(amount float64) string
// FormatPercent formats a percentage using locale-specific formatting.
// Falls back to fmt.Sprintf if no translator is in context.
FormatPercent(n float64) string
// FormatDate formats a date using locale-specific formatting.
// Falls back to time.Format if no translator is in context.
FormatDate(date time.Time) string
// FormatTime formats a time value using locale-specific formatting.
// Falls back to time.Format if no translator is in context.
FormatTime(t time.Time) string
// FormatDateTime formats a datetime using locale-specific formatting.
// Falls back to time.Format if no translator is in context.
FormatDateTime(datetime time.Time) string
// SSE streams Server-Sent Events from the channel to the client.
// Blocks until the channel closes or the request context is cancelled.
// Sends keepalive comments at the configured interval (default 30s).
// Returns nil on clean exit, or an error on marshal/render failure.
SSE(events <-chan SSEEvent) error
}
Context provides request/response access and helper methods. It also implements context.Context by delegating to the underlying request context.
type ErrorHandler ¶
ErrorHandler handles errors returned from handlers.
type Extractor ¶
type Extractor struct {
// contains filtered or unexported fields
}
Extractor tries multiple sources in order and returns the first match.
func NewExtractor ¶
func NewExtractor(sources ...ExtractorSource) Extractor
NewExtractor creates an Extractor that tries the given sources in order.
type ExtractorSource ¶
ExtractorSource extracts a value from the request context. Returns the value and true if found, or ("", false) if not present.
func FromBearerToken ¶
func FromBearerToken() ExtractorSource
FromBearerToken returns a source that reads a Bearer token from the Authorization header. Uses case-insensitive comparison on the "Bearer " prefix.
func FromCookie ¶
func FromCookie(name string) ExtractorSource
FromCookie returns a source that reads from a plain cookie.
func FromCookieEncrypted ¶
func FromCookieEncrypted(name string) ExtractorSource
FromCookieEncrypted returns a source that reads from an encrypted cookie.
func FromCookieSigned ¶
func FromCookieSigned(name string) ExtractorSource
FromCookieSigned returns a source that reads from a signed cookie.
func FromForm ¶
func FromForm(name string) ExtractorSource
FromForm returns a source that reads from a form field.
func FromHeader ¶
func FromHeader(name string) ExtractorSource
FromHeader returns a source that reads from a request header.
func FromParam ¶
func FromParam(name string) ExtractorSource
FromParam returns a source that reads from a URL parameter.
func FromQuery ¶
func FromQuery(name string) ExtractorSource
FromQuery returns a source that reads from a query parameter.
func FromSession ¶
func FromSession(key string) ExtractorSource
FromSession returns a source that reads from a session value. Tries string type assertion first, falls back to fmt.Sprint for non-string values.
type FingerprintMode ¶
type FingerprintMode int
FingerprintMode determines which fingerprint generation algorithm to use.
Fingerprinting helps detect session hijacking by validating that requests come from the same device that created the session.
Example configuration:
app := forge.New(
forge.WithSession(store,
forge.WithSessionFingerprint(
forge.FingerprintCookie, // Mode
forge.FingerprintWarn, // Strictness
),
),
)
const ( // FingerprintDisabled disables fingerprint generation and validation. // Use this for maximum compatibility. FingerprintDisabled FingerprintMode = iota // FingerprintCookie uses default settings, excludes IP. Best for most web apps. // Validates User-Agent and common request headers. FingerprintCookie // FingerprintJWT uses minimal fingerprint (User-Agent + header set), excludes Accept headers. // Optimized for API clients that may send varying Accept headers. FingerprintJWT // FingerprintHTMX uses only User-Agent, avoids HTMX header variations. // Use this for HTMX-heavy applications where HX-* headers change frequently. FingerprintHTMX // FingerprintStrict includes IP address. Use for high-security scenarios. // WARNING: Will cause false positives for mobile users, VPN users, and dynamic proxies. // Only use if your users are on stable networks (e.g., corporate intranet). FingerprintStrict )
type FingerprintStrictness ¶
type FingerprintStrictness int
FingerprintStrictness determines behavior on fingerprint mismatch.
const ( // FingerprintWarn logs a warning but allows the session to continue. // Use when you want visibility without disrupting users. FingerprintWarn FingerprintStrictness = iota // FingerprintReject invalidates the session on fingerprint mismatch. // Returns ErrSessionFingerprintMismatch from LoadSession. FingerprintReject )
type HTTPError ¶
type HTTPError struct {
// Err is the underlying error (for logging, not exposed to users).
Err error
// Message is the user-facing error message.
Message string
// Title is an optional title for the error (defaults derived from Code).
Title string
// Detail is an optional extended description.
Detail string
// ErrorCode is an application-specific error code (for i18n, client handling).
ErrorCode string
// RequestID is the request tracking ID.
RequestID string
// Code is the HTTP status code (e.g., 404, 500).
Code int
}
HTTPError represents an HTTP error with all data needed for rendering. It implements the error interface and provides structured data for error handlers to render error pages or toasts.
func AsHTTPError ¶
AsHTTPError extracts the HTTPError from an error if present. Returns nil if the error is not an HTTPError.
func ErrBadRequest ¶
func ErrBadRequest(message string, opts ...HTTPErrorOption) *HTTPError
func ErrConflict ¶
func ErrConflict(message string, opts ...HTTPErrorOption) *HTTPError
func ErrForbidden ¶
func ErrForbidden(message string, opts ...HTTPErrorOption) *HTTPError
func ErrInternal ¶
func ErrInternal(message string, opts ...HTTPErrorOption) *HTTPError
func ErrNotFound ¶
func ErrNotFound(message string, opts ...HTTPErrorOption) *HTTPError
func ErrServiceUnavailable ¶
func ErrServiceUnavailable(message string, opts ...HTTPErrorOption) *HTTPError
func ErrTooManyRequests ¶
func ErrTooManyRequests(message string, opts ...HTTPErrorOption) *HTTPError
func ErrUnauthorized ¶
func ErrUnauthorized(message string, opts ...HTTPErrorOption) *HTTPError
func ErrUnprocessable ¶
func ErrUnprocessable(message string, opts ...HTTPErrorOption) *HTTPError
func NewHTTPError ¶
NewHTTPError creates a new HTTPError with the given status code and message.
func (*HTTPError) StatusCode ¶
func (*HTTPError) StatusText ¶
type HTTPErrorOption ¶
type HTTPErrorOption func(*HTTPError)
HTTPErrorOption configures an HTTPError.
func WithDetail ¶
func WithDetail(detail string) HTTPErrorOption
func WithError ¶
func WithError(err error) HTTPErrorOption
func WithErrorCode ¶
func WithErrorCode(code string) HTTPErrorOption
func WithRequestID ¶
func WithRequestID(id string) HTTPErrorOption
func WithTitle ¶
func WithTitle(title string) HTTPErrorOption
type Handler ¶
type Handler interface {
Routes(r Router)
}
Handler declares routes on a router.
Example:
type AuthHandler struct {
repo *repository.Queries
}
func (h *AuthHandler) Routes(r forge.Router) {
r.GET("/login", h.showLogin)
r.POST("/login", h.handleLogin)
}
type HandlerFunc ¶
HandlerFunc is the signature for route handlers. It receives a Context and returns an error. Returning a non-nil error triggers the error handling middleware.
type HealthCheckOption ¶
type HealthCheckOption func(healthChecks)
HealthCheckOption adds a readiness check to the health configuration.
func HealthCheck ¶
func HealthCheck(name string, fn CheckFunc) HealthCheckOption
HealthCheck creates a named readiness check option.
type JWTClaimsKey ¶
type JWTClaimsKey struct{}
JWTClaimsKey is the context key used to store parsed JWT claims.
type JobEnqueuer ¶
type JobEnqueuer struct {
// contains filtered or unexported fields
}
JobEnqueuer wraps the pkg/job.Enqueuer for internal use. It provides enqueueing capability without worker processing.
func NewJobEnqueuer ¶
func NewJobEnqueuer(pool *pgxpool.Pool, opts ...job.EnqueuerOption) (*JobEnqueuer, error)
NewJobEnqueuer creates a new JobEnqueuer with the given pool and options.
func (*JobEnqueuer) Enqueue ¶
func (je *JobEnqueuer) Enqueue(ctx context.Context, name string, payload any, opts ...job.EnqueueOption) error
Enqueue adds a job to the queue.
func (*JobEnqueuer) EnqueueTx ¶
func (je *JobEnqueuer) EnqueueTx(ctx context.Context, tx pgx.Tx, name string, payload any, opts ...job.EnqueueOption) error
EnqueueTx adds a job to the queue within a transaction.
func (*JobEnqueuer) Enqueuer ¶
func (je *JobEnqueuer) Enqueuer() *job.Enqueuer
Enqueuer returns the underlying job.Enqueuer.
type JobManager ¶
type JobManager struct {
// contains filtered or unexported fields
}
JobManager wraps the pkg/job.Manager for internal use in the framework.
func NewJobManager ¶
NewJobManager creates a new JobManager with the given pool, config, and options.
func (*JobManager) Manager ¶
func (jm *JobManager) Manager() *job.Manager
Manager returns the underlying job.Manager.
func (*JobManager) Shutdown ¶
func (jm *JobManager) Shutdown() func(context.Context) error
Shutdown returns a shutdown function for the job manager.
type LanguageKey ¶
type LanguageKey struct{}
LanguageKey is the context key used to store the resolved language string.
type Middleware ¶
type Middleware func(next HandlerFunc) HandlerFunc
Middleware wraps a HandlerFunc to add cross-cutting concerns. Middleware can inspect/modify the request, short-circuit processing, or wrap the response.
Example:
func Auth(next forge.HandlerFunc) forge.HandlerFunc {
return func(c forge.Context) error {
if !isAuthenticated(c) {
return c.Redirect(302, "/login")
}
return next(c)
}
}
type Option ¶
type Option func(*App)
Option configures the application.
func WithCookieConfig ¶
WithCookieConfig configures the cookie manager.
func WithCustomLogger ¶
WithCustomLogger sets a fully custom logger. Use this when you need complete control over logging configuration.
func WithErrorHandler ¶
func WithErrorHandler(h ErrorHandler) Option
WithErrorHandler sets a custom error handler for handler errors. Called when a handler returns a non-nil error.
func WithHandlers ¶
WithHandlers registers handlers that declare routes. Each handler's Routes method is called during setup.
func WithHealthChecks ¶
func WithHealthChecks(checks ...HealthCheckOption) Option
WithHealthChecks enables health check endpoints with hardcoded paths. Liveness: /_live — always returns OK if process is running. Readiness: /_ready — runs all configured checks.
func WithJobEnqueuer ¶
func WithJobEnqueuer(pool *pgxpool.Pool, opts ...job.EnqueuerOption) Option
WithJobEnqueuer enables job enqueueing without worker processing. Use this for web servers that dispatch work to separate worker processes. Workers must be running elsewhere to process the enqueued jobs.
func WithJobWorker ¶
WithJobWorker enables job processing without enqueueing capability. Use this for dedicated background worker processes that don't need to dispatch additional jobs. Workers are started automatically when the app runs and stopped gracefully during shutdown.
func WithJobs ¶
WithJobs enables both job enqueueing and worker processing using River. A pgxpool.Pool is required for the job queue. Workers are started automatically when the app runs and stopped gracefully during shutdown.
func WithLogger ¶
func WithLogger(component string, extractors ...logger.ContextExtractor) Option
WithLogger creates a logger with a component name and optional extractors. The component name is added to every log entry for easy filtering. Extractors pull values from context (e.g., request_id, user_id).
func WithMethodNotAllowedHandler ¶
func WithMethodNotAllowedHandler(h HandlerFunc) Option
WithMethodNotAllowedHandler sets a custom 405 handler.
func WithMiddleware ¶
func WithMiddleware(mw ...Middleware) Option
WithMiddleware adds global middleware to the application. Middleware is applied in the order provided.
func WithNotFoundHandler ¶
func WithNotFoundHandler(h HandlerFunc) Option
WithNotFoundHandler sets a custom 404 handler.
func WithRoles ¶
func WithRoles(permissions RolePermissions, extractor RoleExtractorFunc) Option
WithRoles configures role-based access control for the application. The permissions map defines which permissions each role grants. The extractor function determines the current user's role from the request context. Roles are extracted lazily (once per request) and cached.
func WithSSEKeepAlive ¶
WithSSEKeepAlive sets the interval for SSE keepalive comments. Defaults to 30 seconds if not set or if d <= 0.
func WithSession ¶
func WithSession(store Store, opts ...SessionOption) Option
WithSession enables server-side session management. A Store implementation must be provided (e.g., PostgresStore). Sessions are loaded lazily and saved automatically before the response is written. Additional configuration can be provided via SessionOption functions.
func WithStaticFiles ¶
WithStaticFiles mounts a static file handler at the given pattern. Directory listings are disabled. Files are served with default cache headers.
func WithStorage ¶
WithStorage configures file storage for the application. A storage.Storage implementation must be provided (e.g., S3Client). Enables c.Upload(), c.UploadFromURL(), c.Download(), c.DeleteFile(), and c.FileURL().
type ResponseWriter ¶
type ResponseWriter struct {
http.ResponseWriter
// contains filtered or unexported fields
}
ResponseWriter wraps http.ResponseWriter to provide response interception. It tracks write status, runs hooks before the first write, and transforms status codes for HTMX requests.
func NewResponseWriter ¶
func NewResponseWriter(w http.ResponseWriter, isHTMX bool) *ResponseWriter
NewResponseWriter creates a new ResponseWriter.
func (*ResponseWriter) Flush ¶
func (w *ResponseWriter) Flush()
Flush implements the http.Flusher interface.
func (*ResponseWriter) Hijack ¶
func (w *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)
Hijack implements the http.Hijacker interface.
func (*ResponseWriter) OnBeforeWrite ¶
func (w *ResponseWriter) OnBeforeWrite(fn func())
OnBeforeWrite registers a hook to run before the first write. Hooks are called in registration order when WriteHeader or Write is first called.
func (*ResponseWriter) Push ¶
func (w *ResponseWriter) Push(target string, opts *http.PushOptions) error
Push implements the http.Pusher interface.
func (*ResponseWriter) Seal ¶
func (w *ResponseWriter) Seal()
Seal prevents any further writes to the response. After sealing, both Write and WriteHeader become silent no-ops. Used by the timeout middleware to prevent the handler goroutine from corrupting the response after timeout has fired.
func (*ResponseWriter) Size ¶
func (w *ResponseWriter) Size() int64
Size returns the number of bytes written to the response body.
func (*ResponseWriter) Status ¶
func (w *ResponseWriter) Status() int
Status returns the HTTP status code of the response.
func (*ResponseWriter) Unwrap ¶
func (w *ResponseWriter) Unwrap() http.ResponseWriter
Unwrap returns the underlying ResponseWriter. This allows middleware to access the original writer if needed.
func (*ResponseWriter) WriteHeader ¶
func (w *ResponseWriter) WriteHeader(code int)
WriteHeader sends an HTTP response header with the provided status code. For HTMX requests, non-200 status codes are transformed to 200.
func (*ResponseWriter) Written ¶
func (w *ResponseWriter) Written() bool
Written returns true if the response has been written.
type RoleExtractorFunc ¶
RoleExtractorFunc extracts the current user's role from the request context.
type RolePermissions ¶
type RolePermissions = map[string][]Permission
RolePermissions maps role names to their granted permissions.
type Router ¶
type Router interface {
// GET registers a handler for GET requests.
GET(path string, h HandlerFunc, mw ...Middleware)
// POST registers a handler for POST requests.
POST(path string, h HandlerFunc, mw ...Middleware)
// PUT registers a handler for PUT requests.
PUT(path string, h HandlerFunc, mw ...Middleware)
// PATCH registers a handler for PATCH requests.
PATCH(path string, h HandlerFunc, mw ...Middleware)
// DELETE registers a handler for DELETE requests.
DELETE(path string, h HandlerFunc, mw ...Middleware)
// HEAD registers a handler for HEAD requests.
HEAD(path string, h HandlerFunc, mw ...Middleware)
// OPTIONS registers a handler for OPTIONS requests.
OPTIONS(path string, h HandlerFunc, mw ...Middleware)
// Group creates an inline route group.
// All routes defined inside fn share no common pattern prefix.
Group(fn func(r Router))
// Route creates a route group with a pattern prefix.
// All routes defined inside fn share the pattern prefix.
Route(pattern string, fn func(r Router))
// Use appends middleware to the router's middleware stack.
Use(mw ...Middleware)
// Mount attaches an http.Handler at the given pattern.
// Use this for legacy handlers or third-party routers.
Mount(pattern string, h http.Handler)
}
Router is the interface handlers use to declare routes. It provides HTTP method routing and grouping capabilities.
type RunConfig ¶
type RunConfig struct {
Address string `env:"ADDRESS" envDefault:":8080"`
ShutdownTimeout time.Duration `env:"SHUTDOWN_TIMEOUT" envDefault:"30s"`
}
RunConfig holds externally configurable runtime settings.
type RunOption ¶
type RunOption func(*runConfig)
RunOption configures the server runtime.
func WithContext ¶
WithContext sets a custom base context for signal handling. Useful for testing or when integrating with existing context hierarchies. Defaults to context.Background() if not set.
func WithDomain ¶
WithDomain maps a host pattern to an App. Patterns: "api.example.com" (exact) or "*.example.com" (wildcard)
func WithFallback ¶
WithFallback sets the default App for requests that don't match any domain. If no domains are configured, the fallback becomes the main handler.
func WithRunLogger ¶
WithRunLogger sets the application logger for the runtime. If nil, logging is disabled.
func WithShutdownHook ¶
WithShutdownHook registers a cleanup function to run during shutdown. Hooks are called in the order they were registered. Each hook receives a context with the shutdown timeout.
func WithStartupHook ¶
WithStartupHook registers a function to run during server startup. Hooks are called in the order they were registered, after the port is bound but before serving requests. If any hook fails, the server stops and returns the error.
type SSEEvent ¶
type SSEEvent struct {
// contains filtered or unexported fields
}
func SSEComment ¶
type Session ¶
type Session struct {
CreatedAt time.Time
LastActiveAt time.Time
ExpiresAt time.Time
UserID *string // nil = anonymous session
Data map[string]any // Arbitrary session data
ID string // Unique identifier (ULID)
TokenHash string // SHA-256 hash of the cookie token
IP string // Client IP address
UserAgent string // Raw User-Agent header
Device string // Parsed device info (e.g., "Chrome/128 (macOS, desktop)")
Fingerprint string // Device fingerprint for hijacking detection
// contains filtered or unexported fields
}
Session represents a user session with metadata and arbitrary values.
Goroutine Safety ¶
Session methods are NOT goroutine-safe. Session instances should only be accessed from a single request context. If you need to share session data across goroutines, make a copy of the required values first.
Auto-Creation ¶
Sessions are automatically created when accessed via Context methods:
- c.Session() - loads existing or creates anonymous session
- c.SessionValue(key) - auto-creates if needed, returns value
- c.SetSessionValue(key, val) - auto-creates if needed, stores value
No manual c.InitSession() is required. Sessions are lazily created on first access.
Session Lifecycle ¶
Anonymous sessions are created with UserID = nil. When a user authenticates, call c.AuthenticateSession(userID) to promote the session and rotate the token for session fixation prevention.
Example:
func LoginHandler(c forge.Context) error {
// Validate credentials...
if err := c.AuthenticateSession(user.ID); err != nil {
return err
}
return c.Redirect(http.StatusSeeOther, "/dashboard")
}
Session Limits ¶
By default, users can have up to 5 concurrent sessions (configurable up to 10). When the limit is reached, the oldest session is automatically deleted. This prevents session accumulation and provides "logout from all devices" functionality.
func NewSession ¶
NewSession creates a new session with the given token and expiration.
func (*Session) ClearDirty ¶
func (s *Session) ClearDirty()
ClearDirty marks the session as clean (saved). Called by the session manager after persisting changes.
func (*Session) DeleteValue ¶
DeleteValue removes a value from the session. Marks the session as dirty only if the key existed.
WARNING: NOT goroutine-safe. Only call from the request context.
func (*Session) GetValue ¶
GetValue retrieves a value from the session. Returns (value, true) if found, (nil, false) if not found.
WARNING: NOT goroutine-safe. Only call from the request context.
func (*Session) IsAuthenticated ¶
type SessionOption ¶
type SessionOption func(*sessionConfig)
SessionOption configures session settings.
func WithMaxSessionsPerUser ¶
func WithMaxSessionsPerUser(max int) SessionOption
WithMaxSessionsPerUser sets the maximum concurrent sessions per user. Value is capped at maxAllowedSessionsPerUser (10).
func WithSessionCookieName ¶
func WithSessionCookieName(name string) SessionOption
WithSessionCookieName sets the session cookie name.
This is the ONLY session-specific cookie setting. All other cookie settings (domain, path, secure, httpOnly, sameSite) are inherited from the global cookie manager configured via WithCookieConfig().
Example:
app := forge.New(
forge.AppConfig{},
// Configure cookie security globally
forge.WithCookieConfig(cookie.Config{
Domain: ".example.com",
Secure: true,
HTTPOnly: true,
SameSite: "lax",
}),
// Configure session with custom cookie name
forge.WithSession(store,
forge.WithSessionCookieName("__session"), // Only name is customizable
),
)
func WithSessionFingerprint ¶
func WithSessionFingerprint(mode FingerprintMode, strictness FingerprintStrictness) SessionOption
WithSessionFingerprint configures fingerprint validation.
func WithSessionLogger ¶
func WithSessionLogger(logger *slog.Logger) SessionOption
WithSessionLogger sets the logger for session events.
func WithSessionTTL ¶
func WithSessionTTL(ttl time.Duration) SessionOption
WithSessionTTL sets the session time-to-live duration.
func WithSessionTouchThreshold ¶
func WithSessionTouchThreshold(threshold time.Duration) SessionOption
WithSessionTouchThreshold sets the minimum time between LastActiveAt updates.
type Store ¶
type Store interface {
// Create persists a new session.
// The Store is responsible for serializing Session.Data to JSON.
Create(ctx context.Context, s *Session) error
// GetByTokenHash retrieves a session by its SHA-256 token hash.
// Returns ErrSessionNotFound if the session doesn't exist.
// Returns ErrSessionExpired if the session has expired.
//
// PERFORMANCE: This method is called on EVERY request with a session cookie.
// It MUST use an index on token_hash column for fast lookups.
//
// The Store is responsible for deserializing Session.Data from JSON.
GetByTokenHash(ctx context.Context, tokenHash string) (*Session, error)
// Update saves changes to an existing session.
// The Store is responsible for serializing Session.Data to JSON.
Update(ctx context.Context, s *Session) error
// Delete removes a session by its ID.
Delete(ctx context.Context, id string) error
// ListByUserID retrieves all sessions for a user.
// Used for "view active sessions" functionality.
// The Store is responsible for deserializing Session.Data from JSON.
ListByUserID(ctx context.Context, userID string) ([]*Session, error)
// CountByUserID returns the number of sessions for a user.
// Used for enforcing session limits.
CountByUserID(ctx context.Context, userID string) (int, error)
// DeleteByUserID removes all sessions for a user.
// Useful for "logout from all devices" functionality.
DeleteByUserID(ctx context.Context, userID string) error
// DeleteByUserIDExcept removes all sessions for a user except the specified session ID.
// Used for "logout from other devices" functionality.
// FIX #6: Enables batch delete instead of N+1 queries.
DeleteByUserIDExcept(ctx context.Context, userID, exceptID string) error
// DeleteOldestByUserID removes the oldest session for a user (by last_active_at).
// Used for enforcing max session limits per user.
DeleteOldestByUserID(ctx context.Context, userID string) error
// Touch updates the LastActiveAt timestamp without loading the full session.
// Used for activity tracking without full session updates when touch threshold is met.
Touch(ctx context.Context, id string, lastActiveAt time.Time) error
}
Store defines the interface for session persistence.
Database Requirements ¶
Your Store implementation MUST create the following indexes for performance:
CREATE INDEX idx_sessions_token_hash ON sessions(token_hash); CREATE INDEX idx_sessions_user_id ON sessions(user_id); CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
The token_hash index is CRITICAL for session lookup performance.
Data Serialization ¶
The Store is responsible for serializing the Session.Data map[string]any field. For SQL databases, use JSONB or JSON column type. Example Postgres schema below.
Example Postgres Schema ¶
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
token_hash TEXT NOT NULL,
user_id TEXT,
data JSONB NOT NULL DEFAULT '{}',
ip TEXT NOT NULL DEFAULT '',
user_agent TEXT NOT NULL DEFAULT '',
device TEXT NOT NULL DEFAULT '',
fingerprint TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_active_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_sessions_token_hash ON sessions(token_hash);
CREATE INDEX idx_sessions_user_id ON sessions(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
Cleanup Job ¶
Implement a background job to delete expired sessions:
DELETE FROM sessions WHERE expires_at < NOW();
Run this hourly or daily depending on your traffic.
Cookie Configuration ¶
Session cookies inherit security settings from the global cookie manager configured via WithCookieConfig(). Only the cookie name is session-specific and can be customized via WithSessionCookieName().
Example cookie configuration:
app := forge.New(
forge.AppConfig{},
forge.WithCookieConfig(cookie.Config{
Domain: ".example.com",
Secure: true, // All cookies (including sessions) are secure
HTTPOnly: true, // All cookies are httpOnly
SameSite: "strict", // All cookies use strict sameSite
}),
forge.WithSession(store),
)
type TranslatorKey ¶
type TranslatorKey struct{}
TranslatorKey is the context key used to store the i18n Translator.
type ValidationErrors ¶
type ValidationErrors = validator.ValidationErrors
ValidationErrors is a collection of validation errors.