package bfcodegen import ( "errors" "fmt" "gogs.mikescher.com/BlackForestBytes/goext" "gogs.mikescher.com/BlackForestBytes/goext/cmdext" "gogs.mikescher.com/BlackForestBytes/goext/cryptext" "gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/rext" "io" "os" "path" "path/filepath" "regexp" "strings" "time" ) type EnumDefVal struct { VarName string Value string Description *string } type EnumDef struct { File string FileRelative string EnumTypeName string Type string Values []EnumDefVal } var rexPackage = 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 rexValueDef = rext.W(regexp.MustCompile("^\\s*(?P[A-Za-z0-9_]+)\\s+(?P[A-Za-z0-9_]+)\\s*=\\s*(?P(\"[A-Za-z0-9_:]+\"|[0-9]+))\\s*(//(?P.*))?.*$")) var rexChecksumConst = rext.W(regexp.MustCompile("const ChecksumGenerator = \"(?P[A-Za-z0-9_]*)\"")) func GenerateEnumSpecs(sourceDir string, destFile string) error { files, err := os.ReadDir(sourceDir) if err != nil { return err } oldChecksum := "N/A" if _, err := os.Stat(destFile); !os.IsNotExist(err) { content, err := os.ReadFile(destFile) if err != nil { return err } if m, ok := rexChecksumConst.MatchFirst(string(content)); ok { oldChecksum = m.GroupByName("cs").Value() } } 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") }) 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 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 nil } allEnums := make([]EnumDef, 0) pkgname := "" for _, f := range files { fmt.Printf("========= %s =========\n\n", f.Name()) fileEnums, pn, err := processFile(sourceDir, path.Join(sourceDir, f.Name())) if err != nil { return err } fmt.Printf("\n") allEnums = append(allEnums, fileEnums...) if pn != "" { pkgname = pn } } if pkgname == "" { return errors.New("no package name found in any file") } err = os.WriteFile(destFile, []byte(fmtOutput(newChecksum, allEnums, pkgname)), 0o755) if err != nil { return err } res, err := cmdext.RunCommand("go", []string{"fmt", destFile}, langext.Ptr(2*time.Second)) if err != nil { return err } if res.CommandTimedOut { fmt.Println(res.StdCombined) return errors.New("go fmt timed out") } if res.ExitCode != 0 { fmt.Println(res.StdCombined) return errors.New("go fmt did not succeed") } return nil } func processFile(basedir string, fn string) ([]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 := rexPackage.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) fmt.Printf("Found enum definition { '%s' -> '%s' }\n", def.EnumTypeName, def.Type) } if match, ok := rexValueDef.MatchFirst(line); ok { typename := match.GroupByName("type").Value() def := EnumDefVal{ VarName: match.GroupByName("name").Value(), Value: match.GroupByName("value").Value(), Description: match.GroupByNameOrEmpty("descr").ValueOrNil(), } found := false for i, v := range enums { if v.EnumTypeName == typename { enums[i].Values = append(enums[i].Values, def) found = true 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 { 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 fmtOutput(cs string, enums []EnumDef, pkgname string) string { str := "// Code generated by enum-generate.go DO NOT EDIT.\n" str += "\n" str += "package " + pkgname + "\n" str += "\n" str += "import \"gogs.mikescher.com/BlackForestBytes/goext/langext\"" + "\n" str += "\n" str += "const ChecksumGenerator = \"" + cs + "\"" + "\n" str += "\n" str += "type Enum interface {" + "\n" str += " Valid() bool" + "\n" str += " ValuesAny() []any" + "\n" str += " ValuesMeta() []EnumMetaValue" + "\n" str += " VarName() string" + "\n" str += "}" + "\n" str += "" + "\n" str += "type StringEnum interface {" + "\n" str += " Enum" + "\n" str += " String() string" + "\n" str += "}" + "\n" str += "" + "\n" str += "type DescriptionEnum interface {" + "\n" str += " Enum" + "\n" str += " Description() string" + "\n" str += "}" + "\n" str += "\n" str += "type EnumMetaValue struct {" + "\n" str += " VarName string `json:\"varName\"`" + "\n" str += " Value any `json:\"value\"`" + "\n" str += " Description *string `json:\"description\"`" + "\n" str += "}" + "\n" str += "\n" for _, enumdef := range enums { hasDescr := langext.ArrAll(enumdef.Values, func(val EnumDefVal) bool { return val.Description != nil }) hasStr := enumdef.Type == "string" str += "// ================================ " + enumdef.EnumTypeName + " ================================" + "\n" str += "//" + "\n" str += "// File: " + enumdef.FileRelative + "\n" str += "// StringEnum: " + langext.Conditional(hasStr, "true", "false") + "\n" str += "// DescrEnum: " + langext.Conditional(hasDescr, "true", "false") + "\n" str += "//" + "\n" str += "" + "\n" str += "var __" + enumdef.EnumTypeName + "Values = []" + enumdef.EnumTypeName + "{" + "\n" for _, v := range enumdef.Values { str += " " + v.VarName + "," + "\n" } str += "}" + "\n" str += "" + "\n" if hasDescr { str += "var __" + enumdef.EnumTypeName + "Descriptions = map[" + enumdef.EnumTypeName + "]string{" + "\n" for _, v := range enumdef.Values { str += " " + v.VarName + ": \"" + strings.TrimSpace(*v.Description) + "\"," + "\n" } str += "}" + "\n" str += "" + "\n" } str += "var __" + enumdef.EnumTypeName + "Varnames = map[" + enumdef.EnumTypeName + "]string{" + "\n" for _, v := range enumdef.Values { str += " " + v.VarName + ": \"" + v.VarName + "\"," + "\n" } str += "}" + "\n" str += "" + "\n" str += "func (e " + enumdef.EnumTypeName + ") Valid() bool {" + "\n" str += " return langext.InArray(e, __" + enumdef.EnumTypeName + "Values)" + "\n" str += "}" + "\n" str += "" + "\n" str += "func (e " + enumdef.EnumTypeName + ") Values() []" + enumdef.EnumTypeName + " {" + "\n" str += " return __" + enumdef.EnumTypeName + "Values" + "\n" str += "}" + "\n" str += "" + "\n" str += "func (e " + enumdef.EnumTypeName + ") ValuesAny() []any {" + "\n" str += " return langext.ArrCastToAny(__" + enumdef.EnumTypeName + "Values)" + "\n" str += "}" + "\n" str += "" + "\n" str += "func (e " + enumdef.EnumTypeName + ") ValuesMeta() []EnumMetaValue {" + "\n" str += " return []EnumMetaValue{" + "\n" for _, v := range enumdef.Values { if hasDescr { str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: langext.Ptr(\"%s\")},", v.VarName, v.VarName, strings.TrimSpace(*v.Description)) + "\n" } else { str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: nil},", v.VarName, v.VarName) + "\n" } } str += " }" + "\n" str += "}" + "\n" str += "" + "\n" if hasStr { str += "func (e " + enumdef.EnumTypeName + ") String() string {" + "\n" str += " return string(e)" + "\n" str += "}" + "\n" str += "" + "\n" } if hasDescr { str += "func (e " + enumdef.EnumTypeName + ") Description() string {" + "\n" str += " if d, ok := __" + enumdef.EnumTypeName + "Descriptions[e]; ok {" + "\n" str += " return d" + "\n" str += " }" + "\n" str += " return \"\"" + "\n" str += "}" + "\n" str += "" + "\n" } str += "func (e " + enumdef.EnumTypeName + ") VarName() string {" + "\n" str += " if d, ok := __" + enumdef.EnumTypeName + "Varnames[e]; ok {" + "\n" str += " return d" + "\n" str += " }" + "\n" str += " return \"\"" + "\n" str += "}" + "\n" str += "" + "\n" str += "func Parse" + enumdef.EnumTypeName + "(vv string) (" + enumdef.EnumTypeName + ", bool) {" + "\n" str += " for _, ev := range __" + enumdef.EnumTypeName + "Values {" + "\n" str += " if string(ev) == vv {" + "\n" str += " return ev, true" + "\n" str += " }" + "\n" str += " }" + "\n" str += " return \"\", false" + "\n" str += "}" + "\n" str += "" + "\n" str += "func " + enumdef.EnumTypeName + "Values() []" + enumdef.EnumTypeName + " {" + "\n" str += " return __" + enumdef.EnumTypeName + "Values" + "\n" str += "}" + "\n" str += "" + "\n" str += "func " + enumdef.EnumTypeName + "ValuesMeta() []EnumMetaValue {" + "\n" str += " return []EnumMetaValue{" + "\n" for _, v := range enumdef.Values { if hasDescr { str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: langext.Ptr(\"%s\")},", v.VarName, v.VarName, strings.TrimSpace(*v.Description)) + "\n" } else { str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: nil},", v.VarName, v.VarName) + "\n" } } str += " }" + "\n" str += "}" + "\n" str += "" + "\n" } return str }