Mike Schwörer
03a9b276d8
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 7m36s
374 lines
9.1 KiB
Go
374 lines
9.1 KiB
Go
package bfcodegen
|
|
|
|
import (
|
|
"bytes"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"go/format"
|
|
"gogs.mikescher.com/BlackForestBytes/goext"
|
|
"gogs.mikescher.com/BlackForestBytes/goext/cryptext"
|
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
|
"gogs.mikescher.com/BlackForestBytes/goext/rext"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"text/template"
|
|
)
|
|
|
|
type EnumDefVal struct {
|
|
VarName string
|
|
Value string
|
|
Description *string
|
|
Data *map[string]any
|
|
RawComment *string
|
|
}
|
|
|
|
type EnumDef struct {
|
|
File string
|
|
FileRelative string
|
|
EnumTypeName string
|
|
Type string
|
|
Values []EnumDefVal
|
|
}
|
|
|
|
type EnumGenOptions struct {
|
|
DebugOutput *bool
|
|
GoFormat *bool
|
|
}
|
|
|
|
var rexEnumPackage = rext.W(regexp.MustCompile(`^package\s+(?P<name>[A-Za-z0-9_]+)\s*$`))
|
|
|
|
var rexEnumDef = rext.W(regexp.MustCompile(`^\s*type\s+(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*//\s*(@enum:type).*$`))
|
|
|
|
var rexEnumValueDef = rext.W(regexp.MustCompile(`^\s*(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*=\s*(?P<value>("[A-Za-z0-9_:\s\-.]*"|[0-9]+))\s*(//(?P<comm>.*))?.*$`))
|
|
|
|
var rexEnumChecksumConst = rext.W(regexp.MustCompile(`const ChecksumEnumGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))
|
|
|
|
//go:embed enum-generate.template
|
|
var templateEnumGenerateText string
|
|
|
|
func GenerateEnumSpecs(sourceDir string, destFile string, opt EnumGenOptions) error {
|
|
|
|
oldChecksum := "N/A"
|
|
if _, err := os.Stat(destFile); !os.IsNotExist(err) {
|
|
content, err := os.ReadFile(destFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if m, ok := rexEnumChecksumConst.MatchFirst(string(content)); ok {
|
|
oldChecksum = m.GroupByName("cs").Value()
|
|
}
|
|
}
|
|
|
|
gocode, _, changed, err := _generateEnumSpecs(sourceDir, destFile, oldChecksum, langext.Coalesce(opt.GoFormat, true), langext.Coalesce(opt.DebugOutput, false))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !changed {
|
|
return nil
|
|
}
|
|
|
|
err = os.WriteFile(destFile, []byte(gocode), 0o755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func _generateEnumSpecs(sourceDir string, destFile string, oldChecksum string, gofmt bool, debugOutput bool) (string, string, bool, error) {
|
|
|
|
files, err := os.ReadDir(sourceDir)
|
|
if err != nil {
|
|
return "", "", false, err
|
|
}
|
|
|
|
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return v.Name() != path.Base(destFile) })
|
|
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return strings.HasSuffix(v.Name(), ".go") })
|
|
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") })
|
|
langext.SortBy(files, func(v os.DirEntry) string { return v.Name() })
|
|
|
|
newChecksumStr := goext.GoextVersion
|
|
for _, f := range files {
|
|
content, err := os.ReadFile(path.Join(sourceDir, f.Name()))
|
|
if err != nil {
|
|
return "", "", false, err
|
|
}
|
|
newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content)
|
|
}
|
|
|
|
newChecksum := cryptext.BytesSha256([]byte(newChecksumStr))
|
|
|
|
if newChecksum != oldChecksum {
|
|
fmt.Printf("[EnumGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum)
|
|
} else {
|
|
fmt.Printf("[EnumGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum)
|
|
return "", oldChecksum, false, nil
|
|
}
|
|
|
|
allEnums := make([]EnumDef, 0)
|
|
|
|
pkgname := ""
|
|
|
|
for _, f := range files {
|
|
if debugOutput {
|
|
fmt.Printf("========= %s =========\n\n", f.Name())
|
|
}
|
|
|
|
fileEnums, pn, err := processEnumFile(sourceDir, path.Join(sourceDir, f.Name()), debugOutput)
|
|
if err != nil {
|
|
return "", "", false, err
|
|
}
|
|
|
|
if debugOutput {
|
|
fmt.Printf("\n")
|
|
}
|
|
|
|
allEnums = append(allEnums, fileEnums...)
|
|
|
|
if pn != "" {
|
|
pkgname = pn
|
|
}
|
|
}
|
|
|
|
if pkgname == "" {
|
|
return "", "", false, errors.New("no package name found in any file")
|
|
}
|
|
|
|
rdata := fmtEnumOutput(newChecksum, allEnums, pkgname)
|
|
|
|
if !gofmt {
|
|
return rdata, newChecksum, true, nil
|
|
}
|
|
|
|
fdata, err := format.Source([]byte(rdata))
|
|
if err != nil {
|
|
return "", "", false, err
|
|
}
|
|
|
|
return string(fdata), newChecksum, true, nil
|
|
}
|
|
|
|
func processEnumFile(basedir string, fn string, debugOutput bool) ([]EnumDef, string, error) {
|
|
file, err := os.Open(fn)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
defer func() { _ = file.Close() }()
|
|
|
|
bin, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
lines := strings.Split(string(bin), "\n")
|
|
|
|
enums := make([]EnumDef, 0)
|
|
|
|
pkgname := ""
|
|
|
|
for i, line := range lines {
|
|
if i == 0 && strings.HasPrefix(line, "// Code generated by") {
|
|
break
|
|
}
|
|
|
|
if match, ok := rexEnumPackage.MatchFirst(line); i == 0 && ok {
|
|
pkgname = match.GroupByName("name").Value()
|
|
continue
|
|
}
|
|
|
|
if match, ok := rexEnumDef.MatchFirst(line); ok {
|
|
|
|
rfp, err := filepath.Rel(basedir, fn)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
def := EnumDef{
|
|
File: fn,
|
|
FileRelative: rfp,
|
|
EnumTypeName: match.GroupByName("name").Value(),
|
|
Type: match.GroupByName("type").Value(),
|
|
Values: make([]EnumDefVal, 0),
|
|
}
|
|
enums = append(enums, def)
|
|
|
|
if debugOutput {
|
|
fmt.Printf("Found enum definition { '%s' -> '%s' }\n", def.EnumTypeName, def.Type)
|
|
}
|
|
}
|
|
|
|
if match, ok := rexEnumValueDef.MatchFirst(line); ok {
|
|
typename := match.GroupByName("type").Value()
|
|
|
|
comment := match.GroupByNameOrEmpty("comm").ValueOrNil()
|
|
var descr *string = nil
|
|
var data *map[string]any = nil
|
|
if comment != nil {
|
|
comment = langext.Ptr(strings.TrimSpace(*comment))
|
|
if strings.HasPrefix(*comment, "{") {
|
|
if v, ok := tryParseDataComment(*comment); ok {
|
|
data = &v
|
|
if anyDataDescr, ok := v["description"]; ok {
|
|
if dataDescr, ok := anyDataDescr.(string); ok {
|
|
descr = &dataDescr
|
|
}
|
|
}
|
|
} else {
|
|
descr = comment
|
|
}
|
|
} else {
|
|
descr = comment
|
|
}
|
|
}
|
|
|
|
def := EnumDefVal{
|
|
VarName: match.GroupByName("name").Value(),
|
|
Value: match.GroupByName("value").Value(),
|
|
RawComment: comment,
|
|
Description: descr,
|
|
Data: data,
|
|
}
|
|
|
|
found := false
|
|
for i, v := range enums {
|
|
if v.EnumTypeName == typename {
|
|
enums[i].Values = append(enums[i].Values, def)
|
|
found = true
|
|
|
|
if debugOutput {
|
|
if def.Description != nil {
|
|
fmt.Printf("Found enum value [%s] for '%s' ('%s')\n", def.Value, def.VarName, *def.Description)
|
|
} else {
|
|
fmt.Printf("Found enum value [%s] for '%s'\n", def.Value, def.VarName)
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
if debugOutput {
|
|
fmt.Printf("Found non-enum value [%s] for '%s' ( looks like enum value, but no matching @enum:type )\n", def.Value, def.VarName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return enums, pkgname, nil
|
|
}
|
|
|
|
func tryParseDataComment(s string) (map[string]any, bool) {
|
|
|
|
r := make(map[string]any)
|
|
|
|
err := json.Unmarshal([]byte(s), &r)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
|
|
for _, v := range r {
|
|
|
|
rv := reflect.ValueOf(v)
|
|
|
|
if rv.Kind() == reflect.Ptr && rv.IsNil() {
|
|
continue
|
|
}
|
|
if rv.Kind() == reflect.Bool {
|
|
continue
|
|
}
|
|
if rv.Kind() == reflect.String {
|
|
continue
|
|
}
|
|
if rv.Kind() == reflect.Int64 {
|
|
continue
|
|
}
|
|
if rv.Kind() == reflect.Float64 {
|
|
continue
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
return r, true
|
|
}
|
|
|
|
func fmtEnumOutput(cs string, enums []EnumDef, pkgname string) string {
|
|
|
|
templ := template.New("enum-generate")
|
|
|
|
templ = templ.Funcs(template.FuncMap{
|
|
"boolToStr": func(b bool) string { return langext.Conditional(b, "true", "false") },
|
|
"deref": func(v *string) string { return *v },
|
|
"trimSpace": func(str string) string { return strings.TrimSpace(str) },
|
|
"hasStr": func(v EnumDef) bool { return v.Type == "string" },
|
|
"hasDescr": func(v EnumDef) bool {
|
|
return langext.ArrAll(v.Values, func(val EnumDefVal) bool { return val.Description != nil })
|
|
},
|
|
"hasData": func(v EnumDef) bool {
|
|
return len(v.Values) > 0 && langext.ArrAll(v.Values, func(val EnumDefVal) bool { return val.Data != nil })
|
|
},
|
|
"gostr": func(v any) string {
|
|
return fmt.Sprintf("%#+v", v)
|
|
},
|
|
"goobj": func(name string, v any) string {
|
|
return fmt.Sprintf("%#+v", v)
|
|
},
|
|
"godatakey": func(v string) string {
|
|
return strings.ToUpper(v[0:1]) + v[1:]
|
|
},
|
|
"godatavalue": func(v any) string {
|
|
return fmt.Sprintf("%#+v", v)
|
|
},
|
|
"godatatype": func(v any) string {
|
|
return fmt.Sprintf("%T", v)
|
|
},
|
|
"mapindex": func(v map[string]any, k string) any {
|
|
return v[k]
|
|
},
|
|
"generalDataKeys": func(v EnumDef) map[string]string {
|
|
r0 := make(map[string]int)
|
|
|
|
for _, eval := range v.Values {
|
|
for k := range *eval.Data {
|
|
if ctr, ok := r0[k]; ok {
|
|
r0[k] = ctr + 1
|
|
} else {
|
|
r0[k] = 1
|
|
}
|
|
}
|
|
}
|
|
|
|
r1 := langext.MapToArr(r0)
|
|
r2 := langext.ArrFilter(r1, func(p langext.MapEntry[string, int]) bool { return p.Value == len(v.Values) })
|
|
r3 := langext.ArrMap(r2, func(p langext.MapEntry[string, int]) string { return p.Key })
|
|
r4 := langext.ArrToKVMap(r3, func(p string) string { return p }, func(p string) string { return fmt.Sprintf("%T", (*v.Values[0].Data)[p]) })
|
|
|
|
return r4
|
|
},
|
|
})
|
|
|
|
templ = template.Must(templ.Parse(templateEnumGenerateText))
|
|
|
|
buffer := bytes.Buffer{}
|
|
|
|
err := templ.Execute(&buffer, langext.H{
|
|
"PkgName": pkgname,
|
|
"Checksum": cs,
|
|
"GoextVersion": goext.GoextVersion,
|
|
"Enums": enums,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return buffer.String()
|
|
}
|