// Patial Tech. // Author, Ankit Patial package main import ( "fmt" "go/format" "os" "path/filepath" "strings" "golang.org/x/text/cases" "golang.org/x/text/language" ) func generate(scheamPath, outDir string) error { // read schame.sql f, err := os.ReadFile(scheamPath) if err != nil { return err } // Parse scheam.sql tbls, err := parse(f) if err != nil { return err } // Output dir, create if not exists. if _, err := os.Stat(outDir); os.IsNotExist(err) { if err := os.MkdirAll(outDir, 0740); err != nil { return err } } // schema.go will hold all tables info var sb strings.Builder sb.WriteString("// Generated by code.patial.tech/go/pgm/cmd. DO NOT EDIT.\n\n") sb.WriteString(fmt.Sprintf("package %s \n", filepath.Base(outDir))) sb.WriteString(` import "code.patial.tech/go/pgm" var ( `) caser := cases.Title(language.English) var ( modalDir string fieldCount int ) for _, t := range tbls { // skip schema_migrations if t.Table == "schema_migrations" { continue } name := pluralToSingular(t.Table) name = strings.ReplaceAll(name, "_", " ") name = caser.String(name) name = strings.ReplaceAll(name, " ", "") fieldCount = len(t.Columns) sb.WriteString(fmt.Sprintf( " %s = pgm.Table{Name: %q, FieldCount: %d", name, t.Table, fieldCount, )) if len(t.PrimaryKey) > 0 { sb.WriteString(", PK: []string{}") sb.WriteString("}\n") } else { sb.WriteString("}\n") } modalDir = strings.ToLower(name) os.Mkdir(filepath.Join(outDir, modalDir), 0740) if err = writeColFile(t.Table, t.Columns, filepath.Join(outDir, modalDir), caser); err != nil { return err } } sb.WriteString(")") // Format code before saving code, err := formatGoCode(sb.String()) if err != nil { return err } // Save file to disk os.WriteFile(filepath.Join(outDir, "schema.go"), code, 0640) return nil } func writeColFile(tblName string, cols []*Column, outDir string, caser cases.Caser) error { var sb strings.Builder sb.WriteString("// Generated by code.patial.tech/go/pgm/cmd. DO NOT EDIT.\n\n") sb.WriteString(fmt.Sprintf("package %s\n\n", filepath.Base(outDir))) sb.WriteString(fmt.Sprintf("import %q\n\n", "code.patial.tech/go/pgm")) sb.WriteString("const (") var name string for _, c := range cols { name = strings.ReplaceAll(c.Name, "_", " ") name = caser.String(name) name = strings.ReplaceAll(name, " ", "") if strings.HasSuffix(name, "Id") { name = name[0:len(name)-2] + "ID" } sb.WriteString(fmt.Sprintf("\n // %s field has db type %q", name, c.Type)) sb.WriteString(fmt.Sprintf("\n %s pgm.Field = %q", name, tblName+"."+c.Name)) } sb.WriteString("\n)") // Format code before saving code, err := formatGoCode(sb.String()) if err != nil { return err } // Save file to disk. return os.WriteFile(filepath.Join(outDir, tblName+".go"), code, 0640) } // pluralToSingular converts plural table names to singular forms func pluralToSingular(plural string) string { // Handle special irregular plurals irregulars := map[string]string{ "people": "person", "children": "child", "men": "man", "women": "woman", "feet": "foot", "teeth": "tooth", "mice": "mouse", "geese": "goose", "oxen": "ox", "data": "datum", "criteria": "criterion", "phenomena": "phenomenon", } lower := strings.ToLower(plural) if singular, exists := irregulars[lower]; exists { return singular } // Handle words ending in -ies (cities -> city, companies -> company) if strings.HasSuffix(lower, "ies") && len(plural) > 3 { return plural[:len(plural)-3] + "y" } // Handle words ending in -ves (lives -> life, wives -> wife) if strings.HasSuffix(lower, "ves") && len(plural) > 3 { return plural[:len(plural)-3] + "fe" } // Handle words ending in -ses (classes -> class, addresses -> address) if strings.HasSuffix(lower, "ses") && len(plural) > 3 { return plural[:len(plural)-2] } // Handle words ending in -xes (boxes -> box, taxes -> tax) if strings.HasSuffix(lower, "xes") && len(plural) > 3 { return plural[:len(plural)-2] } // Handle words ending in -ches (watches -> watch, benches -> bench) if strings.HasSuffix(lower, "ches") && len(plural) > 4 { return plural[:len(plural)-2] } // Handle words ending in -shes (dishes -> dish, bushes -> bush) if strings.HasSuffix(lower, "shes") && len(plural) > 4 { return plural[:len(plural)-2] } // Handle words ending in -oes (heroes -> hero, potatoes -> potato) if strings.HasSuffix(lower, "oes") && len(plural) > 3 { return plural[:len(plural)-2] } // Handle general -es endings if strings.HasSuffix(lower, "es") && len(plural) > 2 { withoutEs := plural[:len(plural)-2] if len(withoutEs) > 0 { lastChar := strings.ToLower(string(withoutEs[len(withoutEs)-1])) // Only remove -es (not just -s) if the word ends in s, x, ch, sh, o (those need the -es) if lastChar == "s" || lastChar == "x" || lastChar == "o" || strings.HasSuffix(strings.ToLower(withoutEs), "ch") || strings.HasSuffix(strings.ToLower(withoutEs), "sh") { return withoutEs } } // For words like "references", "preferences" - just remove the -s return plural[:len(plural)-1] } // Handle simple -s endings (users -> user, books -> book) if strings.HasSuffix(lower, "s") && len(plural) > 1 { return plural[:len(plural)-1] } // If no rules match, return as-is (might already be singular) return plural } // formatGoCode formats the input Go code using go/format. func formatGoCode(str string) ([]byte, error) { formatted, err := format.Source([]byte(str)) if err != nil { return nil, fmt.Errorf("formatting code: %w", err) } return formatted, nil }