mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-08-04 14:36:07 +00:00
421 lines
12 KiB
Go
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
|
|
}
|