openapi API

openapi

package

API 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"
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"
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

OpenAPIMeta
Method

Returns

map[string]any
func OpenAPIMeta(...)
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

method
*string
path
*string
active
map[reflect.Type]struct{}
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

op
active
map[reflect.Type]struct{}
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

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
struct
Implements: MetaProvider

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

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

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

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

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

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

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

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

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
struct
Implements: MetaProvider

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

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

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

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