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 IDDef struct { File string FileRelative string Name string } var rexIDPackage = rext.W(regexp.MustCompile(`^package\s+(?P[A-Za-z0-9_]+)\s*$`)) var rexIDDef = rext.W(regexp.MustCompile(`^\s*type\s+(?P[A-Za-z0-9_]+)\s+string\s*//\s*(@id:type).*$`)) var rexIDChecksumConst = rext.W(regexp.MustCompile(`const ChecksumIDGenerator = "(?P[A-Za-z0-9_]*)"`)) func GenerateIDSpecs(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 := rexIDChecksumConst.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") }) 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 err } newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content) } newChecksum := cryptext.BytesSha256([]byte(newChecksumStr)) if newChecksum != oldChecksum { fmt.Printf("[IDGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum) } else { fmt.Printf("[IDGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum) return nil } allIDs := make([]IDDef, 0) pkgname := "" for _, f := range files { fmt.Printf("========= %s =========\n\n", f.Name()) fileIDs, pn, err := processIDFile(sourceDir, path.Join(sourceDir, f.Name())) if err != nil { return err } fmt.Printf("\n") allIDs = append(allIDs, fileIDs...) if pn != "" { pkgname = pn } } if pkgname == "" { return errors.New("no package name found in any file") } err = os.WriteFile(destFile, []byte(fmtIDOutput(newChecksum, allIDs, 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 processIDFile(basedir string, fn string) ([]IDDef, 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") ids := make([]IDDef, 0) pkgname := "" for i, line := range lines { if i == 0 && strings.HasPrefix(line, "// Code generated by") { break } if match, ok := rexIDPackage.MatchFirst(line); i == 0 && ok { pkgname = match.GroupByName("name").Value() continue } if match, ok := rexIDDef.MatchFirst(line); ok { rfp, err := filepath.Rel(basedir, fn) if err != nil { return nil, "", err } def := IDDef{ File: fn, FileRelative: rfp, Name: match.GroupByName("name").Value(), } fmt.Printf("Found ID definition { '%s' }\n", def.Name) ids = append(ids, def) } } return ids, pkgname, nil } func fmtIDOutput(cs string, ids []IDDef, pkgname string) string { str := "// Code generated by id-generate.go DO NOT EDIT.\n" str += "\n" str += "package " + pkgname + "\n" str += "\n" str += "import \"go.mongodb.org/mongo-driver/bson\"" + "\n" str += "import \"go.mongodb.org/mongo-driver/bson/bsontype\"" + "\n" str += "import \"go.mongodb.org/mongo-driver/bson/primitive\"" + "\n" str += "import \"gogs.mikescher.com/BlackForestBytes/goext/exerr\"" + "\n" str += "\n" str += "const ChecksumIDGenerator = \"" + cs + "\" // GoExtVersion: " + goext.GoextVersion + "\n" str += "\n" anyDef := langext.ArrFirstOrNil(ids, func(def IDDef) bool { return def.Name == "AnyID" || def.Name == "AnyId" }) for _, iddef := range ids { str += "// ================================ " + iddef.Name + " (" + iddef.FileRelative + ") ================================" + "\n" str += "" + "\n" str += "func (i " + iddef.Name + ") MarshalBSONValue() (bsontype.Type, []byte, error) {" + "\n" str += " if objId, err := primitive.ObjectIDFromHex(string(i)); err == nil {" + "\n" str += " return bson.MarshalValue(objId)" + "\n" str += " } else {" + "\n" str += " return 0, nil, exerr.New(exerr.TypeMarshalEntityID, \"Failed to marshal " + iddef.Name + "(\"+i.String()+\") to ObjectId\").Str(\"value\", string(i)).Type(\"type\", i).Build()" + "\n" str += " }" + "\n" str += "}" + "\n" str += "" + "\n" str += "func (i " + iddef.Name + ") String() string {" + "\n" str += " return string(i)" + "\n" str += "}" + "\n" str += "" + "\n" str += "func (i " + iddef.Name + ") ObjID() (primitive.ObjectID, error) {" + "\n" str += " return primitive.ObjectIDFromHex(string(i))" + "\n" str += "}" + "\n" str += "" + "\n" str += "func (i " + iddef.Name + ") Valid() bool {" + "\n" str += " _, err := primitive.ObjectIDFromHex(string(i))" + "\n" str += " return err == nil" + "\n" str += "}" + "\n" str += "" + "\n" if anyDef != nil { str += "func (i " + iddef.Name + ") AsAny() " + anyDef.Name + " {" + "\n" str += " return " + anyDef.Name + "(i)" + "\n" str += "}" + "\n" str += "" + "\n" } str += "func New" + iddef.Name + "() " + iddef.Name + " {" + "\n" str += " return " + iddef.Name + "(primitive.NewObjectID().Hex())" + "\n" str += "}" + "\n" str += "" + "\n" } return str }