159 lines
4.9 KiB
Go
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}
|
|
}
|