359 lines
9.4 KiB
Go
359 lines
9.4 KiB
Go
package schema
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"unsafe"
|
|
|
|
"aego/core/errs"
|
|
"aego/core/log"
|
|
"aego/core/mathx"
|
|
)
|
|
|
|
type vec2 = mathx.Vec2
|
|
|
|
type color = mathx.Color
|
|
|
|
type rect = mathx.Rect
|
|
|
|
var (
|
|
vec2Type = reflect.TypeFor[mathx.Vec2]()
|
|
colorType = reflect.TypeFor[mathx.Color]()
|
|
rectType = reflect.TypeFor[mathx.Rect]()
|
|
assetRefType = reflect.TypeFor[AssetRef]()
|
|
nodeRefType = reflect.TypeFor[NodeRef]()
|
|
)
|
|
|
|
type enumInfo struct {
|
|
name string
|
|
values []EnumValue
|
|
}
|
|
|
|
type Registry struct {
|
|
frozen bool
|
|
byID map[TypeID]*TypeDescriptor
|
|
byName map[string]*TypeDescriptor
|
|
byType map[reflect.Type]*TypeDescriptor
|
|
enums map[reflect.Type]*enumInfo
|
|
order []*TypeDescriptor
|
|
}
|
|
|
|
func NewRegistry() *Registry {
|
|
return &Registry{
|
|
byID: map[TypeID]*TypeDescriptor{},
|
|
byName: map[string]*TypeDescriptor{},
|
|
byType: map[reflect.Type]*TypeDescriptor{},
|
|
enums: map[reflect.Type]*enumInfo{},
|
|
}
|
|
}
|
|
|
|
func (r *Registry) Frozen() bool {
|
|
return r.frozen
|
|
}
|
|
|
|
func (r *Registry) ByID(id TypeID) (*TypeDescriptor, bool) {
|
|
t, ok := r.byID[id]
|
|
return t, ok
|
|
}
|
|
|
|
func (r *Registry) ByName(name string) (*TypeDescriptor, bool) {
|
|
t, ok := r.byName[name]
|
|
return t, ok
|
|
}
|
|
|
|
func (r *Registry) ByGoType(t reflect.Type) (*TypeDescriptor, bool) {
|
|
d, ok := r.byType[t]
|
|
return d, ok
|
|
}
|
|
|
|
func (r *Registry) Field(t TypeID, name string) (*FieldDescriptor, bool) {
|
|
td, ok := r.byID[t]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
return td.Field(name)
|
|
}
|
|
|
|
func (r *Registry) Each(fn func(*TypeDescriptor)) {
|
|
for _, t := range r.order {
|
|
fn(t)
|
|
}
|
|
}
|
|
|
|
func (r *Registry) Components() []*TypeDescriptor {
|
|
out := make([]*TypeDescriptor, 0, len(r.order))
|
|
for _, t := range r.order {
|
|
if t.Kind == TypeComponent {
|
|
out = append(out, t)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *Registry) Len() int {
|
|
return len(r.order)
|
|
}
|
|
|
|
func RegisterEnum[T ~int8 | ~int16 | ~int32 | ~int64 | ~int | ~uint8 | ~uint16 | ~uint32](r *Registry, name string, values ...EnumValue) error {
|
|
if r.frozen {
|
|
return errs.New(errs.SchemaFrozen, "registry is frozen", log.F("enum", name))
|
|
}
|
|
rt := reflect.TypeFor[T]()
|
|
if _, dup := r.enums[rt]; dup {
|
|
return errs.New(errs.SchemaDuplicate, "enum type is already registered", log.F("enum", name))
|
|
}
|
|
if len(values) == 0 {
|
|
return errs.New(errs.SchemaBadValue, "enum needs at least one value", log.F("enum", name))
|
|
}
|
|
r.enums[rt] = &enumInfo{name: name, values: values}
|
|
return nil
|
|
}
|
|
|
|
func Register[T any](r *Registry, opts ...Option) (*TypeDescriptor, error) {
|
|
return r.register(reflect.TypeFor[T](), TypeComponent, opts)
|
|
}
|
|
|
|
func RegisterStruct[T any](r *Registry, opts ...Option) (*TypeDescriptor, error) {
|
|
return r.register(reflect.TypeFor[T](), TypeStruct, opts)
|
|
}
|
|
|
|
func (r *Registry) register(rt reflect.Type, kind TypeKind, opts []Option) (*TypeDescriptor, error) {
|
|
if r.frozen {
|
|
return nil, errs.New(errs.SchemaFrozen, "registry is frozen", log.F("type", rt.String()))
|
|
}
|
|
if rt.Kind() != reflect.Struct {
|
|
return nil, errs.New(errs.SchemaNotStruct, "only structs can be registered",
|
|
log.F("type", rt.String()))
|
|
}
|
|
var o regOptions
|
|
for _, fn := range opts {
|
|
fn(&o)
|
|
}
|
|
full := o.name
|
|
if full == "" {
|
|
full = defaultName(rt)
|
|
}
|
|
if existing, dup := r.byName[full]; dup {
|
|
return nil, errs.New(errs.SchemaDuplicate, "type name is already registered",
|
|
log.F("name", full), log.F("existing", existing.FullName))
|
|
}
|
|
if _, dup := r.byType[rt]; dup {
|
|
return nil, errs.New(errs.SchemaDuplicate, "go type is already registered",
|
|
log.F("type", rt.String()))
|
|
}
|
|
id := HashName(full)
|
|
if existing, clash := r.byID[id]; clash {
|
|
return nil, errs.New(errs.SchemaCollision, "type name hash collides",
|
|
log.F("name", full), log.F("existing", existing.FullName))
|
|
}
|
|
|
|
td := &TypeDescriptor{
|
|
ID: id,
|
|
FullName: full,
|
|
Former: o.former,
|
|
Version: o.version,
|
|
Size: rt.Size(),
|
|
Kind: kind,
|
|
Doc: o.doc,
|
|
index: map[string]int{},
|
|
byID: map[FieldID]int{},
|
|
New: func() unsafe.Pointer {
|
|
return reflect.New(rt).UnsafePointer()
|
|
},
|
|
Copy: func(dst, src unsafe.Pointer) {
|
|
reflect.NewAt(rt, dst).Elem().Set(reflect.NewAt(rt, src).Elem())
|
|
},
|
|
}
|
|
if td.Version == 0 {
|
|
td.Version = 1
|
|
}
|
|
|
|
for i := 0; i < rt.NumField(); i++ {
|
|
sf := rt.Field(i)
|
|
if !sf.IsExported() {
|
|
continue
|
|
}
|
|
info, err := parseTag(full, sf.Name, sf.Tag.Get("aego"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if info.skip {
|
|
continue
|
|
}
|
|
fd, err := r.buildField(full, sf, info)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if former, ok := o.formerFields[fd.Name]; ok {
|
|
fd.Former = append(fd.Former, former...)
|
|
}
|
|
fd.ID = HashField(id, fd.Name)
|
|
if prev, dup := td.index[fd.Name]; dup {
|
|
return nil, errs.New(errs.SchemaDuplicate, "duplicate field name",
|
|
log.F("type", full), log.F("field", fd.Name), log.F("previous", td.Fields[prev].Name))
|
|
}
|
|
if prev, clash := td.byID[fd.ID]; clash {
|
|
return nil, errs.New(errs.SchemaCollision, "field name hash collides",
|
|
log.F("type", full), log.F("field", fd.Name), log.F("existing", td.Fields[prev].Name))
|
|
}
|
|
td.index[fd.Name] = len(td.Fields)
|
|
td.byID[fd.ID] = len(td.Fields)
|
|
td.Fields = append(td.Fields, fd)
|
|
}
|
|
|
|
r.byID[id] = td
|
|
r.byName[full] = td
|
|
r.byType[rt] = td
|
|
r.order = append(r.order, td)
|
|
return td, nil
|
|
}
|
|
|
|
func (r *Registry) buildField(typeName string, sf reflect.StructField, info tagInfo) (FieldDescriptor, error) {
|
|
fd := FieldDescriptor{
|
|
Name: info.name,
|
|
Former: info.former,
|
|
Flags: info.flags,
|
|
AssetKind: info.assetKind,
|
|
Min: info.min,
|
|
Max: info.max,
|
|
HasMin: info.hasMin,
|
|
HasMax: info.hasMax,
|
|
Hints: info.hints,
|
|
Doc: info.doc,
|
|
Offset: sf.Offset,
|
|
Size: sf.Type.Size(),
|
|
}
|
|
ft := sf.Type
|
|
kind, elem, structID, err := r.kindOf(typeName, info.name, ft)
|
|
if err != nil {
|
|
return fd, err
|
|
}
|
|
fd.Kind = kind
|
|
fd.Elem = elem
|
|
fd.Struct = structID
|
|
if kind == KindEnum {
|
|
if ei, ok := r.enums[ft]; ok {
|
|
fd.Enum = ei.values
|
|
fd.EnumName = ei.name
|
|
}
|
|
}
|
|
if kind == KindAssetRef && fd.AssetKind == "" {
|
|
zero := reflect.New(ft).Interface().(AssetRef)
|
|
fd.AssetKind = zero.AssetKind()
|
|
}
|
|
fd.Fast = kind.IsPOD()
|
|
if err := r.makeAccessors(&fd, ft, sf.Offset); err != nil {
|
|
return fd, err
|
|
}
|
|
return fd, nil
|
|
}
|
|
|
|
func (r *Registry) kindOf(typeName, field string, ft reflect.Type) (Kind, Kind, TypeID, error) {
|
|
if ei, ok := r.enums[ft]; ok {
|
|
_ = ei
|
|
return KindEnum, KindInvalid, 0, nil
|
|
}
|
|
if reflect.PointerTo(ft).Implements(assetRefType) {
|
|
return KindAssetRef, KindInvalid, 0, nil
|
|
}
|
|
if reflect.PointerTo(ft).Implements(nodeRefType) {
|
|
return KindNodeRef, KindInvalid, 0, nil
|
|
}
|
|
switch ft {
|
|
case vec2Type:
|
|
return KindVec2, KindInvalid, 0, nil
|
|
case colorType:
|
|
return KindColor, KindInvalid, 0, nil
|
|
case rectType:
|
|
return KindRect, KindInvalid, 0, nil
|
|
}
|
|
switch ft.Kind() {
|
|
case reflect.Bool:
|
|
return KindBool, KindInvalid, 0, nil
|
|
case reflect.Int32:
|
|
return KindInt32, KindInvalid, 0, nil
|
|
case reflect.Int, reflect.Int64:
|
|
return KindInt64, KindInvalid, 0, nil
|
|
case reflect.Float32:
|
|
return KindFloat32, KindInvalid, 0, nil
|
|
case reflect.Float64:
|
|
return KindFloat64, KindInvalid, 0, nil
|
|
case reflect.String:
|
|
return KindString, KindInvalid, 0, nil
|
|
case reflect.Slice:
|
|
elemKind, _, structID, err := r.kindOf(typeName, field, ft.Elem())
|
|
if err != nil {
|
|
return KindInvalid, KindInvalid, 0, err
|
|
}
|
|
if elemKind == KindList {
|
|
return KindInvalid, KindInvalid, 0, tagError(typeName, field, "nested slices are not supported")
|
|
}
|
|
return KindList, elemKind, structID, nil
|
|
case reflect.Struct:
|
|
td, ok := r.byType[ft]
|
|
if !ok {
|
|
return KindInvalid, KindInvalid, 0, errs.New(errs.SchemaUnknownType,
|
|
"struct field type must be registered with RegisterStruct first",
|
|
log.F("type", typeName), log.F("field", field), log.F("fieldType", ft.String()))
|
|
}
|
|
return KindStruct, KindInvalid, td.ID, nil
|
|
}
|
|
return KindInvalid, KindInvalid, 0, tagError(typeName, field,
|
|
"unsupported field type "+ft.String()+"; tag it with aego:\"-\" to skip")
|
|
}
|
|
|
|
func (r *Registry) Freeze() error {
|
|
if r.frozen {
|
|
return nil
|
|
}
|
|
seen := map[string]*TypeDescriptor{}
|
|
for _, td := range r.order {
|
|
seen[td.FullName] = td
|
|
}
|
|
for _, td := range r.order {
|
|
for _, alias := range td.Former {
|
|
if other, dup := seen[alias]; dup {
|
|
return errs.New(errs.SchemaDuplicate, "former name collides with an existing type",
|
|
log.F("type", td.FullName), log.F("former", alias), log.F("other", other.FullName))
|
|
}
|
|
id := HashName(alias)
|
|
if other, clash := r.byID[id]; clash && other != td {
|
|
return errs.New(errs.SchemaCollision, "former name hash collides",
|
|
log.F("type", td.FullName), log.F("former", alias), log.F("other", other.FullName))
|
|
}
|
|
seen[alias] = td
|
|
r.byName[alias] = td
|
|
r.byID[id] = td
|
|
}
|
|
for i := range td.Fields {
|
|
fd := &td.Fields[i]
|
|
if fd.Kind == KindStruct || fd.Elem == KindStruct {
|
|
if _, ok := r.byID[fd.Struct]; !ok {
|
|
return errs.New(errs.SchemaUnknownType, "field references an unregistered struct",
|
|
log.F("type", td.FullName), log.F("field", fd.Name))
|
|
}
|
|
}
|
|
for _, alias := range fd.Former {
|
|
if _, dup := td.index[alias]; dup {
|
|
return errs.New(errs.SchemaDuplicate, "former field name collides with an existing field",
|
|
log.F("type", td.FullName), log.F("field", fd.Name), log.F("former", alias))
|
|
}
|
|
td.index[alias] = td.index[fd.Name]
|
|
}
|
|
}
|
|
}
|
|
r.frozen = true
|
|
return nil
|
|
}
|
|
|
|
func defaultName(rt reflect.Type) string {
|
|
pkg := rt.PkgPath()
|
|
if i := strings.LastIndexByte(pkg, '/'); i >= 0 {
|
|
pkg = pkg[i+1:]
|
|
}
|
|
if pkg == "" {
|
|
return rt.Name()
|
|
}
|
|
return pkg + "." + rt.Name()
|
|
}
|