openapi API

openapi

package

API reference for the openapi package.

S
struct

Document

Document represents an OpenAPI 3.0.3 document.

pkg/openapi/openapi.go:10-14
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.

pkg/openapi/openapi.go:17-20
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.

pkg/openapi/openapi.go:23-23
type PathItem map[string]Operation
S
struct

Operation

Operation describes a single API operation.

pkg/openapi/openapi.go:26-31
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.

pkg/openapi/openapi.go:34-39
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.

pkg/openapi/openapi.go:42-46
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.

pkg/openapi/openapi.go:49-51
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.

pkg/openapi/openapi.go:54-56
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
pkg/openapi/openapi.go:60-131
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 _, h := range handlers {
		val := reflect.ValueOf(h)
		if val.Kind() == reflect.Ptr {
			val = val.Elem()
		}
		typ := val.Type()

		var method, path string
		extractTags(typ, &method, &path)

		if method == "" || path == "" {
			continue
		}

		op := Operation{
			Responses: map[string]Response{"200": {Description: "OK"}},
		}

		discoverParameters(typ, &op)

		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 params, ok := m["parameters"].([]map[string]any); ok {
				for _, pm := range params {
					p := Parameter{Name: pm["name"].(string), In: pm["in"].(string)}
					if req, ok := pm["required"].(bool); ok {
						p.Required = req
					}
					if sch, ok := pm["schema"].(map[string]any); ok {
						p.Schema.Type, _ = sch["type"].(string)
						if min, ok := sch["minimum"].(int); ok {
							f := float64(min)
							p.Schema.Minimum = &f
						}
					}
					op.Parameters = append(op.Parameters, p)
				}
			}
			if resp, ok := m["responses"].(map[int]any); ok {
				op.Responses = map[string]Response{}
				for code, desc := range resp {
					op.Responses[codeToStr(code)] = Response{Description: desc.(string)}
				}
			}
		}

		pathItem, ok := doc.Paths[path]
		if !ok {
			pathItem = make(PathItem)
		}
		pathItem[method] = op
		doc.Paths[path] = pathItem
	}

	return json.MarshalIndent(doc, "", "  ")
}
F
function

goTypeToOpenAPI

Parameters

Returns

string
pkg/openapi/openapi.go:133-146
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
pkg/openapi/openapi.go:148-154
func codeToStr(code int) string

{
	s := ""
	s += string(rune('0' + code/100))
	s += string(rune('0' + (code/10)%10))
	s += string(rune('0' + code%10))
	return s
}
F
function

extractTags

Parameters

method
*string
path
*string
pkg/openapi/openapi.go:156-169
func extractTags(typ reflect.Type, method, path *string)

{
	for i := 0; i < typ.NumField(); i++ {
		field := typ.Field(i)
		if field.Anonymous {
			extractTags(field.Type, method, path)
		}
		if m := field.Tag.Get("method"); m != "" {
			*method = strings.ToLower(m)
		}
		if p := field.Tag.Get("path"); p != "" {
			*path = p
		}
	}
}
F
function

discoverParameters

Parameters

pkg/openapi/openapi.go:171-191
func discoverParameters(typ reflect.Type, op *Operation)

{
	for i := 0; i < typ.NumField(); i++ {
		field := typ.Field(i)
		if field.Anonymous {
			discoverParameters(field.Type, op)
			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)
		}
	}
}
S
struct
Implements: MetaProvider

TestEndpoint

pkg/openapi/openapi_test.go:8-12
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

pkg/openapi/openapi_test.go:21-53
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

pkg/openapi/openapi_test.go:55-75
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.Name == "name" && p.In == "query" {
			foundQuery = true
		}
	}
	if !foundQuery {
		t.Error("expected query parameter 'name'")
	}
}
F
function

TestBuild_NoTags

Parameters

pkg/openapi/openapi_test.go:77-83
func TestBuild_NoTags(t *testing.T)

{
	type NoTags struct{}
	_, err := Build("Test", "1.0.0", &NoTags{})
	if err != nil {
		t.Fatalf("Build: %v", err)
	}
}
F
function

TestGoTypeToOpenAPI

Parameters

pkg/openapi/openapi_test.go:85-101
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
		})
	}
}