feat: implement OpenAPI/Swagger documentation with swaggo/swag

📝 docs: add comprehensive API documentation
📦 dependencies: add swaggo/swag to go.mod
🔧 chore: add go:generate directive for documentation

- Add comprehensive API documentation using swaggo/swag
- Embed OpenAPI spec in binary using go:embed
- Add Swagger UI at /swagger/
- Document all endpoints, models, and validation rules
- Add go:generate directive for easy regeneration
- Update README, AGENTS, CHANGELOG with documentation
- Finalize ADR 0013 with implementation details
- Gitignore generated docs directory

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-04-05 00:45:40 +02:00
parent 15fcb265bd
commit b279a31f88
12 changed files with 371 additions and 38 deletions

View File

@@ -9,6 +9,48 @@ import (
"github.com/rs/zerolog/log"
)
// GreetResponse represents a greeting response
// @Description GreetResponse represents a greeting response with a message
// @Property message string "The greeting message" example("Hello John!")
type GreetResponse struct {
Message string `json:"message" example:"Hello John!"`
}
// GreetRequest represents a greeting request
// @Description GreetRequest represents a request with a name to greet
// @Property name string "The name to greet" example("John")
type GreetRequest struct {
Name string `json:"name" example:"John"`
}
// ErrorResponse represents an error response
// @Description ErrorResponse represents an error response
type ErrorResponse struct {
Error string `json:"error" example:"invalid_request"`
Message string `json:"message" example:"Invalid name parameter"`
}
// GreetResponseV2 represents a v2 greeting response
// @Description GreetResponseV2 represents a v2 greeting response
type GreetResponseV2 struct {
Message string `json:"message" example:"Hello my friend John!"`
}
// ValidationError represents a validation error response
// @Description ValidationError represents a validation error with details
type ValidationError struct {
Error string `json:"error" example:"validation_failed"`
Message string `json:"message" example:"Invalid request data"`
Details []ValidationDetail `json:"details,omitempty"`
}
// ValidationDetail represents a single validation error detail
// @Description ValidationDetail represents a single field validation error
type ValidationDetail struct {
Field string `json:"field" example:"name"`
Error string `json:"error" example:"must be <= 100 characters"`
}
type Greeter interface {
Greet(ctx context.Context, name string) string
}
@@ -32,11 +74,29 @@ func (h *apiV1GreetHandler) RegisterRoutes(router chi.Router) {
log.Trace().Msg("Greet routes registered")
}
// handleGreetQuery godoc
// @Summary Get default greeting
// @Description Returns a default greeting message
// @Tags greet
// @Accept json
// @Produce json
// @Success 200 {object} GreetResponse "Successful response"
// @Router /v1/greet [get]
func (h *apiV1GreetHandler) handleGreetQuery(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
h.writeJSONResponse(w, h.greeter.Greet(r.Context(), name))
}
// handleGreetPath godoc
// @Summary Get personalized greeting
// @Description Returns a greeting with the specified name
// @Tags greet
// @Accept json
// @Produce json
// @Param name path string true "Name to greet"
// @Success 200 {object} GreetResponse "Successful response"
// @Failure 400 {object} ErrorResponse "Invalid name parameter"
// @Router /v1/greet/{name} [get]
func (h *apiV1GreetHandler) handleGreetPath(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
h.writeJSONResponse(w, h.greeter.Greet(r.Context(), name))

View File

@@ -44,6 +44,16 @@ type greetResponse struct {
Message string `json:"message"`
}
// handleGreetPost godoc
// @Summary Get greeting (v2)
// @Description Returns a greeting message with validation (v2)
// @Tags greet
// @Accept json
// @Produce json
// @Param request body GreetRequest true "Greeting request"
// @Success 200 {object} GreetResponseV2 "Successful response"
// @Failure 400 {object} ValidationError "Validation error"
// @Router /v2/greet [post]
func (h *apiV2GreetHandler) handleGreetPost(w http.ResponseWriter, r *http.Request) {
// Read request body
body, err := io.ReadAll(r.Body)

View File

@@ -1,25 +1,33 @@
//go:generate swag init -g ../../cmd/server/main.go
package server
import (
"context"
"embed"
"net"
"net/http"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog/log"
httpSwagger "github.com/swaggo/http-swagger"
"DanceLessonsCoach/pkg/config"
"DanceLessonsCoach/pkg/greet"
"DanceLessonsCoach/pkg/telemetry"
"DanceLessonsCoach/pkg/validation"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog/log"
)
//go:embed docs/swagger.json
var swaggerJSON embed.FS
type Server struct {
router *chi.Mux
readyCtx context.Context
@@ -75,6 +83,22 @@ func (s *Server) setupRoutes() {
s.registerApiV2Routes(r)
})
}
// Add Swagger UI with embedded spec
// Serve the embedded swagger.json file
s.router.Handle("/swagger/doc.json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
data, err := swaggerJSON.ReadFile("docs/swagger.json")
if err != nil {
log.Error().Err(err).Msg("Failed to read embedded swagger.json")
http.Error(w, "Failed to read swagger.json", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}))
// Setup Swagger UI handler
s.router.Get("/swagger/*", httpSwagger.WrapHandler)
}
func (s *Server) registerApiV1Routes(r chi.Router) {
@@ -109,11 +133,28 @@ func (s *Server) getAllMiddlewares() []func(http.Handler) http.Handler {
return middlewares
}
// handleHealth godoc
// @Summary Health check
// @Description Check if the service is healthy
// @Tags health
// @Accept json
// @Produce json
// @Success 200 {object} map[string]string "Service is healthy"
// @Router /health [get]
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
log.Trace().Msg("Health check requested")
w.Write([]byte(`{"status":"healthy"}`))
}
// handleReadiness godoc
// @Summary Readiness check
// @Description Check if the service is ready to accept traffic
// @Tags health
// @Accept json
// @Produce json
// @Success 200 {object} map[string]bool "Service is ready"
// @Failure 503 {object} map[string]bool "Service is not ready"
// @Router /ready [get]
func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) {
log.Trace().Msg("Readiness check requested")