230 lines
6.2 KiB
Go
230 lines
6.2 KiB
Go
// Package openapi provides an OpenAPI 3.0 specification builder and documentation endpoints.
|
|
//
|
|
// It includes:
|
|
// - OpenAPISpec: A builder for constructing OpenAPI 3.0 specifications
|
|
// - Schema helpers: Typed schema constructors (String, Int, Object, Array, etc.)
|
|
// - Parameter helpers: Path, query, header parameter builders
|
|
// - Documentation: Scalar UI and JSON spec serving via EnableDocs
|
|
//
|
|
// Example:
|
|
//
|
|
// spec := openapi.NewOpenAPISpec("My Service", "1.0.0").
|
|
// WithDescription("Service API documentation").
|
|
// WithBearerSecurity("bearer", "JWT authentication")
|
|
//
|
|
// spec.AddPath("/api/v1/items", "get", map[string]any{
|
|
// "summary": "List items",
|
|
// "tags": []string{"Items"},
|
|
// "responses": map[string]any{
|
|
// "200": openapi.OpResponse("Success", openapi.RefArray("Item")),
|
|
// },
|
|
// })
|
|
//
|
|
// application.EnableDocs(spec)
|
|
package openapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sync"
|
|
)
|
|
|
|
// OpenAPIInfo contains metadata about the API.
|
|
type OpenAPIInfo struct {
|
|
Title string `json:"title"`
|
|
Description string `json:"description,omitempty"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// OpenAPIServer describes a server endpoint.
|
|
type OpenAPIServer struct {
|
|
URL string `json:"url"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
|
|
// OpenAPIComponents contains reusable schema definitions.
|
|
type OpenAPIComponents struct {
|
|
Schemas map[string]any `json:"schemas,omitempty"`
|
|
SecuritySchemes map[string]any `json:"securitySchemes,omitempty"`
|
|
}
|
|
|
|
// OpenAPISpec represents a minimal OpenAPI 3.0 specification.
|
|
type OpenAPISpec struct {
|
|
OpenAPI string `json:"openapi"`
|
|
Info OpenAPIInfo `json:"info"`
|
|
Servers []OpenAPIServer `json:"servers,omitempty"`
|
|
Paths map[string]map[string]any `json:"paths"`
|
|
Tags []OpenAPITag `json:"tags,omitempty"`
|
|
Components *OpenAPIComponents `json:"components,omitempty"`
|
|
Security []map[string][]string `json:"security,omitempty"`
|
|
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// OpenAPITag groups operations together.
|
|
type OpenAPITag struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
|
|
// NewOpenAPISpec creates a new OpenAPI specification builder.
|
|
func NewOpenAPISpec(title, version string) *OpenAPISpec {
|
|
return &OpenAPISpec{
|
|
OpenAPI: "3.0.3",
|
|
Info: OpenAPIInfo{
|
|
Title: title,
|
|
Version: version,
|
|
},
|
|
Paths: make(map[string]map[string]any),
|
|
}
|
|
}
|
|
|
|
// WithDescription sets the API description.
|
|
func (s *OpenAPISpec) WithDescription(desc string) *OpenAPISpec {
|
|
s.Info.Description = desc
|
|
return s
|
|
}
|
|
|
|
// WithServer adds a server to the spec.
|
|
func (s *OpenAPISpec) WithServer(url, description string) *OpenAPISpec {
|
|
s.Servers = append(s.Servers, OpenAPIServer{
|
|
URL: url,
|
|
Description: description,
|
|
})
|
|
return s
|
|
}
|
|
|
|
// WithTag adds a tag for grouping operations.
|
|
func (s *OpenAPISpec) WithTag(name, description string) *OpenAPISpec {
|
|
s.Tags = append(s.Tags, OpenAPITag{
|
|
Name: name,
|
|
Description: description,
|
|
})
|
|
return s
|
|
}
|
|
|
|
// AddPath adds an operation to the spec.
|
|
// method should be lowercase (get, post, put, patch, delete).
|
|
func (s *OpenAPISpec) AddPath(path, method string, operation map[string]any) *OpenAPISpec {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if s.Paths[path] == nil {
|
|
s.Paths[path] = make(map[string]any)
|
|
}
|
|
s.Paths[path][method] = operation
|
|
return s
|
|
}
|
|
|
|
// ensureComponents initializes the Components field if nil.
|
|
// Must be called while holding s.mu.
|
|
func (s *OpenAPISpec) ensureComponents() {
|
|
if s.Components == nil {
|
|
s.Components = &OpenAPIComponents{
|
|
Schemas: make(map[string]any),
|
|
SecuritySchemes: make(map[string]any),
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithSchema adds a reusable schema to components/schemas.
|
|
func (s *OpenAPISpec) WithSchema(name string, schema Schema) *OpenAPISpec {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.ensureComponents()
|
|
if s.Components.Schemas == nil {
|
|
s.Components.Schemas = make(map[string]any)
|
|
}
|
|
s.Components.Schemas[name] = schema
|
|
return s
|
|
}
|
|
|
|
// WithAPIKeySecurity adds API key security scheme.
|
|
func (s *OpenAPISpec) WithAPIKeySecurity(name, headerName, description string) *OpenAPISpec {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.ensureComponents()
|
|
if s.Components.SecuritySchemes == nil {
|
|
s.Components.SecuritySchemes = make(map[string]any)
|
|
}
|
|
s.Components.SecuritySchemes[name] = map[string]any{
|
|
"type": "apiKey",
|
|
"in": "header",
|
|
"name": headerName,
|
|
"description": description,
|
|
}
|
|
return s
|
|
}
|
|
|
|
// WithBearerSecurity adds Bearer token security scheme.
|
|
func (s *OpenAPISpec) WithBearerSecurity(name, description string) *OpenAPISpec {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.ensureComponents()
|
|
if s.Components.SecuritySchemes == nil {
|
|
s.Components.SecuritySchemes = make(map[string]any)
|
|
}
|
|
s.Components.SecuritySchemes[name] = map[string]any{
|
|
"type": "http",
|
|
"scheme": "bearer",
|
|
"bearerFormat": "JWT",
|
|
"description": description,
|
|
}
|
|
return s
|
|
}
|
|
|
|
// WithGlobalSecurity sets global security requirements.
|
|
func (s *OpenAPISpec) WithGlobalSecurity(schemeName string) *OpenAPISpec {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.Security = append(s.Security, map[string][]string{
|
|
schemeName: {},
|
|
})
|
|
return s
|
|
}
|
|
|
|
// JSON returns the spec as JSON bytes.
|
|
func (s *OpenAPISpec) JSON() ([]byte, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return json.MarshalIndent(s, "", " ")
|
|
}
|
|
|
|
// Op creates an OpenAPI operation helper.
|
|
func Op(summary, description string, tags ...string) map[string]any {
|
|
return map[string]any{
|
|
"summary": summary,
|
|
"description": description,
|
|
"tags": tags,
|
|
"responses": map[string]any{
|
|
"200": map[string]any{"description": "Success"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// OpWithBody creates an OpenAPI operation with a request body.
|
|
func OpWithBody(summary, description string, tags ...string) map[string]any {
|
|
return map[string]any{
|
|
"summary": summary,
|
|
"description": description,
|
|
"tags": tags,
|
|
"requestBody": map[string]any{
|
|
"required": true,
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"schema": map[string]any{
|
|
"type": "object",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{"description": "Success"},
|
|
"201": map[string]any{"description": "Created"},
|
|
},
|
|
}
|
|
}
|