feat: new parser & generator

This commit is contained in:
2025-03-09 13:12:12 +07:00
parent 70fc3345d8
commit a3283b9a57
25 changed files with 372 additions and 301 deletions

41
model/gen_field.go Normal file
View File

@@ -0,0 +1,41 @@
package model
import (
"bytes"
"fmt"
"text/template"
)
const fieldTemplate = `{
{{ if .Metadata.Hidden }} hidden: true,
{{ end }}{{ if .Metadata.Label }} russian: "{{ .Metadata.Label }}",
{{ end }}{{ if .Metadata.Readonly }} readonly: true,
{{ end }}{{ if .IsArray }} many: true,
{{ end }}{{ .GeneratedType }}
}`
type FieldTemplateContext struct {
Metadata FieldMetadata
IsArray bool
GeneratedType string
}
func (field *Field) GenerateFieldCode() string {
tmpl, err := template.New("field").Parse(fieldTemplate)
if err != nil {
panic(fmt.Sprintf("Error parsing field template: %v", err))
}
data := FieldTemplateContext{
Metadata: field.Metadata,
IsArray: field.Metadata.IsSlice,
GeneratedType: field.GenerateType(),
}
var result bytes.Buffer
if err := tmpl.Execute(&result, data); err != nil {
panic(fmt.Sprintf("Error executing field template: %v", err))
}
return result.String()
}

72
model/gen_field_type.go Normal file
View File

@@ -0,0 +1,72 @@
package model
import (
"bytes"
"fmt"
"go/ast"
"strings"
"text/template"
)
var PrimitiveTypes = map[string]string{
"string": "string",
"boolean": "boolean",
"bool": "boolean",
"int": "number",
"uint": "number",
"float32": "number",
"float64": "number",
"int32": "number",
"int64": "number",
"uint32": "number",
"uint64": "number",
"int8": "number",
"int16": "number",
"uint8": "number",
"uint16": "number",
"byte": "number",
"rune": "number",
}
const typeTemplate = ` type: {
{{ if .IsPrimitive }} primitive: "{{ .PrimitiveType }}",{{ else }} nested: {
values: [],
field: {{ .Field }}
}, {{ end }}
},`
type TypeTemplateContext struct {
IsPrimitive bool
PrimitiveType string
Field string
}
func (field *Field) GenerateType() string {
keys := make([]string, 0, len(PrimitiveTypes))
for k := range PrimitiveTypes {
keys = append(keys, k)
}
var data TypeTemplateContext
if field.Metadata.IsPrimitiveType {
data.IsPrimitive = true
typeName := field.Type.(*ast.Ident).Name
data.PrimitiveType = PrimitiveTypes[typeName]
} else {
data.IsPrimitive = false
data.Field = "['" + strings.Join(field.Metadata.RelatedFields, "', '") + "']"
}
tmpl, err := template.New("type").Parse(typeTemplate)
if err != nil {
panic(fmt.Sprintf("Error parsing template: %v", err))
}
var result bytes.Buffer
if err := tmpl.Execute(&result, data); err != nil {
panic(fmt.Sprintf("Error executing template: %v", err))
}
return result.String()
}

View File

@@ -0,0 +1,13 @@
package model
import "nto_cli/utils"
func GetNotImplementedModels(models []Model) []Model {
var unimplementedModels []Model
for _, m := range models {
if !utils.IsEntityImplemented(m.Name) {
unimplementedModels = append(unimplementedModels, m)
}
}
return unimplementedModels
}

27
model/model.go Normal file
View File

@@ -0,0 +1,27 @@
package model
import "go/ast"
type Model struct {
Name string
Fields []Field
}
type Field struct {
Name string
Type ast.Expr
Tag string
Metadata FieldMetadata
}
type FieldMetadata struct {
Hidden bool
Readonly bool
Label string
IsSlice bool
IsPrimitiveType bool
IsRelatedModel bool
RelatedModel string
Datatype string
RelatedFields []string
}

151
model/parse_models.go Normal file
View File

@@ -0,0 +1,151 @@
package model
import (
"go/ast"
"go/parser"
"go/token"
"log/slog"
"nto_cli/utils"
"os"
"path/filepath"
"strings"
"github.com/kuzgoga/fogg"
)
func ParseModelsPackage(modelsPkgDir string) ([]Model, error) {
var models []Model
fileset := token.NewFileSet()
files, err := os.ReadDir(modelsPkgDir)
if err != nil {
return nil, err
}
for _, file := range files {
filepathPath := filepath.Join(modelsPkgDir, file.Name())
fileAst, err := parser.ParseFile(fileset, filepathPath, nil, parser.DeclarationErrors)
if err != nil {
return nil, err
}
models = append(models, parseFileStructs(fileset, fileAst.Decls)...)
}
ParseRelatedModels(&models)
return models, nil
}
func parseFileStructs(fileSet *token.FileSet, decls []ast.Decl) []Model {
var models []Model
for _, decl := range decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok {
continue
}
if genDecl.Tok != token.TYPE {
continue
}
for _, spec := range genDecl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
if _, ok := typeSpec.Type.(*ast.StructType); ok && typeSpec.Name != nil {
models = append(models, parseModelDecl(fileSet, typeSpec))
}
}
}
return models
}
func parseModelDecl(fileset *token.FileSet, decl *ast.TypeSpec) Model {
name := decl.Name.Name
structType, _ := decl.Type.(*ast.StructType)
var fields []Field
for _, fieldDecl := range structType.Fields.List {
fieldPos := fileset.Position(fieldDecl.Pos()).String()
if len(fieldDecl.Names) == 0 {
slog.Error("%s Embedded structure isn't supported", fieldPos)
continue
}
var tag string
if fieldDecl.Tag != nil {
tag = fieldDecl.Tag.Value[1 : len(fieldDecl.Tag.Value)-1]
}
storage, err := fogg.Parse(tag)
if err != nil {
slog.Error("%s Struct tag parsing error: %s", fieldPos, err)
}
var metadata FieldMetadata
if storage.HasTag("ui") {
uiTag := storage.GetTag("ui")
metadata.Hidden = uiTag.HasOption("hidden")
metadata.Label = uiTag.GetParamOr("label", "")
metadata.IsSlice = utils.IsSlice(fieldDecl.Type)
metadata.IsPrimitiveType = utils.IsPrimitiveType(fieldDecl.Type)
metadata.Datatype = uiTag.GetParamOr("datatype", "")
if uiTag.HasParam("field") {
metadata.RelatedFields = strings.Split(uiTag.GetParam("field").Value, ".")
}
} else {
slog.Warn("%s Field does not have a UI tag", fieldPos)
}
field := Field{
Name: fieldDecl.Names[0].Name,
Type: fieldDecl.Type,
Tag: tag,
Metadata: metadata,
}
fields = append(fields, field)
}
return Model{
Name: name,
Fields: fields,
}
}
func ParseRelatedModels(models *[]Model) {
for i := range *models {
model := &(*models)[i]
for j := range model.Fields {
field := &model.Fields[j]
if field.Metadata.IsPrimitiveType {
continue
}
relatedModelName := utils.ResolveBaseType(field.Type)
if relatedModelName == nil {
slog.Error("Failed to resolve base type for field `%s` in model `%s`", field.Name, model.Name)
continue
}
found := false
for _, m := range *models {
if m.Name == *relatedModelName {
field.Metadata.RelatedModel = m.Name
field.Metadata.IsRelatedModel = true
found = true
break
}
}
if !found {
slog.Error("Cannot classify field type `%s` in model `%s`, type `%s`",
field.Name, model.Name, *relatedModelName)
continue
}
}
}
}