sing-box/schema/generator.go
2026-08-04 21:51:25 +08:00

421 lines
12 KiB
Go

package schema
import (
"context"
"encoding"
stdjson "encoding/json"
"reflect"
"slices"
"strconv"
"strings"
"github.com/sagernet/sing/common/byteformats"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
)
var (
jsonUnmarshalerType = reflect.TypeFor[stdjson.Unmarshaler]()
contextUnmarshalerType = reflect.TypeFor[json.ContextUnmarshaler]()
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
durationType = reflect.TypeFor[badoption.Duration]()
addrType = reflect.TypeFor[badoption.Addr]()
prefixType = reflect.TypeFor[badoption.Prefix]()
prefixableType = reflect.TypeFor[badoption.Prefixable]()
httpHeaderType = reflect.TypeFor[badoption.HTTPHeader]()
memoryBytesType = reflect.TypeFor[byteformats.MemoryBytes]()
networkBytesCompatType = reflect.TypeFor[byteformats.NetworkBytesCompat]()
)
type generator struct {
ctx context.Context
defs map[string]*Node
defTypes map[reflect.Type]string
path []string
}
func (g *generator) Context() context.Context {
return g.ctx
}
func (g *generator) Define(name string, build func() (*Node, error)) (*Node, error) {
_, exists := g.defs[name]
if exists {
return RefNode(name), nil
}
g.defs[name] = nil
node, err := build()
if err != nil {
return nil, err
}
g.defs[name] = node
return RefNode(name), nil
}
func implementationOf[T any](valueType reflect.Type) (T, bool) {
interfaceType := reflect.TypeFor[T]()
if !valueType.Implements(interfaceType) && !reflect.PointerTo(valueType).Implements(interfaceType) {
var zeroValue T
return zeroValue, false
}
return reflect.New(valueType).Interface().(T), true
}
func (g *generator) Describe(valueType reflect.Type) (*Node, error) {
for valueType.Kind() == reflect.Pointer {
valueType = valueType.Elem()
}
describer, described := implementationOf[Describer](valueType)
if described {
return describer.DescribeSchema(g)
}
switch valueType {
case durationType:
return g.Define("Duration", func() (*Node, error) {
return DurationNode(), nil
})
case addrType, prefixType, prefixableType:
return StringNode(), nil
case httpHeaderType:
return g.Define("HTTPHeader", func() (*Node, error) {
return &Node{Type: "object", AdditionalProperties: ListableOf(StringNode())}, nil
})
case memoryBytesType, networkBytesCompatType:
return AnyOf(UnsignedNode(64), StringNode()), nil
}
if isListable(valueType) {
elementNode, err := g.Describe(valueType.Elem())
if err != nil {
return nil, err
}
return ListableOf(elementNode), nil
}
if isTypedMap(valueType) {
return g.typedMapNode(valueType)
}
pointerType := reflect.PointerTo(valueType)
if pointerType.Implements(jsonUnmarshalerType) || pointerType.Implements(contextUnmarshalerType) {
return nil, E.New("unmapped custom JSON type ", valueType.String(), " at ", strings.Join(g.path, "."))
}
if pointerType.Implements(textUnmarshalerType) {
return StringNode(), nil
}
switch valueType.Kind() {
case reflect.Struct:
if valueType.Name() == "" {
node := StrictObject()
err := g.FlattenStruct(node, valueType)
if err != nil {
return nil, err
}
return node, nil
}
return g.Define(g.defNameFor(valueType), func() (*Node, error) {
node := StrictObject()
err := g.FlattenStruct(node, valueType)
if err != nil {
return nil, err
}
return node, nil
})
case reflect.Slice, reflect.Array:
if valueType.Kind() == reflect.Slice && valueType.Elem().Kind() == reflect.Uint8 {
// encoding/json accepts both base64 strings and number arrays.
return AnyOf(StringNode(), &Node{Type: "array", Items: UnsignedNode(8)}), nil
}
elementNode, err := g.Describe(valueType.Elem())
if err != nil {
return nil, err
}
return &Node{Type: "array", Items: elementNode}, nil
case reflect.Map:
if valueType.Key().Kind() != reflect.String {
return nil, E.New("unsupported map key type ", valueType.String(), " at ", strings.Join(g.path, "."))
}
valueNode, err := g.Describe(valueType.Elem())
if err != nil {
return nil, err
}
return &Node{Type: "object", AdditionalProperties: valueNode}, nil
case reflect.Bool:
return BooleanNode(), nil
case reflect.String:
return StringNode(), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return IntegerNode(), nil
case reflect.Uint, reflect.Uint64:
return UnsignedNode(64), nil
case reflect.Uint8:
return UnsignedNode(8), nil
case reflect.Uint16:
return UnsignedNode(16), nil
case reflect.Uint32:
return UnsignedNode(32), nil
case reflect.Float32, reflect.Float64:
return &Node{Type: "number"}, nil
default:
return nil, E.New("unsupported kind ", valueType.Kind().String(), " for ", valueType.String(), " at ", strings.Join(g.path, "."))
}
}
func (g *generator) defNameFor(fieldType reflect.Type) string {
existingName, loaded := g.defTypes[fieldType]
if loaded {
return existingName
}
name := fieldType.Name()
for otherType, otherName := range g.defTypes {
if otherName == name && otherType != fieldType {
name = pathBase(fieldType.PkgPath()) + "." + name
break
}
}
g.defTypes[fieldType] = name
return name
}
func pathBase(packagePath string) string {
index := strings.LastIndexByte(packagePath, '/')
if index < 0 {
return packagePath
}
return packagePath[index+1:]
}
// FlattenStruct merges the JSON fields of structType into node, following the
// same flattening semantics as badjson.MarshallObjects / anonymous embedding.
func (g *generator) FlattenStruct(node *Node, structType reflect.Type) error {
for structType.Kind() == reflect.Pointer {
structType = structType.Elem()
}
if structType.Kind() != reflect.Struct {
return E.New("cannot flatten non-struct type ", structType.String(), " at ", strings.Join(g.path, "."))
}
for i := range structType.NumField() {
field := structType.Field(i)
if !field.IsExported() && !field.Anonymous {
continue
}
tagValue := field.Tag.Get("json")
tagName, _, _ := strings.Cut(tagValue, ",")
if tagName == "-" {
continue
}
fieldType := field.Type
for fieldType.Kind() == reflect.Pointer {
fieldType = fieldType.Elem()
}
if field.Tag.Get("schema") == "omit" {
continue
}
if field.Anonymous && tagName == "" {
err := g.FlattenStruct(node, fieldType)
if err != nil {
return err
}
continue
}
if tagName == "" {
tagName = field.Name
}
enumTag := field.Tag.Get("enum")
examplesTag := field.Tag.Get("examples")
referenceTag := field.Tag.Get("reference")
g.path = append(g.path, structType.Name()+"."+tagName)
var fieldNode *Node
var err error
if enumTag != "" || examplesTag != "" || referenceTag != "" {
fieldNode, err = taggedFieldNode(fieldType, enumTag, examplesTag, referenceTag)
} else {
fieldNode, err = g.Describe(fieldType)
}
g.path = g.path[:len(g.path)-1]
if err != nil {
return err
}
node.Properties.Put(tagName, fieldNode)
}
return nil
}
func taggedFieldNode(fieldType reflect.Type, enumTag string, examplesTag string, referenceTag string) (*Node, error) {
elementType := fieldType
for elementType.Kind() == reflect.Pointer {
elementType = elementType.Elem()
}
listable := isListable(fieldType)
plainSlice := !listable && elementType.Kind() == reflect.Slice && elementType.Elem().Kind() == reflect.String
if listable {
elementType = fieldType.Elem()
} else if plainSlice {
elementType = elementType.Elem()
}
var element *Node
var err error
if enumTag != "" {
element, err = taggedValueNode(elementType, strings.Split(enumTag, ","))
if err != nil {
return nil, err
}
} else {
element, err = taggedValueNode(elementType, nil)
if err != nil {
return nil, err
}
}
if examplesTag != "" {
examples, parseErr := taggedValues(elementType, strings.Split(examplesTag, ","))
if parseErr != nil {
return nil, parseErr
}
element.Examples = examples
}
if referenceTag != "" {
if elementType.Kind() != reflect.String {
return nil, E.New("reference tags require a string field, got ", fieldType.String())
}
element.TagReference = referenceTag
}
if listable {
return ListableOf(element), nil
}
if plainSlice {
return &Node{Type: "array", Items: element}, nil
}
return element, nil
}
func taggedValueNode(fieldType reflect.Type, values []string) (*Node, error) {
switch fieldType.Kind() {
case reflect.String:
node := StringNode()
for _, value := range values {
node.Enum = append(node.Enum, value)
}
return node, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
node := IntegerNode()
enumValues, err := taggedValues(fieldType, values)
if err != nil {
return nil, err
}
node.Enum = enumValues
return node, nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
node := UnsignedNode(fieldType.Bits())
enumValues, err := taggedValues(fieldType, values)
if err != nil {
return nil, err
}
node.Enum = enumValues
return node, nil
default:
node := StringNode()
for _, value := range values {
err := unmarshalTaggedValue(fieldType, value)
if err != nil {
return nil, err
}
node.Enum = append(node.Enum, value)
}
return node, nil
}
}
func unmarshalTaggedValue(fieldType reflect.Type, value string) error {
err := json.Unmarshal([]byte(strconv.Quote(value)), reflect.New(fieldType).Interface())
if err != nil {
return E.Cause(err, "unmarshal tagged value ", value, " as ", fieldType.String())
}
return nil
}
func taggedValues(fieldType reflect.Type, values []string) ([]any, error) {
result := make([]any, 0, len(values))
for _, value := range values {
switch fieldType.Kind() {
case reflect.String:
result = append(result, value)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
integerValue, err := strconv.ParseInt(value, 10, fieldType.Bits())
if err != nil {
return nil, E.Cause(err, "parse enum value ", value, " for ", fieldType.String())
}
result = append(result, integerValue)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
unsignedValue, err := strconv.ParseUint(value, 10, fieldType.Bits())
if err != nil {
return nil, E.Cause(err, "parse enum value ", value, " for ", fieldType.String())
}
result = append(result, unsignedValue)
default:
err := unmarshalTaggedValue(fieldType, value)
if err != nil {
return nil, err
}
result = append(result, value)
}
}
return result, nil
}
func isListable(fieldType reflect.Type) bool {
return fieldType.Kind() == reflect.Slice &&
fieldType.PkgPath() == "github.com/sagernet/sing/common/json/badoption" &&
strings.HasPrefix(fieldType.Name(), "Listable[")
}
func isTypedMap(fieldType reflect.Type) bool {
return fieldType.Kind() == reflect.Struct &&
fieldType.PkgPath() == "github.com/sagernet/sing/common/json/badjson" &&
strings.HasPrefix(fieldType.Name(), "TypedMap[")
}
func (g *generator) typedMapNode(fieldType reflect.Type) (*Node, error) {
mapField, found := fieldType.FieldByName("Map")
if !found {
return nil, E.New("unexpected TypedMap layout: missing Map in ", fieldType.String())
}
rawMapField, found := mapField.Type.FieldByName("rawMap")
if !found {
return nil, E.New("unexpected TypedMap layout: missing rawMap in ", fieldType.String())
}
keyType := rawMapField.Type.Key()
elementValueField, found := rawMapField.Type.Elem().Elem().FieldByName("Value")
if !found {
return nil, E.New("unexpected TypedMap layout: missing element value in ", fieldType.String())
}
entryValueField, found := elementValueField.Type.FieldByName("Value")
if !found {
return nil, E.New("unexpected TypedMap layout: missing entry value in ", fieldType.String())
}
valueNode, err := g.Describe(entryValueField.Type)
if err != nil {
return nil, err
}
node := &Node{Type: "object", AdditionalProperties: valueNode}
if keyType.Kind() != reflect.String || keyType.PkgPath() != "" {
keyNode, keyErr := g.Describe(keyType)
if keyErr != nil {
return nil, keyErr
}
node.PropertyNames = keyNode
}
return node, nil
}
func (g *generator) sortedDefs() *badjson.TypedMap[string, *Node] {
names := make([]string, 0, len(g.defs))
for name := range g.defs {
names = append(names, name)
}
slices.Sort(names)
result := new(badjson.TypedMap[string, *Node])
for _, name := range names {
result.Put(name, g.defs[name])
}
return result
}