Codevault initial commit
This commit is contained in:
4
internal/api/doc.go
Normal file
4
internal/api/doc.go
Normal file
@@ -0,0 +1,4 @@
|
||||
// Package api contains the OpenAPI-generated GIN strict-server code for CodeVault.
|
||||
package api
|
||||
|
||||
//go:generate oapi-codegen --config=oapi-codegen.yaml openapi.yaml
|
||||
363
internal/api/generated.go
Normal file
363
internal/api/generated.go
Normal file
@@ -0,0 +1,363 @@
|
||||
// Package api provides primitives to interact with the openapi HTTP API.
|
||||
//
|
||||
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT.
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/gin-gonic/gin"
|
||||
strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
BasicAuthScopes = "basicAuth.Scopes"
|
||||
)
|
||||
|
||||
// GetValueRequest defines model for GetValueRequest.
|
||||
type GetValueRequest struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// GetValueResponse defines model for GetValueResponse.
|
||||
type GetValueResponse struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// SetValueRequest defines model for SetValueRequest.
|
||||
type SetValueRequest struct {
|
||||
Code string `json:"code"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// GetValueJSONRequestBody defines body for GetValue for application/json ContentType.
|
||||
type GetValueJSONRequestBody = GetValueRequest
|
||||
|
||||
// SetValueJSONRequestBody defines body for SetValue for application/json ContentType.
|
||||
type SetValueJSONRequestBody = SetValueRequest
|
||||
|
||||
// ServerInterface represents all server handlers.
|
||||
type ServerInterface interface {
|
||||
|
||||
// (POST /api/v1/getValue)
|
||||
GetValue(c *gin.Context)
|
||||
|
||||
// (POST /api/v1/setValue)
|
||||
SetValue(c *gin.Context)
|
||||
}
|
||||
|
||||
// ServerInterfaceWrapper converts contexts to parameters.
|
||||
type ServerInterfaceWrapper struct {
|
||||
Handler ServerInterface
|
||||
HandlerMiddlewares []MiddlewareFunc
|
||||
ErrorHandler func(*gin.Context, error, int)
|
||||
}
|
||||
|
||||
type MiddlewareFunc func(c *gin.Context)
|
||||
|
||||
// GetValue operation middleware
|
||||
func (siw *ServerInterfaceWrapper) GetValue(c *gin.Context) {
|
||||
|
||||
for _, middleware := range siw.HandlerMiddlewares {
|
||||
middleware(c)
|
||||
if c.IsAborted() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
siw.Handler.GetValue(c)
|
||||
}
|
||||
|
||||
// SetValue operation middleware
|
||||
func (siw *ServerInterfaceWrapper) SetValue(c *gin.Context) {
|
||||
|
||||
c.Set(BasicAuthScopes, []string{})
|
||||
|
||||
for _, middleware := range siw.HandlerMiddlewares {
|
||||
middleware(c)
|
||||
if c.IsAborted() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
siw.Handler.SetValue(c)
|
||||
}
|
||||
|
||||
// GinServerOptions provides options for the Gin server.
|
||||
type GinServerOptions struct {
|
||||
BaseURL string
|
||||
Middlewares []MiddlewareFunc
|
||||
ErrorHandler func(*gin.Context, error, int)
|
||||
}
|
||||
|
||||
// RegisterHandlers creates http.Handler with routing matching OpenAPI spec.
|
||||
func RegisterHandlers(router gin.IRouter, si ServerInterface) {
|
||||
RegisterHandlersWithOptions(router, si, GinServerOptions{})
|
||||
}
|
||||
|
||||
// RegisterHandlersWithOptions creates http.Handler with additional options
|
||||
func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) {
|
||||
errorHandler := options.ErrorHandler
|
||||
if errorHandler == nil {
|
||||
errorHandler = func(c *gin.Context, err error, statusCode int) {
|
||||
c.JSON(statusCode, gin.H{"msg": err.Error()})
|
||||
}
|
||||
}
|
||||
|
||||
wrapper := ServerInterfaceWrapper{
|
||||
Handler: si,
|
||||
HandlerMiddlewares: options.Middlewares,
|
||||
ErrorHandler: errorHandler,
|
||||
}
|
||||
|
||||
router.POST(options.BaseURL+"/api/v1/getValue", wrapper.GetValue)
|
||||
router.POST(options.BaseURL+"/api/v1/setValue", wrapper.SetValue)
|
||||
}
|
||||
|
||||
type GetValueRequestObject struct {
|
||||
Body *GetValueJSONRequestBody
|
||||
}
|
||||
|
||||
type GetValueResponseObject interface {
|
||||
VisitGetValueResponse(w http.ResponseWriter) error
|
||||
}
|
||||
|
||||
type GetValue200JSONResponse GetValueResponse
|
||||
|
||||
func (response GetValue200JSONResponse) VisitGetValueResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(200)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type GetValue400Response struct {
|
||||
}
|
||||
|
||||
func (response GetValue400Response) VisitGetValueResponse(w http.ResponseWriter) error {
|
||||
w.WriteHeader(400)
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetValue404Response struct {
|
||||
}
|
||||
|
||||
func (response GetValue404Response) VisitGetValueResponse(w http.ResponseWriter) error {
|
||||
w.WriteHeader(404)
|
||||
return nil
|
||||
}
|
||||
|
||||
type SetValueRequestObject struct {
|
||||
Body *SetValueJSONRequestBody
|
||||
}
|
||||
|
||||
type SetValueResponseObject interface {
|
||||
VisitSetValueResponse(w http.ResponseWriter) error
|
||||
}
|
||||
|
||||
type SetValue204Response struct {
|
||||
}
|
||||
|
||||
func (response SetValue204Response) VisitSetValueResponse(w http.ResponseWriter) error {
|
||||
w.WriteHeader(204)
|
||||
return nil
|
||||
}
|
||||
|
||||
type SetValue400Response struct {
|
||||
}
|
||||
|
||||
func (response SetValue400Response) VisitSetValueResponse(w http.ResponseWriter) error {
|
||||
w.WriteHeader(400)
|
||||
return nil
|
||||
}
|
||||
|
||||
type SetValue401Response struct {
|
||||
}
|
||||
|
||||
func (response SetValue401Response) VisitSetValueResponse(w http.ResponseWriter) error {
|
||||
w.WriteHeader(401)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StrictServerInterface represents all server handlers.
|
||||
type StrictServerInterface interface {
|
||||
|
||||
// (POST /api/v1/getValue)
|
||||
GetValue(ctx context.Context, request GetValueRequestObject) (GetValueResponseObject, error)
|
||||
|
||||
// (POST /api/v1/setValue)
|
||||
SetValue(ctx context.Context, request SetValueRequestObject) (SetValueResponseObject, error)
|
||||
}
|
||||
|
||||
type StrictHandlerFunc = strictgin.StrictGinHandlerFunc
|
||||
type StrictMiddlewareFunc = strictgin.StrictGinMiddlewareFunc
|
||||
|
||||
func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
|
||||
return &strictHandler{ssi: ssi, middlewares: middlewares}
|
||||
}
|
||||
|
||||
type strictHandler struct {
|
||||
ssi StrictServerInterface
|
||||
middlewares []StrictMiddlewareFunc
|
||||
}
|
||||
|
||||
// GetValue operation middleware
|
||||
func (sh *strictHandler) GetValue(ctx *gin.Context) {
|
||||
var request GetValueRequestObject
|
||||
|
||||
var body GetValueJSONRequestBody
|
||||
if err := ctx.ShouldBindJSON(&body); err != nil {
|
||||
ctx.Status(http.StatusBadRequest)
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
request.Body = &body
|
||||
|
||||
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
||||
return sh.ssi.GetValue(ctx, request.(GetValueRequestObject))
|
||||
}
|
||||
for _, middleware := range sh.middlewares {
|
||||
handler = middleware(handler, "GetValue")
|
||||
}
|
||||
|
||||
response, err := handler(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
ctx.Status(http.StatusInternalServerError)
|
||||
} else if validResponse, ok := response.(GetValueResponseObject); ok {
|
||||
if err := validResponse.VisitGetValueResponse(ctx.Writer); err != nil {
|
||||
ctx.Error(err)
|
||||
}
|
||||
} else if response != nil {
|
||||
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
|
||||
}
|
||||
}
|
||||
|
||||
// SetValue operation middleware
|
||||
func (sh *strictHandler) SetValue(ctx *gin.Context) {
|
||||
var request SetValueRequestObject
|
||||
|
||||
var body SetValueJSONRequestBody
|
||||
if err := ctx.ShouldBindJSON(&body); err != nil {
|
||||
ctx.Status(http.StatusBadRequest)
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
request.Body = &body
|
||||
|
||||
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
||||
return sh.ssi.SetValue(ctx, request.(SetValueRequestObject))
|
||||
}
|
||||
for _, middleware := range sh.middlewares {
|
||||
handler = middleware(handler, "SetValue")
|
||||
}
|
||||
|
||||
response, err := handler(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
ctx.Status(http.StatusInternalServerError)
|
||||
} else if validResponse, ok := response.(SetValueResponseObject); ok {
|
||||
if err := validResponse.VisitSetValueResponse(ctx.Writer); err != nil {
|
||||
ctx.Error(err)
|
||||
}
|
||||
} else if response != nil {
|
||||
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
|
||||
}
|
||||
}
|
||||
|
||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"H4sIAAAAAAAC/7STwW7bMAyGX8XgdjRiZ+3Jt26HIYcBwwL0EuSgSEzCwhVVkQ4QFH73QZKTLcuKbgV6",
|
||||
"siH95E9+pJ7B8mNgj14FumcQu8dHk3+/ot6bfsAf+DSgaDoKkQNGJcwCyw7TV48BoQPRSH4H41hDxKeB",
|
||||
"IjroVkW1rk8q3jygVRjr3/JLYC94bXBI1687FNnfLJZvbKH+V+scX79YwViDoB0i6XGZwBbTjRGyd4Pu",
|
||||
"z8BTTD6Fc4q9aoAxZSC/5VwJaZ9uvrDDezP0Wt19XyRzjELsoYP5rJ21qXoO6E0g6OBm1s5uoIZgdJ/N",
|
||||
"GxOoOcyb3YQmM+HCJpExSuwXDrrzfKC0jKKf2R0LNa/oc4gJoSebg5oHSVWcdij9fYy4hQ4+NL+WrJk2",
|
||||
"rPlzvcZLthoHzAdlOXLtn9r2Heyn7cv+DsVGClp4ZkG15cG7RPW22F9qFv5genLVhKjaJEZZfHstLgk9",
|
||||
"6ylpNj2NRF4dyfJ9R7J8y0hebFOUU9j/g5tfi7+RCPldxbGiKc5GdOiVTC8XDw261cUTW63HdeL8MwAA",
|
||||
"//98/iCR6gQAAA==",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
// or error if failed to decode
|
||||
func decodeSpec() ([]byte, error) {
|
||||
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error base64 decoding spec: %w", err)
|
||||
}
|
||||
zr, err := gzip.NewReader(bytes.NewReader(zipped))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decompressing spec: %w", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
_, err = buf.ReadFrom(zr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decompressing spec: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
var rawSpec = decodeSpecCached()
|
||||
|
||||
// a naive cached of a decoded swagger spec
|
||||
func decodeSpecCached() func() ([]byte, error) {
|
||||
data, err := decodeSpec()
|
||||
return func() ([]byte, error) {
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
|
||||
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
|
||||
res := make(map[string]func() ([]byte, error))
|
||||
if len(pathToFile) > 0 {
|
||||
res[pathToFile] = rawSpec
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// GetSwagger returns the Swagger specification corresponding to the generated code
|
||||
// in this file. The external references of Swagger specification are resolved.
|
||||
// The logic of resolving external references is tightly connected to "import-mapping" feature.
|
||||
// Externally referenced files must be embedded in the corresponding golang packages.
|
||||
// Urls can be supported but this task was out of the scope.
|
||||
func GetSwagger() (swagger *openapi3.T, err error) {
|
||||
resolvePath := PathToRawSpec("")
|
||||
|
||||
loader := openapi3.NewLoader()
|
||||
loader.IsExternalRefsAllowed = true
|
||||
loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
|
||||
pathToFile := url.String()
|
||||
pathToFile = path.Clean(pathToFile)
|
||||
getSpec, ok := resolvePath[pathToFile]
|
||||
if !ok {
|
||||
err1 := fmt.Errorf("path not found: %s", pathToFile)
|
||||
return nil, err1
|
||||
}
|
||||
return getSpec()
|
||||
}
|
||||
var specData []byte
|
||||
specData, err = rawSpec()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
swagger, err = loader.LoadFromData(specData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
55
internal/api/handlers.go
Normal file
55
internal/api/handlers.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
|
||||
"github.com/kanootoko/codevault/internal/db"
|
||||
"github.com/kanootoko/codevault/internal/observability/metrics"
|
||||
)
|
||||
|
||||
// StrictHandler implements StrictServerInterface.
|
||||
type StrictHandler struct {
|
||||
Secrets *db.SecretsRepository
|
||||
Metrics metrics.ValueMetrics
|
||||
}
|
||||
|
||||
func NewHandler(secrets *db.SecretsRepository, m metrics.ValueMetrics) *StrictHandler {
|
||||
return &StrictHandler{Secrets: secrets, Metrics: m}
|
||||
}
|
||||
|
||||
func (h *StrictHandler) GetValue(ctx context.Context, request GetValueRequestObject) (GetValueResponseObject, error) {
|
||||
if request.Body == nil || request.Body.Code == "" {
|
||||
return GetValue400Response{}, nil
|
||||
}
|
||||
|
||||
value, err := h.Secrets.Get(ctx, request.Body.Code)
|
||||
if errors.Is(err, db.ErrNotFound) {
|
||||
h.Metrics.GetFailedTotal.Add(ctx, 1, metric.WithAttributes(attribute.String("reason", "not_found")))
|
||||
return GetValue404Response{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.Metrics.GetSuccessTotal.Add(ctx, 1, metric.WithAttributes(attribute.String("code", request.Body.Code)))
|
||||
return GetValue200JSONResponse{Value: value}, nil
|
||||
}
|
||||
|
||||
func (h *StrictHandler) SetValue(ctx context.Context, request SetValueRequestObject) (SetValueResponseObject, error) {
|
||||
if request.Body == nil || request.Body.Code == "" || request.Body.Value == "" || len(request.Body.Value) > 10*1024*1024 {
|
||||
h.Metrics.SetFailedTotal.Add(ctx, 1, metric.WithAttributes(attribute.String("reason", "bad_request")))
|
||||
return SetValue400Response{}, nil
|
||||
}
|
||||
|
||||
if err := h.Secrets.Set(ctx, request.Body.Code, request.Body.Value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
login, _ := LoginFromContext(ctx)
|
||||
h.Metrics.SetSuccessTotal.Add(ctx, 1, metric.WithAttributes(attribute.String("user", login)))
|
||||
return SetValue204Response{}, nil
|
||||
}
|
||||
81
internal/api/middleware.go
Normal file
81
internal/api/middleware.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
|
||||
"github.com/kanootoko/codevault/internal/db"
|
||||
"github.com/kanootoko/codevault/internal/observability/metrics"
|
||||
"github.com/kanootoko/codevault/internal/ratelimit"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const loginContextKey contextKey = "login"
|
||||
|
||||
// LoginFromContext returns the Basic Auth login stashed by BasicAuthMiddleware,
|
||||
// if any. Strict-server handlers only see context.Context, not *gin.Context, so
|
||||
// this is how they recover the authenticated login.
|
||||
func LoginFromContext(ctx context.Context) (string, bool) {
|
||||
login, ok := ctx.Value(loginContextKey).(string)
|
||||
return login, ok
|
||||
}
|
||||
|
||||
// BasicAuthMiddleware checks the request's HTTP Basic Auth credentials against
|
||||
// the DB-backed user store.
|
||||
func BasicAuthMiddleware(users *db.UsersRepository, vm metrics.ValueMetrics) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
login, password, ok := c.Request.BasicAuth()
|
||||
if !ok || users.Verify(c.Request.Context(), login, password) != nil {
|
||||
vm.SetFailedTotal.Add(c.Request.Context(), 1, metric.WithAttributes(attribute.String("reason", "unauthorized")))
|
||||
c.Header("WWW-Authenticate", `Basic realm="codevault"`)
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), loginContextKey, login))
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RateLimitMiddleware short-circuits to 404 once the client IP has accumulated
|
||||
// too many failures, without touching the handler (and thus the database). A
|
||||
// short-circuited request counts as a failure itself, same as a genuine 404 from
|
||||
// the handler.
|
||||
func RateLimitMiddleware(limiter *ratelimit.Limiter, vm metrics.ValueMetrics) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ip := c.ClientIP()
|
||||
if !limiter.Allowed(ip) {
|
||||
limiter.RecordFailure(ip)
|
||||
vm.GetFailedTotal.Add(c.Request.Context(), 1, metric.WithAttributes(attribute.String("reason", "rate_limited")))
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
|
||||
if c.Writer.Status() == http.StatusNotFound {
|
||||
limiter.RecordFailure(ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InstrumentMiddleware records request count and duration metrics per route.
|
||||
func InstrumentMiddleware(route string, m metrics.HTTPMetrics) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
c.Next()
|
||||
duration := time.Since(start).Seconds()
|
||||
|
||||
attrs := metric.WithAttributes(
|
||||
attribute.String("route", route),
|
||||
attribute.Int("status", c.Writer.Status()),
|
||||
)
|
||||
m.RequestsTotal.Add(c.Request.Context(), 1, attrs)
|
||||
m.RequestDuration.Record(c.Request.Context(), duration, metric.WithAttributes(attribute.String("route", route)))
|
||||
}
|
||||
}
|
||||
7
internal/api/oapi-codegen.yaml
Normal file
7
internal/api/oapi-codegen.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
package: api
|
||||
generate:
|
||||
gin-server: true
|
||||
strict-server: true
|
||||
models: true
|
||||
embedded-spec: true
|
||||
output: generated.go
|
||||
73
internal/api/openapi.yaml
Normal file
73
internal/api/openapi.yaml
Normal file
@@ -0,0 +1,73 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: CodeVault API
|
||||
version: "1.0.0"
|
||||
paths:
|
||||
/api/v1/getValue:
|
||||
post:
|
||||
operationId: getValue
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GetValueRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Value found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GetValueResponse'
|
||||
'400':
|
||||
description: Invalid request body
|
||||
'404':
|
||||
description: Value not found
|
||||
/api/v1/setValue:
|
||||
post:
|
||||
operationId: setValue
|
||||
security:
|
||||
- basicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SetValueRequest'
|
||||
responses:
|
||||
'204':
|
||||
description: Value stored
|
||||
'400':
|
||||
description: Invalid request body
|
||||
'401':
|
||||
description: Missing or invalid credentials
|
||||
components:
|
||||
securitySchemes:
|
||||
basicAuth:
|
||||
type: http
|
||||
scheme: basic
|
||||
schemas:
|
||||
GetValueRequest:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
GetValueResponse:
|
||||
type: object
|
||||
required:
|
||||
- value
|
||||
properties:
|
||||
value:
|
||||
type: string
|
||||
SetValueRequest:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
- value
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
35
internal/api/swagger.go
Normal file
35
internal/api/swagger.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
// RegisterSwaggerHandlers serves the OpenAPI spec and an interactive Swagger UI
|
||||
// (embedded swagger-ui-dist assets, no CDN) under /api.
|
||||
func RegisterSwaggerHandlers(router *gin.Engine) error {
|
||||
spec, err := GetSwagger()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error on building swagger specification: %w", err)
|
||||
}
|
||||
specBody, err := spec.MarshalJSON()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error on marshaling swagger specification: %w", err)
|
||||
}
|
||||
|
||||
// /api/docs/*any (rather than /api/*any) so this wildcard doesn't collide with
|
||||
// the static /api/openapi.json route at the same tree level.
|
||||
router.GET("/api/openapi.json", func(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "application/json", specBody)
|
||||
})
|
||||
router.GET("/api", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "/api/docs/index.html")
|
||||
})
|
||||
router.GET("/api/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, ginSwagger.URL("/api/openapi.json")))
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user