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[A-Za-z0-9_]+)\s*$`)) var rexEnumDef = rext.W(regexp.MustCompile(`^\s*type\s+(?P[A-Za-z0-9_]+)\s+(?P[A-Za-z0-9_]+)\s*//\s*(@enum:type).*$`)) var rexEnumValueDef = rext.W(regexp.MustCompile(`^\s*(?P[A-Za-z0-9_]+)\s+(?P[A-Za-z0-9_]+)\s*=\s*(?P("[A-Za-z0-9_:\s\-.]*"|[0-9]+))\s*(//(?P.*))?.*$`)) var rexEnumChecksumConst = rext.W(regexp.MustCompile(`const ChecksumEnumGenerator = "(?P[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() }