navidrome/server/apiv1/api_gen.go
Deluan Quintão c22ce9ebb2
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling

vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.

* fix(api): pin the Redocly CLI version

Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.

* fix(api): bundle the OpenAPI spec with vacuum

vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.

* fix(api): make spec lint rules cover all schemas and error codes

nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.

* feat(api): embed the bundled OpenAPI spec and expose its version

* feat(api): generate the v1 server interface with oapi-codegen

* feat(api): add RFC 9457 problem responses for API v1

* feat(api): add API v1 router with /server discovery and spec routes

* fix(api): serve the OpenAPI document without range support

* feat(api): mount API v1 behind the DevAPIv1 flag

* chore(ci): lint, regenerate and diff the OpenAPI v1 spec

* refactor(api): tighten spec version access, lint rules and test naming

* refactor(api): simplify spec routes, tests and OpenAPI tooling

Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.

Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.

* refactor(api): raise the list limit maximum to 2000 and drop the flag test

* feat(api): treat added enum values as non-breaking

Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.

* feat(api): gate breaking changes on x-stability-level

Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.

* feat(api): declare loginMethods as an enum

Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.

* feat(api): send Allow on 405 and answer HEAD wherever GET is routed

chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.

* refactor(api): hash the spec ETag with xxh3

The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.

* docs(api): explain the about:blank problem type

* feat(api): make code the problem identifier and omit a blank type

RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.

* fix(api): advertise the configured base path in the served OpenAPI spec

With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.

Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00

390 lines
12 KiB
Go

// Package apiv1 provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.8.0 DO NOT EDIT.
package apiv1
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
)
// Defines values for ProblemCode.
const (
ProblemCodeForbidden ProblemCode = "forbidden"
ProblemCodeInternal ProblemCode = "internal"
ProblemCodeMethodNotAllowed ProblemCode = "method_not_allowed"
ProblemCodeNotFound ProblemCode = "not_found"
ProblemCodeUnauthorized ProblemCode = "unauthorized"
ProblemCodeUnavailable ProblemCode = "unavailable"
ProblemCodeValidation ProblemCode = "validation"
)
// Valid indicates whether the value is a known member of the ProblemCode enum.
func (e ProblemCode) Valid() bool {
switch e {
case ProblemCodeForbidden:
return true
case ProblemCodeInternal:
return true
case ProblemCodeMethodNotAllowed:
return true
case ProblemCodeNotFound:
return true
case ProblemCodeUnauthorized:
return true
case ProblemCodeUnavailable:
return true
case ProblemCodeValidation:
return true
default:
return false
}
}
// Defines values for ServerInfoLoginMethods.
const (
ServerInfoLoginMethodsPassword ServerInfoLoginMethods = "password"
)
// Valid indicates whether the value is a known member of the ServerInfoLoginMethods enum.
func (e ServerInfoLoginMethods) Valid() bool {
switch e {
case ServerInfoLoginMethodsPassword:
return true
default:
return false
}
}
// Problem RFC 9457 problem details, returned for every 4xx and 5xx response.
type Problem struct {
// Code Machine-readable error code, and the value clients switch on. New codes may be added.
Code ProblemCode `json:"code"`
// Detail Human-readable explanation specific to this occurrence. Omitted for internal errors.
Detail *string `json:"detail,omitempty"`
// Errors Per-field failures. Present only when `code` is `validation`.
Errors *[]ValidationError `json:"errors,omitempty"`
// Status HTTP status code of this response.
Status int `json:"status"`
// Title Short human-readable summary, the same for all occurrences of this problem type.
Title string `json:"title"`
// Type URI reference identifying the problem type. Omitted while the problem carries no semantics
// beyond its HTTP status code, which RFC 9457 defines as `about:blank`. Problems with their
// own semantics get their own URI; switch on `code` instead.
Type *string `json:"type,omitempty"`
}
// ProblemCode Machine-readable error code, and the value clients switch on. New codes may be added.
type ProblemCode string
// ServerInfo Public server description. Everything an add-server screen needs before login.
type ServerInfo struct {
// LoginMethods Login methods this server accepts. New methods may be added; clients ignore values they do not recognise.
LoginMethods []ServerInfoLoginMethods `json:"loginMethods"`
// Name Human-readable server product name.
Name string `json:"name"`
// ServerVersion Version of the running server build.
ServerVersion string `json:"serverVersion"`
// SetupRequired True until the first admin user has been created.
SetupRequired bool `json:"setupRequired"`
// SpecVersion Version of the OpenAPI document this server implements.
SpecVersion string `json:"specVersion"`
}
// ServerInfoLoginMethods defines model for ServerInfo.LoginMethods.
type ServerInfoLoginMethods string
// ValidationError One field-level validation failure.
type ValidationError struct {
// Field Name of the offending query parameter, path parameter, or body field (dotted for nested).
Field string `json:"field"`
// Message Why the value was rejected.
Message string `json:"message"`
}
// InternalError RFC 9457 problem details, returned for every 4xx and 5xx response.
type InternalError = Problem
// ServerInterface represents all server handlers.
type ServerInterface interface {
// GetServerInfo Describe the server
// (GET /server)
GetServerInfo(w http.ResponseWriter, r *http.Request)
}
// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint.
type Unimplemented struct{}
// GetServerInfo Describe the server
// (GET /server)
func (_ Unimplemented) GetServerInfo(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// ServerInterfaceWrapper converts contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
HandlerMiddlewares []MiddlewareFunc
ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
}
type MiddlewareFunc func(http.Handler) http.Handler
// GetServerInfo operation middleware
func (siw *ServerInterfaceWrapper) GetServerInfo(w http.ResponseWriter, r *http.Request) {
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetServerInfo(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
type UnescapedCookieParamError struct {
ParamName string
Err error
}
func (e *UnescapedCookieParamError) Error() string {
return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName)
}
func (e *UnescapedCookieParamError) Unwrap() error {
return e.Err
}
type UnmarshalingParamError struct {
ParamName string
Err error
}
func (e *UnmarshalingParamError) Error() string {
return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error())
}
func (e *UnmarshalingParamError) Unwrap() error {
return e.Err
}
type RequiredParamError struct {
ParamName string
}
func (e *RequiredParamError) Error() string {
return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName)
}
type RequiredHeaderError struct {
ParamName string
Err error
}
func (e *RequiredHeaderError) Error() string {
return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName)
}
func (e *RequiredHeaderError) Unwrap() error {
return e.Err
}
type InvalidParamFormatError struct {
ParamName string
Err error
}
func (e *InvalidParamFormatError) Error() string {
return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error())
}
func (e *InvalidParamFormatError) Unwrap() error {
return e.Err
}
type TooManyValuesForParamError struct {
ParamName string
Count int
}
func (e *TooManyValuesForParamError) Error() string {
return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count)
}
// Handler creates http.Handler with routing matching OpenAPI spec.
func Handler(si ServerInterface) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{})
}
type ChiServerOptions struct {
BaseURL string
BaseRouter chi.Router
Middlewares []MiddlewareFunc
ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
}
// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{
BaseRouter: r,
})
}
func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{
BaseURL: baseURL,
BaseRouter: r,
})
}
// HandlerWithOptions creates http.Handler with additional options
func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler {
r := options.BaseRouter
if r == nil {
r = chi.NewRouter()
}
if options.ErrorHandlerFunc == nil {
options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
wrapper := ServerInterfaceWrapper{
Handler: si,
HandlerMiddlewares: options.Middlewares,
ErrorHandlerFunc: options.ErrorHandlerFunc,
}
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/server", wrapper.GetServerInfo)
})
return r
}
type InternalErrorApplicationProblemPlusJSONResponse Problem
type GetServerInfoRequestObject struct {
}
type GetServerInfoResponseObject interface {
VisitGetServerInfoResponse(w http.ResponseWriter) error
}
type GetServerInfo200JSONResponse ServerInfo
func (response GetServerInfo200JSONResponse) VisitGetServerInfoResponse(w http.ResponseWriter) error {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(response); err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
_, err := buf.WriteTo(w)
return err
}
type GetServerInfo500ApplicationProblemPlusJSONResponse struct {
InternalErrorApplicationProblemPlusJSONResponse
}
func (response GetServerInfo500ApplicationProblemPlusJSONResponse) VisitGetServerInfoResponse(w http.ResponseWriter) error {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(response); err != nil {
return err
}
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(500)
_, err := buf.WriteTo(w)
return err
}
// StrictServerInterface represents all server handlers.
type StrictServerInterface interface {
// GetServerInfo Describe the server
// (GET /server)
GetServerInfo(ctx context.Context, request GetServerInfoRequestObject) (GetServerInfoResponseObject, error)
}
type StrictHandlerFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, request any) (any, error)
type StrictMiddlewareFunc func(f StrictHandlerFunc, operationID string) StrictHandlerFunc
type StrictHTTPServerOptions struct {
RequestErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
ResponseErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
}
func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
return &strictHandler{ssi: ssi, middlewares: middlewares, options: StrictHTTPServerOptions{
RequestErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, err.Error(), http.StatusBadRequest)
},
ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, err.Error(), http.StatusInternalServerError)
},
}}
}
func NewStrictHandlerWithOptions(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc, options StrictHTTPServerOptions) ServerInterface {
if options.RequestErrorHandlerFunc == nil {
options.RequestErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
if options.ResponseErrorHandlerFunc == nil {
options.ResponseErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
return &strictHandler{ssi: ssi, middlewares: middlewares, options: options}
}
type strictHandler struct {
ssi StrictServerInterface
middlewares []StrictMiddlewareFunc
options StrictHTTPServerOptions
}
// GetServerInfo operation middleware
func (sh *strictHandler) GetServerInfo(w http.ResponseWriter, r *http.Request) {
var request GetServerInfoRequestObject
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.GetServerInfo(ctx, request.(GetServerInfoRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "GetServerInfo")
}
response, err := handler(r.Context(), w, r, request)
if err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
} else if validResponse, ok := response.(GetServerInfoResponseObject); ok {
if err := validResponse.VisitGetServerInfoResponse(w); err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
}
} else if response != nil {
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
}
}