371 lines
11 KiB
Go
371 lines
11 KiB
Go
package apigen
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"go/ast"
|
|
goparser "go/parser"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.yuntuo.hk/k2safe/go-omni/contract"
|
|
"git.yuntuo.hk/k2safe/go-omni/openapi"
|
|
)
|
|
|
|
// Named response types form a graph, not an infinitely expanded reflect.StructOf
|
|
// tree. Compile each object's local fields with the existing contract compiler,
|
|
// then link named fields through OpenAPI references. Request binders still use
|
|
// the complete, nonrecursive reflection tree.
|
|
type responseGraph struct {
|
|
types map[string]Type
|
|
exprs map[string][]ast.Expr
|
|
recursive map[string]bool // includes types that can reach a cycle
|
|
scalars map[string]reflect.Type
|
|
scalarDefs []contract.Scalar
|
|
components map[string]*openapi.Schema
|
|
}
|
|
|
|
func newResponseGraph(spec *Spec, types map[string]Type, scalars map[string]reflect.Type, defs []contract.Scalar) (*responseGraph, error) {
|
|
g := &responseGraph{types: types, exprs: map[string][]ast.Expr{}, recursive: map[string]bool{}, scalars: scalars, scalarDefs: defs, components: map[string]*openapi.Schema{}}
|
|
dependencies, values := map[string][]string{}, map[string][]string{}
|
|
for _, typ := range spec.Types {
|
|
seen := map[string]bool{}
|
|
for _, f := range typ.Fields {
|
|
if !exportedName.MatchString(f.Name) || seen[f.Name] {
|
|
return nil, fmt.Errorf("%s: invalid or duplicate field %s", typ.Name, f.Name)
|
|
}
|
|
seen[f.Name] = true
|
|
e, err := goparser.ParseExpr(f.Type)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Validate every expression, including otherwise unused declarations.
|
|
if _, err = g.expression(e, 0, func(name string, _ int) (reflect.Type, error) {
|
|
dependencies[typ.Name] = append(dependencies[typ.Name], name)
|
|
return reflect.TypeOf(struct{}{}), nil
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("%s.%s: %w", typ.Name, f.Name, err)
|
|
}
|
|
g.exprs[typ.Name] = append(g.exprs[typ.Name], e)
|
|
if id, ok := e.(*ast.Ident); ok && types[id.Name].Name != "" {
|
|
values[typ.Name] = append(values[typ.Name], id.Name)
|
|
}
|
|
}
|
|
}
|
|
// Only pointer/slice/map indirection can break a Go struct layout cycle.
|
|
state := map[string]int{}
|
|
var valueCycle func(string) error
|
|
valueCycle = func(name string) error {
|
|
if state[name] == 1 {
|
|
return fmt.Errorf("%s: invalid recursive value type; use a pointer, slice or map", name)
|
|
}
|
|
if state[name] == 2 {
|
|
return nil
|
|
}
|
|
state[name] = 1
|
|
for _, next := range values[name] {
|
|
if err := valueCycle(next); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
state[name] = 2
|
|
return nil
|
|
}
|
|
for _, typ := range spec.Types {
|
|
if err := valueCycle(typ.Name); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
state = map[string]int{}
|
|
var cyclic func(string) bool
|
|
cyclic = func(name string) bool {
|
|
if state[name] == 1 {
|
|
return true
|
|
}
|
|
if state[name] == 2 {
|
|
return g.recursive[name]
|
|
}
|
|
state[name] = 1
|
|
for _, next := range dependencies[name] {
|
|
if cyclic(next) {
|
|
g.recursive[name] = true
|
|
}
|
|
}
|
|
state[name] = 2
|
|
return g.recursive[name]
|
|
}
|
|
for _, typ := range spec.Types {
|
|
cyclic(typ.Name)
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
func (g *responseGraph) expression(e ast.Expr, depth int, named func(string, int) (reflect.Type, error)) (reflect.Type, error) {
|
|
if depth > 32 {
|
|
return nil, fmt.Errorf("type nesting exceeds 32")
|
|
}
|
|
switch e := e.(type) {
|
|
case *ast.Ident:
|
|
if t := g.scalars[e.Name]; t != nil {
|
|
return t, nil
|
|
}
|
|
if t := primitiveTypes[e.Name]; t != nil {
|
|
return t, nil
|
|
}
|
|
if g.types[e.Name].Name == "" {
|
|
return nil, fmt.Errorf("unknown type %s", e.Name)
|
|
}
|
|
return named(e.Name, depth+1)
|
|
case *ast.StarExpr:
|
|
if _, double := e.X.(*ast.StarExpr); double {
|
|
return nil, fmt.Errorf("multiple pointer levels unsupported")
|
|
}
|
|
t, err := g.expression(e.X, depth+1, named)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return reflect.PointerTo(t), nil
|
|
case *ast.ArrayType:
|
|
if e.Len != nil {
|
|
return nil, fmt.Errorf("use slices, not fixed arrays")
|
|
}
|
|
t, err := g.expression(e.Elt, depth+1, named)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return reflect.SliceOf(t), nil
|
|
case *ast.MapType:
|
|
key, ok := e.Key.(*ast.Ident)
|
|
if !ok || key.Name != "string" {
|
|
return nil, fmt.Errorf("map keys must be string")
|
|
}
|
|
t, err := g.expression(e.Value, depth+1, named)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return reflect.MapOf(reflect.TypeOf(""), t), nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported type expression")
|
|
}
|
|
}
|
|
|
|
func (g *responseGraph) component(name string) (*openapi.Schema, error) {
|
|
key := "DTO_" + name
|
|
ref := &openapi.Schema{Ref: "#/components/schemas/" + key}
|
|
if _, ok := g.components[key]; ok {
|
|
return ref, nil
|
|
}
|
|
// Register before following edges so both self and mutual references terminate.
|
|
g.components[key] = nil
|
|
typ := g.types[name]
|
|
var fields []reflect.StructField
|
|
for i, f := range typ.Fields {
|
|
t, err := g.expression(g.exprs[name][i], 0, func(string, int) (reflect.Type, error) { return reflect.TypeOf(struct{}{}), nil })
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tag, err := withoutExample(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fields = append(fields, reflect.StructField{Name: f.Name, Type: t, Tag: tag})
|
|
}
|
|
b, err := contract.Describe(reflect.StructOf(fields), g.scalarDefs...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", typ.Position, err)
|
|
}
|
|
s, err := b.ResponseSchema()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", typ.Position, err)
|
|
}
|
|
for i, f := range typ.Fields {
|
|
wire := strings.Split(reflect.StructTag(fieldTag(f)).Get("json"), ",")[0]
|
|
property := s.Properties[wire]
|
|
// Examples are checked against a finite projection of their actual values,
|
|
// with the same contract rules and without running application decoders.
|
|
if _, ok := reflect.StructTag(fieldTag(f)).Lookup("example"); ok {
|
|
example, err := g.example(f, g.exprs[name][i])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s.%s: %w", name, f.Name, err)
|
|
}
|
|
property.Example = example
|
|
}
|
|
property, err = g.link(g.exprs[name][i], property)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.Properties[wire] = property
|
|
}
|
|
g.components[key] = s
|
|
return ref, nil
|
|
}
|
|
|
|
func (g *responseGraph) link(e ast.Expr, s *openapi.Schema) (*openapi.Schema, error) {
|
|
switch e := e.(type) {
|
|
case *ast.Ident:
|
|
if g.types[e.Name].Name == "" {
|
|
return s, nil
|
|
}
|
|
ref, err := g.component(e.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s.Nullable {
|
|
ref = nullableReference(ref)
|
|
}
|
|
if s.Description != "" || s.Example != nil {
|
|
// OpenAPI 3.0 ignores siblings on Reference Objects.
|
|
ref = &openapi.Schema{AllOf: []*openapi.Schema{ref}, Description: s.Description, Example: s.Example}
|
|
}
|
|
return ref, nil
|
|
case *ast.StarExpr:
|
|
return g.link(e.X, s)
|
|
case *ast.ArrayType:
|
|
item, err := g.link(e.Elt, s.Items)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.Items = item
|
|
case *ast.MapType:
|
|
item, err := g.link(e.Value, s.AdditionalProperties)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.AdditionalProperties = item
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func nullableReference(s *openapi.Schema) *openapi.Schema {
|
|
return &openapi.Schema{AnyOf: []*openapi.Schema{s, {Type: "object", Nullable: true, Enum: []any{nil}}}}
|
|
}
|
|
|
|
func withoutExample(f Field) (reflect.StructTag, error) {
|
|
tags, err := contract.ParseTags(fieldTag(f))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
delete(tags, "example")
|
|
keys := make([]string, 0, len(tags))
|
|
for key := range tags {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
var parts []string
|
|
for _, key := range keys {
|
|
parts = append(parts, key+":"+strconv.Quote(tags[key]))
|
|
}
|
|
return reflect.StructTag(strings.Join(parts, " ")), nil
|
|
}
|
|
|
|
func (g *responseGraph) example(f Field, e ast.Expr) (any, error) {
|
|
raw := reflect.StructTag(fieldTag(f)).Get("example")
|
|
var value any
|
|
// Plain string examples intentionally are not JSON literals.
|
|
if f.Type == "string" || f.Type == "*string" {
|
|
value = raw
|
|
} else {
|
|
decoder := json.NewDecoder(strings.NewReader(raw))
|
|
decoder.UseNumber()
|
|
if err := decoder.Decode(&value); err != nil {
|
|
return nil, fmt.Errorf("invalid example JSON: %w", err)
|
|
}
|
|
}
|
|
budget := 10000
|
|
t, err := g.projectExample(e, []any{value}, 0, &budget)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b, err := contract.Describe(reflect.StructOf([]reflect.StructField{{Name: f.Name, Type: t, Tag: reflect.StructTag(fieldTag(f))}}), g.scalarDefs...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s, err := b.ResponseSchema()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wire := strings.Split(reflect.StructTag(fieldTag(f)).Get("json"), ",")[0]
|
|
return s.Properties[wire].Example, nil
|
|
}
|
|
|
|
// Only materialize named objects where the finite example has a non-null value.
|
|
// Empty slices/maps and null pointers stop expansion. Unioning collection values
|
|
// checks every item using one reflection type, including missing/unknown fields.
|
|
func (g *responseGraph) projectExample(e ast.Expr, values []any, depth int, budget *int) (reflect.Type, error) {
|
|
*budget--
|
|
if depth > 32 || *budget < 0 {
|
|
return nil, fmt.Errorf("example type expansion exceeds 32 levels or 10000 nodes")
|
|
}
|
|
var present []any
|
|
for _, v := range values {
|
|
if v != nil {
|
|
present = append(present, v)
|
|
}
|
|
}
|
|
switch e := e.(type) {
|
|
case *ast.Ident:
|
|
return g.expression(e, 0, func(name string, _ int) (reflect.Type, error) {
|
|
if len(present) == 0 {
|
|
return reflect.TypeOf(struct{}{}), nil
|
|
}
|
|
var fields []reflect.StructField
|
|
for i, f := range g.types[name].Fields {
|
|
wire := strings.Split(reflect.StructTag(fieldTag(f)).Get("json"), ",")[0]
|
|
var nested []any
|
|
for _, v := range present {
|
|
if obj, ok := v.(map[string]any); ok {
|
|
if child, found := obj[wire]; found {
|
|
nested = append(nested, child)
|
|
}
|
|
}
|
|
}
|
|
t, err := g.projectExample(g.exprs[name][i], nested, depth+1, budget)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tag, err := withoutExample(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fields = append(fields, reflect.StructField{Name: f.Name, Type: t, Tag: tag})
|
|
}
|
|
return reflect.StructOf(fields), nil
|
|
})
|
|
case *ast.StarExpr:
|
|
t, err := g.projectExample(e.X, present, depth+1, budget)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return reflect.PointerTo(t), nil
|
|
case *ast.ArrayType:
|
|
var nested []any
|
|
for _, v := range present {
|
|
if list, ok := v.([]any); ok {
|
|
nested = append(nested, list...)
|
|
}
|
|
}
|
|
t, err := g.projectExample(e.Elt, nested, depth+1, budget)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return reflect.SliceOf(t), nil
|
|
case *ast.MapType:
|
|
var nested []any
|
|
for _, v := range present {
|
|
if obj, ok := v.(map[string]any); ok {
|
|
for _, v := range obj {
|
|
nested = append(nested, v)
|
|
}
|
|
}
|
|
}
|
|
t, err := g.projectExample(e.Value, nested, depth+1, budget)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return reflect.MapOf(reflect.TypeOf(""), t), nil
|
|
}
|
|
return nil, fmt.Errorf("unsupported example type")
|
|
}
|