openapi
API
openapi
packageAPI reference for the openapi
package.
S
struct
Document
Document represents an OpenAPI 3.0.3 document.
core/openapi/openapi.go:13-17
type Document struct
Fields
| Name | Type | Description |
|---|---|---|
| OpenAPI | string | json:"openapi" |
| Info | Info | json:"info" |
| Paths | map[string]PathItem | json:"paths" |
Uses
S
struct
Info
Info holds the API title and version.
core/openapi/openapi.go:20-23
type Info struct
Fields
| Name | Type | Description |
|---|---|---|
| Title | string | json:"title" |
| Version | string | json:"version" |
T
type
PathItem
PathItem maps HTTP methods to operations for a path.
core/openapi/openapi.go:26-26
type PathItem map[string]Operation
S
struct
Operation
Operation describes a single API operation.
core/openapi/openapi.go:29-34
type Operation struct
Fields
| Name | Type | Description |
|---|---|---|
| Summary | string | json:"summary,omitempty" |
| Description | string | json:"description,omitempty" |
| Parameters | []Parameter | json:"parameters,omitempty" |
| Responses | map[string]Response | json:"responses" |
S
struct
Parameter
Parameter describes an operation parameter.
core/openapi/openapi.go:37-42
type Parameter struct
Fields
| Name | Type | Description |
|---|---|---|
| Name | string | json:"name" |
| In | string | json:"in" |
| Required | bool | json:"required" |
| Schema | Schema | json:"schema" |
Uses
S
struct
Schema
Schema describes a parameter schema.
core/openapi/openapi.go:45-49
type Schema struct
Fields
| Name | Type | Description |
|---|---|---|
| Type | string | json:"type,omitempty" |
| Minimum | *float64 | json:"minimum,omitempty" |
| Enum | []string | json:"enum,omitempty" |
S
struct
Response
Response describes an operation response.
core/openapi/openapi.go:52-54
type Response struct
Fields
| Name | Type | Description |
|---|---|---|
| Description | string | json:"description" |
I
interface
MetaProvider
MetaProvider is an optional interface endpoints can implement for OpenAPI metadata.
core/openapi/openapi.go:57-59
type MetaProvider interface
Methods
F
function
Build
Build generates an OpenAPI 3.0.3 JSON document from struct-tagged handlers.
Each handler must have method and path struct tags.
Parameters
title
string
version
string
handlers
...any
Returns
[]byte
error
core/openapi/openapi.go:63-181
func Build(title, version string, handlers ...any) ([]byte, error)
{
doc := Document{
OpenAPI: "3.0.3",
Info: Info{
Title: title,
Version: version,
},
Paths: map[string]PathItem{},
}
for index, h := range handlers {
val := reflect.ValueOf(h)
if !val.IsValid() {
return nil, fmt.Errorf("openapi: handler %d is nil", index)
}
for val.Kind() == reflect.Ptr {
if val.IsNil() {
return nil, fmt.Errorf("openapi: handler %d is a nil pointer", index)
}
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return nil, fmt.Errorf("openapi: handler %d must be a struct or pointer to struct", index)
}
typ := val.Type()
var method, path string
extractTags(typ, &method, &path, make(map[reflect.Type]struct{}))
if method == "" || path == "" {
return nil, fmt.Errorf("openapi: handler %d must declare method and path", index)
}
if !validHTTPMethod(method) {
return nil, fmt.Errorf("openapi: handler %d has unsupported HTTP method %q", index, method)
}
openAPIPath, pathParameters, err := normalizePath(path)
if err != nil {
return nil, fmt.Errorf("openapi: handler %d: %w", index, err)
}
op := Operation{
Responses: map[string]Response{"200": {Description: "OK"}},
}
discoverParameters(typ, &op, make(map[reflect.Type]struct{}))
if mp, ok := h.(MetaProvider); ok {
m := mp.OpenAPIMeta()
if s, ok := m["summary"].(string); ok {
op.Summary = s
}
if d, ok := m["description"].(string); ok {
op.Description = d
}
if paramsValue, exists := m["parameters"]; exists {
params, ok := paramsValue.([]map[string]any)
if !ok {
return nil, fmt.Errorf("openapi: handler %d metadata parameters must be []map[string]any", index)
}
for _, pm := range params {
name, nameOK := pm["name"].(string)
location, locationOK := pm["in"].(string)
if !nameOK || name == "" || !locationOK || location == "" {
return nil, fmt.Errorf("openapi: handler %d metadata parameter requires string name and in", index)
}
p := Parameter{Name: name, In: location}
if req, ok := pm["required"].(bool); ok {
p.Required = req
}
if schemaValue, exists := pm["schema"]; exists {
sch, ok := schemaValue.(map[string]any)
if !ok {
return nil, fmt.Errorf("openapi: handler %d metadata parameter schema must be map[string]any", index)
}
p.Schema.Type, _ = sch["type"].(string)
if minimum, exists := sch["minimum"]; exists {
min, ok := numericValue(minimum)
if !ok {
return nil, fmt.Errorf("openapi: handler %d metadata minimum must be numeric", index)
}
p.Schema.Minimum = &min
}
}
op.Parameters = append(op.Parameters, p)
}
}
if responsesValue, exists := m["responses"]; exists {
resp, ok := responsesValue.(map[int]any)
if !ok {
return nil, fmt.Errorf("openapi: handler %d metadata responses must be map[int]any", index)
}
op.Responses = map[string]Response{}
for code, desc := range resp {
description, ok := desc.(string)
if !ok {
return nil, fmt.Errorf("openapi: handler %d response %d description must be a string", index, code)
}
op.Responses[codeToStr(code)] = Response{Description: description}
}
}
}
if err := validateParameters(op.Parameters, pathParameters); err != nil {
return nil, fmt.Errorf("openapi: handler %d: %w", index, err)
}
pathItem, ok := doc.Paths[openAPIPath]
if !ok {
pathItem = make(PathItem)
}
if _, exists := pathItem[method]; exists {
return nil, fmt.Errorf("openapi: duplicate operation %s %s", strings.ToUpper(method), openAPIPath)
}
pathItem[method] = op
doc.Paths[openAPIPath] = pathItem
}
return json.MarshalIndent(doc, "", " ")
}
F
function
validHTTPMethod
Parameters
method
string
Returns
bool
core/openapi/openapi.go:183-192
func validHTTPMethod(method string) bool
{
switch strings.ToUpper(method) {
case http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut,
http.MethodPatch, http.MethodDelete, http.MethodConnect,
http.MethodOptions, http.MethodTrace:
return true
default:
return false
}
}
F
function
goTypeToOpenAPI
Parameters
Returns
string
core/openapi/openapi.go:194-207
func goTypeToOpenAPI(t reflect.Type) string
{
switch t.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return "integer"
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return "integer"
case reflect.Float32, reflect.Float64:
return "number"
case reflect.Bool:
return "boolean"
default:
return "string"
}
}
F
function
codeToStr
Parameters
code
int
Returns
string
core/openapi/openapi.go:209-211
func codeToStr(code int) string
{
return strconv.Itoa(code)
}
F
function
extractTags
Parameters
core/openapi/openapi.go:213-234
func extractTags(typ reflect.Type, method, path *string, active map[reflect.Type]struct{})
{
typ = indirectType(typ)
if typ == nil || typ.Kind() != reflect.Struct {
return
}
if _, exists := active[typ]; exists {
return
}
active[typ] = struct{}{}
defer delete(active, typ)
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if field.Anonymous {
extractTags(field.Type, method, path, active)
}
if m := field.Tag.Get("method"); m != "" {
*method = strings.ToLower(m)
*path = field.Tag.Get("path")
}
}
}
F
function
discoverParameters
Parameters
core/openapi/openapi.go:236-269
func discoverParameters(typ reflect.Type, op *Operation, active map[reflect.Type]struct{})
{
typ = indirectType(typ)
if typ == nil || typ.Kind() != reflect.Struct {
return
}
if _, exists := active[typ]; exists {
return
}
active[typ] = struct{}{}
defer delete(active, typ)
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if field.Tag.Get("method") != "" {
continue
}
if field.Anonymous {
discoverParameters(field.Type, op, active)
continue
}
if q := field.Tag.Get("query"); q != "" {
p := Parameter{Name: q, In: "query", Schema: Schema{Type: goTypeToOpenAPI(field.Type)}}
op.Parameters = append(op.Parameters, p)
}
if p := field.Tag.Get("path"); p != "" {
param := Parameter{Name: p, In: "path", Required: true, Schema: Schema{Type: goTypeToOpenAPI(field.Type)}}
op.Parameters = append(op.Parameters, param)
}
if h := field.Tag.Get("header"); h != "" {
param := Parameter{Name: h, In: "header", Schema: Schema{Type: goTypeToOpenAPI(field.Type)}}
op.Parameters = append(op.Parameters, param)
}
}
}
F
function
normalizePath
Parameters
path
string
Returns
string
map[string]struct{}
error
core/openapi/openapi.go:271-306
func normalizePath(path string) (string, map[string]struct{}, error)
{
if !strings.HasPrefix(path, "/") {
return "", nil, fmt.Errorf("path must start with /")
}
parameters := make(map[string]struct{})
segments := strings.Split(path, "/")
for index, segment := range segments {
if !strings.ContainsAny(segment, "{}") {
continue
}
if len(segment) < 3 || segment[0] != '{' || segment[len(segment)-1] != '}' {
return "", nil, fmt.Errorf("malformed path segment %q", segment)
}
definition := segment[1 : len(segment)-1]
if strings.HasPrefix(definition, "*") {
definition = strings.TrimPrefix(definition, "*")
if index != len(segments)-1 || strings.Contains(definition, ":") {
return "", nil, fmt.Errorf("malformed catch-all path segment %q", segment)
}
} else if separator := strings.IndexByte(definition, ':'); separator >= 0 {
definition = definition[:separator]
}
if !validParameterName(definition) {
return "", nil, fmt.Errorf("invalid path parameter name %q", definition)
}
if _, exists := parameters[definition]; exists {
return "", nil, fmt.Errorf("duplicate path parameter %q", definition)
}
parameters[definition] = struct{}{}
segments[index] = "{" + definition + "}"
}
return strings.Join(segments, "/"), parameters, nil
}
F
function
validParameterName
Parameters
name
string
Returns
bool
core/openapi/openapi.go:308-329
func validParameterName(name string) bool
{
if name == "" {
return false
}
for index, character := range name {
if index == 0 {
if character != '_' &&
(character < 'A' || character > 'Z') &&
(character < 'a' || character > 'z') {
return false
}
continue
}
if character != '_' &&
(character < 'A' || character > 'Z') &&
(character < 'a' || character > 'z') &&
(character < '0' || character > '9') {
return false
}
}
return true
}
F
function
validateParameters
Parameters
parameters
pathParameters
map[string]struct{}
Returns
error
core/openapi/openapi.go:331-358
func validateParameters(parameters []Parameter, pathParameters map[string]struct{}) error
{
seen := make(map[string]struct{}, len(parameters))
matchedPathParameters := make(map[string]struct{}, len(pathParameters))
for _, parameter := range parameters {
key := parameter.In + "\x00" + parameter.Name
if _, exists := seen[key]; exists {
return fmt.Errorf("duplicate %s parameter %q", parameter.In, parameter.Name)
}
seen[key] = struct{}{}
if parameter.In != "path" {
continue
}
if _, exists := pathParameters[parameter.Name]; !exists {
return fmt.Errorf("path parameter %q is not present in the route", parameter.Name)
}
if !parameter.Required {
return fmt.Errorf("path parameter %q must be required", parameter.Name)
}
matchedPathParameters[parameter.Name] = struct{}{}
}
for name := range pathParameters {
if _, exists := matchedPathParameters[name]; !exists {
return fmt.Errorf("route path parameter %q has no matching field", name)
}
}
return nil
}
F
function
indirectType
Parameters
typ
Returns
core/openapi/openapi.go:360-365
func indirectType(typ reflect.Type) reflect.Type
{
for typ != nil && typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
return typ
}
F
function
numericValue
Parameters
value
any
Returns
float64
bool
core/openapi/openapi.go:367-378
func numericValue(value any) (float64, bool)
{
switch number := value.(type) {
case int:
return float64(number), true
case int64:
return float64(number), true
case float64:
return number, true
default:
return 0, false
}
}
S
TestEndpoint
foundation:ignore handler
core/openapi/openapi_test.go:11-15
type TestEndpoint struct
Methods
OpenAPIMeta
Method
Returns
map[string]any
func (*TestEndpoint) OpenAPIMeta() map[string]any
{
return map[string]any{
"summary": "Test endpoint",
"description": "A test endpoint",
}
}
Fields
| Name | Type | Description |
|---|---|---|
| Meta | struct{} | method:"GET" path:"/api/v1/test" |
| Name | string | query:"name" |
| Age | int | query:"age" |
F
function
TestBuild_BasicDocument
Parameters
t
core/openapi/openapi_test.go:24-56
func TestBuild_BasicDocument(t *testing.T)
{
doc, err := Build("Test API", "1.0.0", &TestEndpoint{})
if err != nil {
t.Fatalf("Build: %v", err)
}
var result Document
if err := json.Unmarshal(doc, &result); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if result.OpenAPI != "3.0.3" {
t.Errorf("OpenAPI = %q, want 3.0.3", result.OpenAPI)
}
if result.Info.Title != "Test API" {
t.Errorf("Title = %q, want Test API", result.Info.Title)
}
if result.Info.Version != "1.0.0" {
t.Errorf("Version = %q, want 1.0.0", result.Info.Version)
}
pathItem, ok := result.Paths["/api/v1/test"]
if !ok {
t.Fatal("path /api/v1/test not found")
}
op, ok := pathItem["get"]
if !ok {
t.Fatal("method get not found")
}
if op.Summary != "Test endpoint" {
t.Errorf("Summary = %q, want Test endpoint", op.Summary)
}
}
F
function
TestBuild_AutoParameters
Parameters
t
core/openapi/openapi_test.go:58-81
func TestBuild_AutoParameters(t *testing.T)
{
doc, err := Build("Test", "1.0.0", &TestEndpoint{})
if err != nil {
t.Fatalf("Build: %v", err)
}
var result Document
json.Unmarshal(doc, &result)
op := result.Paths["/api/v1/test"]["get"]
foundQuery := false
for _, p := range op.Parameters {
if p.In == "path" {
t.Errorf("unexpected path parameter %q", p.Name)
}
if p.Name == "name" && p.In == "query" {
foundQuery = true
}
}
if !foundQuery {
t.Error("expected query parameter 'name'")
}
}
F
function
TestBuildNormalizesConstrainedAndCatchAllPaths
Parameters
t
core/openapi/openapi_test.go:83-108
func TestBuildNormalizesConstrainedAndCatchAllPaths(t *testing.T)
{
type constrainedEndpoint struct {
Meta struct{} `method:"GET" path:"/users/{id:int}"`
ID int `path:"id"`
}
type catchAllEndpoint struct {
Meta struct{} `method:"GET" path:"/static/{*filepath}"`
Path string `path:"filepath"`
}
doc, err := Build("Test", "1.0.0", &constrainedEndpoint{}, &catchAllEndpoint{})
if err != nil {
t.Fatalf("Build: %v", err)
}
var result Document
if err := json.Unmarshal(doc, &result); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if _, exists := result.Paths["/users/{id}"]; !exists {
t.Fatal("constrained path was not normalized")
}
if _, exists := result.Paths["/static/{filepath}"]; !exists {
t.Fatal("catch-all path was not normalized")
}
}
F
function
TestBuildRejectsMismatchedPathParameters
Parameters
t
core/openapi/openapi_test.go:110-128
func TestBuildRejectsMismatchedPathParameters(t *testing.T)
{
type missingField struct {
Meta struct{} `method:"GET" path:"/users/{id:int}"`
}
type extraField struct {
Meta struct{} `method:"GET" path:"/users"`
UserID int `path:"id"`
}
type wrongField struct {
Meta struct{} `method:"GET" path:"/users/{id}"`
UserID int `path:"user_id"`
}
for _, handler := range []any{&missingField{}, &extraField{}, &wrongField{}} {
if _, err := Build("Test", "1.0.0", handler); err == nil {
t.Fatalf("Build() accepted mismatched path parameters on %#v", handler)
}
}
}
F
function
TestBuild_NoTags
Parameters
t
core/openapi/openapi_test.go:130-136
func TestBuild_NoTags(t *testing.T)
{
type NoTags struct{}
_, err := Build("Test", "1.0.0", &NoTags{})
if err == nil {
t.Fatal("Build() accepted a handler without route tags")
}
}
F
function
TestBuildRejectsInvalidRoutes
Parameters
t
core/openapi/openapi_test.go:138-150
func TestBuildRejectsInvalidRoutes(t *testing.T)
{
type invalidMethod struct {
Meta struct{} `method:"FETCH" path:"/items"`
}
type invalidPath struct {
Meta struct{} `method:"GET" path:"items"`
}
for _, handler := range []any{&invalidMethod{}, &invalidPath{}} {
if _, err := Build("Test", "1.0.0", handler); err == nil {
t.Fatalf("Build() accepted invalid route %#v", handler)
}
}
}
F
function
TestGoTypeToOpenAPI
Parameters
t
core/openapi/openapi_test.go:152-168
func TestGoTypeToOpenAPI(t *testing.T)
{
tests := []struct {
kind string
goType string
want string
}{
{"int", "int", "integer"},
{"string", "string", "string"},
{"float64", "float64", "number"},
{"bool", "bool", "boolean"},
}
for _, tt := range tests {
t.Run(tt.kind, func(t *testing.T) {
_ = tt.want
})
}
}
F
function
TestBuildRejectsInvalidHandlers
Parameters
t
core/openapi/openapi_test.go:170-177
func TestBuildRejectsInvalidHandlers(t *testing.T)
{
var typedNil *TestEndpoint
for _, handler := range []any{nil, typedNil, 42} {
if _, err := Build("Test", "1.0.0", handler); err == nil {
t.Fatalf("Build() accepted invalid handler %#v", handler)
}
}
}
S
invalidMetadataEndpoint
foundation:ignore handler
core/openapi/openapi_test.go:180-182
type invalidMetadataEndpoint struct
Methods
OpenAPIMeta
Method
Returns
map[string]any
func (*invalidMetadataEndpoint) OpenAPIMeta() map[string]any
{
return map[string]any{
"parameters": []map[string]any{{"name": 1, "in": "query"}},
}
}
Fields
| Name | Type | Description |
|---|---|---|
| Meta | struct{} | method:"GET" path:"/invalid" |
F
function
TestBuildRejectsMalformedMetadata
Parameters
t
core/openapi/openapi_test.go:190-194
func TestBuildRejectsMalformedMetadata(t *testing.T)
{
if _, err := Build("Test", "1.0.0", &invalidMetadataEndpoint{}); err == nil {
t.Fatal("Build() accepted malformed metadata")
}
}
F
function
TestBuildRejectsDuplicateOperations
Parameters
t
core/openapi/openapi_test.go:196-205
func TestBuildRejectsDuplicateOperations(t *testing.T)
{
if _, err := Build(
"Test",
"1.0.0",
&TestEndpoint{},
&TestEndpoint{},
); err == nil {
t.Fatal("Build() accepted duplicate method and path")
}
}
S
struct
recursiveEndpoint
core/openapi/openapi_test.go:207-210
type recursiveEndpoint struct
Fields
| Name | Type | Description |
|---|---|---|
| Meta | struct{} | method:"GET" path:"/recursive" |
F
function
TestBuildHandlesRecursiveEmbedding
Parameters
t
core/openapi/openapi_test.go:212-216
func TestBuildHandlesRecursiveEmbedding(t *testing.T)
{
if _, err := Build("Test", "1.0.0", &recursiveEndpoint{}); err != nil {
t.Fatalf("Build: %v", err)
}
}