initial
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/importer"
|
||||
goparser "go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseFile парсит Go-файл и возвращает список сообщений.
|
||||
func ParseFile(path string) ([]Message, error) {
|
||||
schema, err := ParseSchemaFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return schema.Messages, nil
|
||||
}
|
||||
|
||||
// ParseSource парсит исходный код из строки (удобно для тестов).
|
||||
func ParseSource(src string) ([]Message, error) {
|
||||
schema, err := ParseSchemaSource(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return schema.Messages, nil
|
||||
}
|
||||
|
||||
// ParseSchemaFile парсит файл и возвращает полную схему: сообщения и enum-ы.
|
||||
func ParseSchemaFile(path string) (Schema, error) {
|
||||
fset := token.NewFileSet()
|
||||
|
||||
f, err := goparser.ParseFile(fset, path, nil, 0)
|
||||
if err != nil {
|
||||
return Schema{}, fmt.Errorf("parse %s: %w", path, err)
|
||||
}
|
||||
|
||||
return parseASTFile(fset, f)
|
||||
}
|
||||
|
||||
// ParseSchemaSource парсит исходник и возвращает полную схему.
|
||||
func ParseSchemaSource(src string) (Schema, error) {
|
||||
fset := token.NewFileSet()
|
||||
|
||||
f, err := goparser.ParseFile(fset, "source.go", src, 0)
|
||||
if err != nil {
|
||||
return Schema{}, fmt.Errorf("parse source: %w", err)
|
||||
}
|
||||
|
||||
return parseASTFile(fset, f)
|
||||
}
|
||||
|
||||
func parseASTFile(fset *token.FileSet, f *ast.File) (Schema, error) {
|
||||
pkgName := f.Name.Name
|
||||
|
||||
knownStructs := map[string]bool{}
|
||||
namedPrimitives := map[string]PrimitiveKind{}
|
||||
var enumOrder []string
|
||||
|
||||
for _, decl := range f.Decls {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok || genDecl.Tok != token.TYPE {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, spec := range genDecl.Specs {
|
||||
typeSpec, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch t := typeSpec.Type.(type) {
|
||||
case *ast.StructType:
|
||||
knownStructs[typeSpec.Name.Name] = true
|
||||
case *ast.Ident:
|
||||
primKind, isPrimitive := goPrimitiveKind(t.Name)
|
||||
if !isPrimitive {
|
||||
continue
|
||||
}
|
||||
namedPrimitives[typeSpec.Name.Name] = primKind
|
||||
if IsIntegralPrimitive(primKind) {
|
||||
enumOrder = append(enumOrder, typeSpec.Name.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info, err := typeCheckFile(fset, f)
|
||||
if err != nil {
|
||||
return Schema{}, err
|
||||
}
|
||||
|
||||
schema := Schema{PackageName: pkgName}
|
||||
enumIndex := make(map[string]int, len(enumOrder))
|
||||
for _, name := range enumOrder {
|
||||
enumIndex[name] = len(schema.Enums)
|
||||
schema.Enums = append(schema.Enums, Enum{
|
||||
Name: name,
|
||||
Primitive: namedPrimitives[name],
|
||||
})
|
||||
}
|
||||
|
||||
for _, decl := range f.Decls {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch genDecl.Tok {
|
||||
case token.TYPE:
|
||||
for _, spec := range genDecl.Specs {
|
||||
typeSpec, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
structType, ok := typeSpec.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
msg, err := parseStruct(pkgName, typeSpec.Name.Name, structType, knownStructs, namedPrimitives)
|
||||
if err != nil {
|
||||
return Schema{}, fmt.Errorf("struct %s: %w", typeSpec.Name.Name, err)
|
||||
}
|
||||
|
||||
schema.Messages = append(schema.Messages, msg)
|
||||
}
|
||||
case token.CONST:
|
||||
if err := parseConstDecls(genDecl, info, enumIndex, &schema); err != nil {
|
||||
return Schema{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
func typeCheckFile(fset *token.FileSet, f *ast.File) (*types.Info, error) {
|
||||
info := &types.Info{
|
||||
Defs: make(map[*ast.Ident]types.Object),
|
||||
}
|
||||
|
||||
cfg := &types.Config{
|
||||
Importer: importer.Default(),
|
||||
}
|
||||
|
||||
if _, err := cfg.Check(f.Name.Name, fset, []*ast.File{f}, info); err != nil {
|
||||
return nil, fmt.Errorf("typecheck %s: %w", f.Name.Name, err)
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func parseConstDecls(genDecl *ast.GenDecl, info *types.Info, enumIndex map[string]int, schema *Schema) error {
|
||||
for _, spec := range genDecl.Specs {
|
||||
valueSpec, ok := spec.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, name := range valueSpec.Names {
|
||||
obj, ok := info.Defs[name].(*types.Const)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
named, ok := obj.Type().(*types.Named)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
idx, ok := enumIndex[named.Obj().Name()]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
schema.Enums[idx].Values = append(schema.Enums[idx].Values, EnumValue{
|
||||
Name: name.Name,
|
||||
Value: obj.Val().String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseStruct(
|
||||
pkg string,
|
||||
name string,
|
||||
st *ast.StructType,
|
||||
knownStructs map[string]bool,
|
||||
namedPrimitives map[string]PrimitiveKind,
|
||||
) (Message, error) {
|
||||
msg := Message{PackageName: pkg, Name: name}
|
||||
|
||||
for _, astField := range st.Fields.List {
|
||||
if len(astField.Names) == 0 {
|
||||
continue // embedded field, пропускаем
|
||||
}
|
||||
|
||||
var rawTag string
|
||||
if astField.Tag != nil {
|
||||
tag := reflect.StructTag(strings.Trim(astField.Tag.Value, "`"))
|
||||
rawTag = tag.Get("pack")
|
||||
}
|
||||
|
||||
for _, fieldName := range astField.Names {
|
||||
field, err := parseFieldType(fieldName.Name, astField.Type, rawTag, knownStructs, namedPrimitives)
|
||||
if err != nil {
|
||||
return Message{}, fmt.Errorf("field %s: %w", fieldName.Name, err)
|
||||
}
|
||||
|
||||
msg.Fields = append(msg.Fields, field)
|
||||
}
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func parseFieldType(
|
||||
name string,
|
||||
expr ast.Expr,
|
||||
rawTag string,
|
||||
knownStructs map[string]bool,
|
||||
namedPrimitives map[string]PrimitiveKind,
|
||||
) (Field, error) {
|
||||
switch t := expr.(type) {
|
||||
case *ast.Ident:
|
||||
return parsePrimitiveOrNested(name, t.Name, rawTag, knownStructs, namedPrimitives)
|
||||
|
||||
case *ast.ArrayType:
|
||||
if t.Len == nil {
|
||||
elem, err := parseFieldType("", t.Elt, rawTag, knownStructs, namedPrimitives)
|
||||
if err != nil {
|
||||
return Field{}, fmt.Errorf("slice element: %w", err)
|
||||
}
|
||||
|
||||
return Field{
|
||||
Name: name,
|
||||
Kind: KindSlice,
|
||||
Elem: &elem,
|
||||
}, nil
|
||||
}
|
||||
|
||||
n, err := parseArrayLen(t.Len)
|
||||
if err != nil {
|
||||
return Field{}, fmt.Errorf("array length: %w", err)
|
||||
}
|
||||
|
||||
elem, err := parseFieldType("", t.Elt, rawTag, knownStructs, namedPrimitives)
|
||||
if err != nil {
|
||||
return Field{}, fmt.Errorf("array element: %w", err)
|
||||
}
|
||||
|
||||
return Field{
|
||||
Name: name,
|
||||
Kind: KindFixedArray,
|
||||
Elem: &elem,
|
||||
FixedLen: n,
|
||||
}, nil
|
||||
|
||||
case *ast.StarExpr:
|
||||
return Field{}, fmt.Errorf("pointer types not supported")
|
||||
|
||||
case *ast.SelectorExpr:
|
||||
return Field{}, fmt.Errorf("external package types not supported (use only types from the same file)")
|
||||
}
|
||||
|
||||
return Field{}, fmt.Errorf("unsupported type expression %T", expr)
|
||||
}
|
||||
|
||||
func parsePrimitiveOrNested(
|
||||
name string,
|
||||
typeName string,
|
||||
rawTag string,
|
||||
knownStructs map[string]bool,
|
||||
namedPrimitives map[string]PrimitiveKind,
|
||||
) (Field, error) {
|
||||
primKind, isPrimitive := goPrimitiveKind(typeName)
|
||||
if !isPrimitive {
|
||||
if namedPrimitive, ok := namedPrimitives[typeName]; ok {
|
||||
return buildPrimitiveField(name, typeName, namedPrimitive, rawTag)
|
||||
}
|
||||
|
||||
if !knownStructs[typeName] {
|
||||
return Field{}, fmt.Errorf("unknown type %q (not a primitive and not defined in the same file)", typeName)
|
||||
}
|
||||
return Field{
|
||||
Name: name,
|
||||
Kind: KindNested,
|
||||
TypeName: typeName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return buildPrimitiveField(name, "", primKind, rawTag)
|
||||
}
|
||||
|
||||
func buildPrimitiveField(name, namedType string, primKind PrimitiveKind, rawTag string) (Field, error) {
|
||||
field := Field{
|
||||
Name: name,
|
||||
Kind: KindPrimitive,
|
||||
Primitive: primKind,
|
||||
NamedType: namedType,
|
||||
}
|
||||
|
||||
if rawTag != "" {
|
||||
if primKind != KindFloat32 && primKind != KindFloat64 {
|
||||
typeLabel := field.GoTypeName()
|
||||
return Field{}, fmt.Errorf("arpack tag can only be applied to float32/float64, got %s", typeLabel)
|
||||
}
|
||||
quant, err := parseQuantTag(rawTag)
|
||||
if err != nil {
|
||||
return Field{}, fmt.Errorf("arpack tag: %w", err)
|
||||
}
|
||||
field.Quant = quant
|
||||
}
|
||||
|
||||
return field, nil
|
||||
}
|
||||
|
||||
func parseQuantTag(tag string) (*QuantInfo, error) {
|
||||
info := &QuantInfo{Bits: 16}
|
||||
parts := strings.Split(tag, ",")
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
kv := strings.SplitN(p, "=", 2)
|
||||
if len(kv) != 2 {
|
||||
return nil, fmt.Errorf("invalid tag part %q (expected key=value)", p)
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(kv[0])
|
||||
val := strings.TrimSpace(kv[1])
|
||||
|
||||
switch key {
|
||||
case "min":
|
||||
v, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("min: %w", err)
|
||||
}
|
||||
|
||||
info.Min = v
|
||||
case "max":
|
||||
v, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("max: %w", err)
|
||||
}
|
||||
|
||||
info.Max = v
|
||||
case "bits":
|
||||
v, err := strconv.Atoi(val)
|
||||
if err != nil || (v != 8 && v != 16) {
|
||||
return nil, fmt.Errorf("bits must be 8 or 16, got %q", val)
|
||||
}
|
||||
|
||||
info.Bits = v
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown tag key %q", key)
|
||||
}
|
||||
}
|
||||
|
||||
if info.Max <= info.Min {
|
||||
return nil, fmt.Errorf("max (%.6g) must be greater than min (%.6g)", info.Max, info.Min)
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func parseArrayLen(expr ast.Expr) (int, error) {
|
||||
lit, ok := expr.(*ast.BasicLit)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("array length must be a literal integer constant")
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(lit.Value)
|
||||
if err != nil || n <= 0 {
|
||||
return 0, fmt.Errorf("array length must be a positive integer, got %q", lit.Value)
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func goPrimitiveKind(name string) (PrimitiveKind, bool) {
|
||||
switch name {
|
||||
case "float32":
|
||||
return KindFloat32, true
|
||||
case "float64":
|
||||
return KindFloat64, true
|
||||
case "int8":
|
||||
return KindInt8, true
|
||||
case "int16":
|
||||
return KindInt16, true
|
||||
case "int32", "int":
|
||||
return KindInt32, true
|
||||
case "int64":
|
||||
return KindInt64, true
|
||||
case "uint8", "byte":
|
||||
return KindUint8, true
|
||||
case "uint16":
|
||||
return KindUint16, true
|
||||
case "uint32", "uint":
|
||||
return KindUint32, true
|
||||
case "uint64":
|
||||
return KindUint64, true
|
||||
case "bool":
|
||||
return KindBool, true
|
||||
case "string":
|
||||
return KindString, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
const sampleSource = `package messages
|
||||
|
||||
type Vector3 struct {
|
||||
X float32 ` + "`" + `pack:"min=-500,max=500,bits=16"` + "`" + `
|
||||
Y float32 ` + "`" + `pack:"min=-500,max=500,bits=16"` + "`" + `
|
||||
Z float32 ` + "`" + `pack:"min=-500,max=500,bits=16"` + "`" + `
|
||||
}
|
||||
|
||||
type MoveMessage struct {
|
||||
Position Vector3
|
||||
Velocity [3]float32
|
||||
Waypoints []Vector3
|
||||
PlayerID uint32
|
||||
Name string
|
||||
Active bool
|
||||
}
|
||||
|
||||
type SpawnMessage struct {
|
||||
ID uint64
|
||||
Position Vector3
|
||||
Tags [4]string
|
||||
Data []uint8
|
||||
}
|
||||
`
|
||||
|
||||
const enumSource = `package messages
|
||||
|
||||
type Opcode uint16
|
||||
|
||||
const (
|
||||
OpcodeUnknown Opcode = iota
|
||||
OpcodeAuthorize
|
||||
OpcodeJoinRoom
|
||||
)
|
||||
|
||||
type EnvelopeMessage struct {
|
||||
Code Opcode
|
||||
Counter uint8
|
||||
}
|
||||
`
|
||||
|
||||
func TestParseSource_Primitives(t *testing.T) {
|
||||
msgs, err := ParseSource(sampleSource)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSource: %v", err)
|
||||
}
|
||||
if len(msgs) != 3 {
|
||||
t.Fatalf("expected 3 messages, got %d", len(msgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSource_Vector3(t *testing.T) {
|
||||
msgs, err := ParseSource(sampleSource)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSource: %v", err)
|
||||
}
|
||||
|
||||
v3 := msgs[0]
|
||||
if v3.Name != "Vector3" {
|
||||
t.Fatalf("expected Vector3, got %s", v3.Name)
|
||||
}
|
||||
if len(v3.Fields) != 3 {
|
||||
t.Fatalf("expected 3 fields, got %d", len(v3.Fields))
|
||||
}
|
||||
|
||||
for _, f := range v3.Fields {
|
||||
if f.Kind != KindPrimitive {
|
||||
t.Errorf("field %s: expected KindPrimitive, got %d", f.Name, f.Kind)
|
||||
}
|
||||
if f.Primitive != KindFloat32 {
|
||||
t.Errorf("field %s: expected KindFloat32, got %d", f.Name, f.Primitive)
|
||||
}
|
||||
if f.Quant == nil {
|
||||
t.Errorf("field %s: expected quant info, got nil", f.Name)
|
||||
continue
|
||||
}
|
||||
if f.Quant.Min != -500 || f.Quant.Max != 500 || f.Quant.Bits != 16 {
|
||||
t.Errorf("field %s: wrong quant info %+v", f.Name, f.Quant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSource_MoveMessage(t *testing.T) {
|
||||
msgs, err := ParseSource(sampleSource)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSource: %v", err)
|
||||
}
|
||||
|
||||
msg := msgs[1]
|
||||
if msg.Name != "MoveMessage" {
|
||||
t.Fatalf("expected MoveMessage, got %s", msg.Name)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
kind FieldKind
|
||||
typeName string
|
||||
fixedLen int
|
||||
elemKind FieldKind
|
||||
}{
|
||||
{"Position", KindNested, "Vector3", 0, 0},
|
||||
{"Velocity", KindFixedArray, "", 3, KindPrimitive},
|
||||
{"Waypoints", KindSlice, "", 0, KindNested},
|
||||
{"PlayerID", KindPrimitive, "", 0, 0},
|
||||
{"Name", KindPrimitive, "", 0, 0},
|
||||
{"Active", KindPrimitive, "", 0, 0},
|
||||
}
|
||||
|
||||
if len(msg.Fields) != len(tests) {
|
||||
t.Fatalf("expected %d fields, got %d", len(tests), len(msg.Fields))
|
||||
}
|
||||
|
||||
for i, tc := range tests {
|
||||
f := msg.Fields[i]
|
||||
if f.Name != tc.name {
|
||||
t.Errorf("[%d] expected field %s, got %s", i, tc.name, f.Name)
|
||||
}
|
||||
if f.Kind != tc.kind {
|
||||
t.Errorf("field %s: expected kind %d, got %d", tc.name, tc.kind, f.Kind)
|
||||
}
|
||||
if tc.typeName != "" && f.TypeName != tc.typeName {
|
||||
t.Errorf("field %s: expected TypeName %s, got %s", tc.name, tc.typeName, f.TypeName)
|
||||
}
|
||||
if tc.fixedLen > 0 {
|
||||
if f.FixedLen != tc.fixedLen {
|
||||
t.Errorf("field %s: expected FixedLen %d, got %d", tc.name, tc.fixedLen, f.FixedLen)
|
||||
}
|
||||
if f.Elem == nil {
|
||||
t.Errorf("field %s: Elem is nil", tc.name)
|
||||
} else if f.Elem.Kind != tc.elemKind {
|
||||
t.Errorf("field %s: Elem.Kind expected %d, got %d", tc.name, tc.elemKind, f.Elem.Kind)
|
||||
}
|
||||
}
|
||||
if tc.kind == KindSlice {
|
||||
if f.Elem == nil {
|
||||
t.Errorf("field %s: Elem is nil for slice", tc.name)
|
||||
} else if f.Elem.Kind != tc.elemKind {
|
||||
t.Errorf("field %s: Elem.Kind expected %d, got %d", tc.name, tc.elemKind, f.Elem.Kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSource_SpawnMessage(t *testing.T) {
|
||||
msgs, err := ParseSource(sampleSource)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSource: %v", err)
|
||||
}
|
||||
|
||||
msg := msgs[2]
|
||||
if msg.Name != "SpawnMessage" {
|
||||
t.Fatalf("expected SpawnMessage, got %s", msg.Name)
|
||||
}
|
||||
|
||||
// Tags: [4]string
|
||||
tags := msg.Fields[2]
|
||||
if tags.Kind != KindFixedArray || tags.FixedLen != 4 {
|
||||
t.Errorf("Tags: expected KindFixedArray[4], got kind=%d fixedLen=%d", tags.Kind, tags.FixedLen)
|
||||
}
|
||||
if tags.Elem == nil || tags.Elem.Primitive != KindString {
|
||||
t.Errorf("Tags: expected string element")
|
||||
}
|
||||
|
||||
// Data: []uint8
|
||||
data := msg.Fields[3]
|
||||
if data.Kind != KindSlice {
|
||||
t.Errorf("Data: expected KindSlice, got %d", data.Kind)
|
||||
}
|
||||
if data.Elem == nil || data.Elem.Primitive != KindUint8 {
|
||||
t.Errorf("Data: expected uint8 element")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSchemaSource_Enums(t *testing.T) {
|
||||
schema, err := ParseSchemaSource(enumSource)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSchemaSource: %v", err)
|
||||
}
|
||||
|
||||
if len(schema.Messages) != 1 {
|
||||
t.Fatalf("expected 1 message, got %d", len(schema.Messages))
|
||||
}
|
||||
if len(schema.Enums) != 1 {
|
||||
t.Fatalf("expected 1 enum, got %d", len(schema.Enums))
|
||||
}
|
||||
|
||||
enum := schema.Enums[0]
|
||||
if enum.Name != "Opcode" {
|
||||
t.Fatalf("expected enum Opcode, got %s", enum.Name)
|
||||
}
|
||||
if enum.Primitive != KindUint16 {
|
||||
t.Fatalf("expected Opcode base kind uint16, got %d", enum.Primitive)
|
||||
}
|
||||
if len(enum.Values) != 3 {
|
||||
t.Fatalf("expected 3 enum values, got %d", len(enum.Values))
|
||||
}
|
||||
if enum.Values[1].Name != "OpcodeAuthorize" || enum.Values[1].Value != "1" {
|
||||
t.Fatalf("unexpected enum value %#v", enum.Values[1])
|
||||
}
|
||||
|
||||
field := schema.Messages[0].Fields[0]
|
||||
if field.Kind != KindPrimitive {
|
||||
t.Fatalf("expected EnvelopeMessage.Code to be primitive, got %d", field.Kind)
|
||||
}
|
||||
if field.NamedType != "Opcode" {
|
||||
t.Fatalf("expected named type Opcode, got %q", field.NamedType)
|
||||
}
|
||||
if field.Primitive != KindUint16 {
|
||||
t.Fatalf("expected underlying uint16, got %d", field.Primitive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuantTag_Errors(t *testing.T) {
|
||||
cases := []struct {
|
||||
src string
|
||||
wantErr bool
|
||||
}{
|
||||
{`package p; type T struct { X float32 ` + "`" + `pack:"min=0,max=100"` + "`" + ` }`, false},
|
||||
{`package p; type T struct { X float32 ` + "`" + `pack:"min=100,max=0"` + "`" + ` }`, true}, // max < min
|
||||
{`package p; type T struct { X float32 ` + "`" + `pack:"min=0,max=100,bits=32"` + "`" + ` }`, true}, // bad bits
|
||||
{`package p; type T struct { X int32 ` + "`" + `pack:"min=0,max=100"` + "`" + ` }`, true}, // quant на int
|
||||
{`package p; type T struct { X float32 ` + "`" + `pack:"foo=1"` + "`" + ` }`, true}, // unknown key
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
_, err := ParseSource(tc.src)
|
||||
if tc.wantErr && err == nil {
|
||||
t.Errorf("[%d] expected error, got nil", i)
|
||||
}
|
||||
if !tc.wantErr && err != nil {
|
||||
t.Errorf("[%d] unexpected error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWireSize(t *testing.T) {
|
||||
msgs, err := ParseSource(sampleSource)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSource: %v", err)
|
||||
}
|
||||
|
||||
v3 := msgs[0]
|
||||
// Vector3: 3 × uint16 (квантизованные float32) = 6 байт
|
||||
if v3.MinWireSize() != 6 {
|
||||
t.Errorf("Vector3.MinWireSize: expected 6, got %d", v3.MinWireSize())
|
||||
}
|
||||
if v3.HasVariableFields() {
|
||||
t.Errorf("Vector3 should not have variable fields")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedUnknownType(t *testing.T) {
|
||||
src := `package p
|
||||
type Msg struct {
|
||||
Pos UnknownType
|
||||
}
|
||||
`
|
||||
_, err := ParseSource(src)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown nested type, got nil")
|
||||
}
|
||||
}
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
package parser
|
||||
|
||||
// PrimitiveKind — конкретный примитивный тип.
|
||||
type PrimitiveKind int
|
||||
|
||||
const (
|
||||
KindFloat32 PrimitiveKind = iota
|
||||
KindFloat64
|
||||
KindInt8
|
||||
KindInt16
|
||||
KindInt32
|
||||
KindInt64
|
||||
KindUint8
|
||||
KindUint16
|
||||
KindUint32
|
||||
KindUint64
|
||||
KindBool
|
||||
KindString
|
||||
)
|
||||
|
||||
// FieldKind — категория поля.
|
||||
type FieldKind int
|
||||
|
||||
const (
|
||||
KindPrimitive FieldKind = iota // float, int, uint, bool, string
|
||||
KindNested // ссылка на другой Message
|
||||
KindFixedArray // [N]T
|
||||
KindSlice // []T
|
||||
)
|
||||
|
||||
// QuantInfo описывает квантизацию float → uint8/uint16.
|
||||
type QuantInfo struct {
|
||||
Min float64
|
||||
Max float64
|
||||
Bits int // 8 или 16, default 16
|
||||
}
|
||||
|
||||
// MaxUint — максимальное целое значение для данного числа бит.
|
||||
func (q *QuantInfo) MaxUint() float64 {
|
||||
if q.Bits == 8 {
|
||||
return 255
|
||||
}
|
||||
return 65535
|
||||
}
|
||||
|
||||
// WireBytes — размер на проводе в байтах.
|
||||
func (q *QuantInfo) WireBytes() int {
|
||||
return q.Bits / 8
|
||||
}
|
||||
|
||||
// Field — одно поле структуры-сообщения.
|
||||
type Field struct {
|
||||
Name string
|
||||
Kind FieldKind
|
||||
|
||||
// KindPrimitive
|
||||
Primitive PrimitiveKind
|
||||
NamedType string
|
||||
Quant *QuantInfo // nil если нет квантизации
|
||||
|
||||
// KindNested
|
||||
TypeName string
|
||||
|
||||
// KindFixedArray / KindSlice
|
||||
Elem *Field
|
||||
FixedLen int // >0 только для KindFixedArray
|
||||
}
|
||||
|
||||
// WireSize — размер в байтах на проводе.
|
||||
// Возвращает -1 для полей переменного размера.
|
||||
func (f *Field) WireSize() int {
|
||||
switch f.Kind {
|
||||
case KindPrimitive:
|
||||
if f.Quant != nil {
|
||||
return f.Quant.WireBytes()
|
||||
}
|
||||
return primitiveWireSize(f.Primitive)
|
||||
case KindNested:
|
||||
return -1 // зависит от конкретного типа, узнаём через Message.MinWireSize
|
||||
case KindFixedArray:
|
||||
elemSize := f.Elem.WireSize()
|
||||
if elemSize == -1 {
|
||||
return -1
|
||||
}
|
||||
return f.FixedLen * elemSize
|
||||
case KindSlice:
|
||||
return -1 // uint16 len + переменная часть
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func primitiveWireSize(k PrimitiveKind) int {
|
||||
switch k {
|
||||
case KindFloat32, KindInt32, KindUint32:
|
||||
return 4
|
||||
case KindFloat64, KindInt64, KindUint64:
|
||||
return 8
|
||||
case KindInt16, KindUint16:
|
||||
return 2
|
||||
case KindInt8, KindUint8, KindBool:
|
||||
return 1
|
||||
case KindString:
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// IsIntegralPrimitive — подходит ли тип как базовый для enum.
|
||||
func IsIntegralPrimitive(k PrimitiveKind) bool {
|
||||
switch k {
|
||||
case KindInt8, KindInt16, KindInt32, KindInt64, KindUint8, KindUint16, KindUint32, KindUint64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GoTypeName — имя типа в Go.
|
||||
func (f *Field) GoTypeName() string {
|
||||
switch f.Kind {
|
||||
case KindPrimitive:
|
||||
if f.NamedType != "" {
|
||||
return f.NamedType
|
||||
}
|
||||
return primitiveGoName(f.Primitive)
|
||||
case KindNested:
|
||||
return f.TypeName
|
||||
case KindFixedArray:
|
||||
return "[" + itoa(f.FixedLen) + "]" + f.Elem.GoTypeName()
|
||||
case KindSlice:
|
||||
return "[]" + f.Elem.GoTypeName()
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// CSharpTypeName — имя типа в C#.
|
||||
func (f *Field) CSharpTypeName() string {
|
||||
switch f.Kind {
|
||||
case KindPrimitive:
|
||||
if f.NamedType != "" {
|
||||
return f.NamedType
|
||||
}
|
||||
return primitiveCSharpName(f.Primitive)
|
||||
case KindNested:
|
||||
return f.TypeName
|
||||
case KindFixedArray:
|
||||
return f.Elem.CSharpTypeName() + "[]"
|
||||
case KindSlice:
|
||||
return f.Elem.CSharpTypeName() + "[]"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func primitiveGoName(k PrimitiveKind) string {
|
||||
switch k {
|
||||
case KindFloat32:
|
||||
return "float32"
|
||||
case KindFloat64:
|
||||
return "float64"
|
||||
case KindInt8:
|
||||
return "int8"
|
||||
case KindInt16:
|
||||
return "int16"
|
||||
case KindInt32:
|
||||
return "int32"
|
||||
case KindInt64:
|
||||
return "int64"
|
||||
case KindUint8:
|
||||
return "uint8"
|
||||
case KindUint16:
|
||||
return "uint16"
|
||||
case KindUint32:
|
||||
return "uint32"
|
||||
case KindUint64:
|
||||
return "uint64"
|
||||
case KindBool:
|
||||
return "bool"
|
||||
case KindString:
|
||||
return "string"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// GoPrimitiveTypeName — базовый примитивный тип поля в Go.
|
||||
func (f *Field) GoPrimitiveTypeName() string {
|
||||
return primitiveGoName(f.Primitive)
|
||||
}
|
||||
|
||||
func primitiveCSharpName(k PrimitiveKind) string {
|
||||
switch k {
|
||||
case KindFloat32:
|
||||
return "float"
|
||||
case KindFloat64:
|
||||
return "double"
|
||||
case KindInt8:
|
||||
return "sbyte"
|
||||
case KindInt16:
|
||||
return "short"
|
||||
case KindInt32:
|
||||
return "int"
|
||||
case KindInt64:
|
||||
return "long"
|
||||
case KindUint8:
|
||||
return "byte"
|
||||
case KindUint16:
|
||||
return "ushort"
|
||||
case KindUint32:
|
||||
return "uint"
|
||||
case KindUint64:
|
||||
return "ulong"
|
||||
case KindBool:
|
||||
return "bool"
|
||||
case KindString:
|
||||
return "string"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// CSharpPrimitiveTypeName — базовый примитивный тип поля в C#.
|
||||
func (f *Field) CSharpPrimitiveTypeName() string {
|
||||
return primitiveCSharpName(f.Primitive)
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
buf := [20]byte{}
|
||||
pos := len(buf)
|
||||
for n > 0 {
|
||||
pos--
|
||||
buf[pos] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
return string(buf[pos:])
|
||||
}
|
||||
|
||||
// Message — описание одной структуры-сообщения.
|
||||
type Message struct {
|
||||
PackageName string
|
||||
Name string
|
||||
Fields []Field
|
||||
}
|
||||
|
||||
// EnumValue — одно именованное значение enum.
|
||||
type EnumValue struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Enum — enum-подобный тип на основе именованного примитива.
|
||||
type Enum struct {
|
||||
Name string
|
||||
Primitive PrimitiveKind
|
||||
Values []EnumValue
|
||||
}
|
||||
|
||||
// Schema — полная модель входного файла.
|
||||
type Schema struct {
|
||||
PackageName string
|
||||
Messages []Message
|
||||
Enums []Enum
|
||||
}
|
||||
|
||||
// MinWireSize — минимальный гарантированный размер в байтах.
|
||||
// Для вложенных типов считается только если размер известен заранее.
|
||||
// Строки и слайсы считаются как 2 байта (length prefix).
|
||||
func (m *Message) MinWireSize() int {
|
||||
total := 0
|
||||
for _, f := range m.Fields {
|
||||
s := f.WireSize()
|
||||
if s == -1 {
|
||||
total += 2 // минимум: length prefix
|
||||
} else {
|
||||
total += s
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// HasVariableFields — есть ли поля переменного размера.
|
||||
func (m *Message) HasVariableFields() bool {
|
||||
for _, f := range m.Fields {
|
||||
if f.WireSize() == -1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user