package api_gen import ( "encoding/json" "go/types" "os" "path/filepath" ) type openapiResp struct { Description string `json:"description"` Content map[string]openapiContent `json:"content"` } type openapiContent struct { Schema openapiType `json:"schema"` } type openapiReq struct { Required bool `json:"required"` Content map[string]openapiContent `json:"content"` } type openapiEndpoint struct { OperationId string `json:"operationId"` RequestBody openapiReq `json:"requestBody"` Responses map[string]openapiResp `json:"responses"` } type openapiType struct { Ref string `json:"$ref,omitempty"` Type string `json:"type,omitempty"` Format string `json:"format,omitempty"` Properties map[string]openapiType `json:"properties,omitempty"` Items *openapiType `json:"items,omitempty"` } type openapiServer struct { Url string `json:"url"` Description string `json:"description"` } type openapiDoc struct { Openapi string `json:"openapi"` Info struct { Title string `json:"title"` Version string `json:"version"` Description string `json:"description"` } `json:"info"` Servers []openapiServer `json:"servers"` Paths map[string]map[string]openapiEndpoint `json:"paths"` Components struct { Schemas map[string]openapiType `json:"schemas"` } `json:"components"` } func generateOpenAPI(outputDir string, pkg *APIPackage, cfg Config) error { var doc openapiDoc doc.Openapi = "3.0.0" doc.Info.Title = cfg.Title doc.Info.Version = cfg.Version doc.Info.Description = cfg.Description doc.Servers = append(doc.Servers, openapiServer{ Url: cfg.ServerURL, Description: cfg.ServerDescription, }) doc.Components.Schemas = map[string]openapiType{ "core.ID": {Type: "string", Format: "core.ID"}, "time.Time": {Type: "string", Format: "date-time"}, } g := &openapiGen{ schemas: doc.Components.Schemas, localPath: pkg.PkgPath, known: make(map[string]bool), } for name := range doc.Components.Schemas { g.known[name] = true } for name := range pkg.Types { g.known[name] = true } doc.Paths = make(map[string]map[string]openapiEndpoint) for _, e := range pkg.Endpoints { doc.Paths[e.Path] = map[string]openapiEndpoint{ e.Method: { OperationId: e.Name, RequestBody: openapiReq{ Required: true, Content: jsonContent(refSchema(e.ReqType)), }, Responses: map[string]openapiResp{ "200": { Description: "Success", Content: jsonContent(refSchema(e.ResType)), }, "400": { Description: "Bad request (invalid JSON or failed validation)", Content: jsonContent(refSchema("ErrorResponse")), }, "401": { Description: "Unauthorized", Content: jsonContent(refSchema("ErrorResponse")), }, "403": { Description: "Forbidden", Content: jsonContent(refSchema("ErrorResponse")), }, "500": { Description: "Internal server error", Content: jsonContent(refSchema("ErrorResponse")), }, }, }, } } for name, t := range pkg.Types { // Expand the underlying type so the top-level schema is the // actual structure; nested fields still emit $ref via convert. doc.Components.Schemas[name] = g.convert(t.Underlying()) } b, err := json.MarshalIndent(doc, "", " ") if err != nil { return err } return os.WriteFile(filepath.Join(outputDir, "openapi.json"), b, 0644) } func refSchema(name string) openapiType { return openapiType{Ref: "#/components/schemas/" + name} } func jsonContent(schema openapiType) map[string]openapiContent { return map[string]openapiContent{ "application/json": {Schema: schema}, } } type openapiGen struct { schemas map[string]openapiType known map[string]bool localPath string } func (g *openapiGen) convert(t types.Type) openapiType { if p, ok := t.(*types.Pointer); ok { return g.convert(p.Elem()) } if named, ok := t.(*types.Named); ok { if key, ok := g.namedKey(named); ok && g.known[key] { return refSchema(key) } } if tt, ok := t.(*types.Basic); ok { return openapiType{Type: convertBasicType(tt)} } if tt, ok := t.Underlying().(*types.Basic); ok { return openapiType{Type: convertBasicType(tt)} } if sl, ok := t.(*types.Slice); ok { it := g.convert(sl.Elem()) return openapiType{Type: "array", Items: &it} } if st, ok := t.Underlying().(*types.Struct); ok { props := make(map[string]openapiType) for f := range st.Fields() { if !f.Exported() { continue } props[f.Name()] = g.convert(f.Type()) } return openapiType{Type: "object", Properties: props} } return openapiType{Type: "string"} } func (g *openapiGen) namedKey(n *types.Named) (string, bool) { obj := n.Obj() pkg := obj.Pkg() if pkg == nil { return "", false } if pkg.Path() == g.localPath { return obj.Name(), true } return pkg.Name() + "." + obj.Name(), true } func convertBasicType(t *types.Basic) string { switch t.Kind() { case types.Bool: return "boolean" case types.Float32, types.Float64: return "number" case types.Int, types.Int16, types.Int32, types.Int64, types.Int8: return "integer" case types.String: return "string" case types.Uint, types.Uint16, types.Uint32, types.Uint64, types.Uint8: return "integer" default: return "string" } }