Files
nexarch/internal/openapi/openapi.go
T

159 lines
4.9 KiB
Go

// Package openapi implementiert Core API-04: eine OpenAPI-3.x-Beschreibung
// der Core-API, die automatisiert gegen die tatsaechlich registrierten
// Routen von internal/apiserver.Server geprueft wird (Akzeptanzkriterium 2)
// — kein manuell gepflegtes Dokument, das unbemerkt vom Code abweichen kann.
package openapi
import (
"fmt"
"sort"
"gitea.perlbach24.de/scripte/nexarch/internal/apiserver"
)
// Entry beschreibt EINEN dokumentierten Endpunkt inklusive eines
// Beispielaufrufs (Akzeptanzkriterium 2 / Pruefung 2: "Beispielaufrufe aus
// dem Dokument gegen die echte API erfolgreich ausgefuehrt").
type Entry struct {
// Path ist der VOLLSTAENDIGE, versionierte Pfad (z.B. "/api/v1/things"),
// identisch zu dem, was apiserver.Server.RegisteredPaths() liefert.
Path string
Method string
Summary string
Description string
// ExampleRequest wird von CheckExamples tatsaechlich gegen den Server
// ausgefuehrt (Cookie, Body etc. sind Sache des Aufrufers, siehe
// openapi_test.go) — dieses Paket fuehrt nur die HTTP-Anfrage aus und
// prueft ExpectStatus.
ExampleRequest ExampleRequest
ExpectStatus int
}
// ExampleRequest ist minimal genug, um sowohl in das OpenAPI-Dokument als
// auch als tatsaechliche HTTP-Anfrage verwendet zu werden — EIN Beispiel,
// zwei Verwendungen, damit Dokument und Test nie auseinanderlaufen koennen.
type ExampleRequest struct {
Method string
Path string
Description string
}
// Document ist eine bewusst schlanke OpenAPI-3.0-Repraesentation — genug,
// um valide zu sein und von Swagger UI/Postman importiert zu werden
// (Akzeptanzkriterium 3), ohne eine vollstaendige OpenAPI-Bibliothek zu
// integrieren.
type Document struct {
OpenAPI string `json:"openapi"`
Info Info `json:"info"`
Paths map[string]PathItem `json:"paths"`
}
type Info struct {
Title string `json:"title"`
Version string `json:"version"`
}
type PathItem map[string]Operation
type Operation struct {
Summary string `json:"summary"`
Description string `json:"description,omitempty"`
Responses map[string]Response `json:"responses"`
}
type Response struct {
Description string `json:"description"`
}
// BuildDocument erzeugt das OpenAPI-Dokument AUS denselben Entries, die auch
// fuer den Drift-Abgleich (CheckNoDrift) und die Beispielausfuehrung
// (siehe openapi_test.go) verwendet werden — eine einzige Quelle statt
// eines separat gepflegten Dokuments.
func BuildDocument(title, version string, entries []Entry) Document {
paths := make(map[string]PathItem)
for _, e := range entries {
item, ok := paths[e.Path]
if !ok {
item = PathItem{}
}
item[toLowerMethod(e.Method)] = Operation{
Summary: e.Summary,
Description: e.Description,
Responses: map[string]Response{
fmt.Sprintf("%d", e.ExpectStatus): {Description: "Beispielhafte Antwort"},
},
}
paths[e.Path] = item
}
return Document{
OpenAPI: "3.0.3",
Info: Info{Title: title, Version: version},
Paths: paths,
}
}
func toLowerMethod(m string) string {
switch m {
case "GET", "get":
return "get"
case "POST", "post":
return "post"
case "PUT", "put":
return "put"
case "DELETE", "delete":
return "delete"
case "PATCH", "patch":
return "patch"
default:
return "get"
}
}
// ErrDrift wird von CheckNoDrift geliefert, wenn dokumentierte und
// tatsaechlich registrierte Pfade auseinanderlaufen (Akzeptanzkriterium 2 /
// Pruefung 1).
type ErrDrift struct {
MissingInDocument []string // registriert, aber nicht dokumentiert
MissingAsRoute []string // dokumentiert, aber nicht (mehr) registriert
}
func (e *ErrDrift) Error() string {
return fmt.Sprintf("openapi: drift erkannt — nicht dokumentiert: %v, nicht (mehr) registriert: %v",
e.MissingInDocument, e.MissingAsRoute)
}
// CheckNoDrift vergleicht die tatsaechlich registrierten Pfade eines
// Servers mit den in entries dokumentierten Pfaden — vollstaendige
// Uebereinstimmung der PATH-Menge (nicht Methode je Pfad, da
// apiserver.Server.Handle methodenunabhaengig registriert). Ein absichtlich
// entfernter Eintrag auf beiden Seiten (siehe Tests) macht diese Funktion
// fehlschlagen, das ist der geforderte Drift-Nachweis.
func CheckNoDrift(server *apiserver.Server, entries []Entry) error {
registered := make(map[string]bool)
for _, p := range server.RegisteredPaths() {
registered[p] = true
}
documented := make(map[string]bool)
for _, e := range entries {
documented[e.Path] = true
}
var missingInDoc, missingAsRoute []string
for p := range registered {
if !documented[p] {
missingInDoc = append(missingInDoc, p)
}
}
for p := range documented {
if !registered[p] {
missingAsRoute = append(missingAsRoute, p)
}
}
if len(missingInDoc) == 0 && len(missingAsRoute) == 0 {
return nil
}
sort.Strings(missingInDoc)
sort.Strings(missingAsRoute)
return &ErrDrift{MissingInDocument: missingInDoc, MissingAsRoute: missingAsRoute}
}