// 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} }