diff --git a/examples/examples_test.go b/examples/base/base_test.go similarity index 99% rename from examples/examples_test.go rename to examples/base/base_test.go index a5e6fb7..22d4e5c 100644 --- a/examples/examples_test.go +++ b/examples/base/base_test.go @@ -1,4 +1,4 @@ -package examples +package base import ( "testing" diff --git a/examples/business.go b/examples/base/business.go similarity index 97% rename from examples/business.go rename to examples/base/business.go index 73f5a9e..18d755a 100644 --- a/examples/business.go +++ b/examples/base/business.go @@ -1,4 +1,4 @@ -package examples +package base import ( "time" diff --git a/examples/translator.go b/examples/base/translator.go similarity index 98% rename from examples/translator.go rename to examples/base/translator.go index ef7f24b..496ac17 100644 --- a/examples/translator.go +++ b/examples/base/translator.go @@ -1,4 +1,4 @@ -package examples +package base import ( "bytes" diff --git a/examples/user.go b/examples/base/user.go similarity index 97% rename from examples/user.go rename to examples/base/user.go index 3a954f5..e67f000 100644 --- a/examples/user.go +++ b/examples/base/user.go @@ -1,4 +1,4 @@ -package examples +package base import "github.com/9ssi7/rapidval" diff --git a/examples/structured/user.go b/examples/structured/user.go new file mode 100644 index 0000000..4a5ed31 --- /dev/null +++ b/examples/structured/user.go @@ -0,0 +1,10 @@ +package structured + +//go:generate go run ../../rapidval/main.go -type=User -input=$GOFILE +type User struct { + ID int64 `json:"id" validate:"min=1"` + Username string `json:"username" validate:"required,min=3,max=50"` + Email string `json:"email" validate:"required,email"` + Age int `json:"age" validate:"min=0,max=150"` + IsActive bool `json:"is_active"` +} diff --git a/examples/structured/user_validate.go b/examples/structured/user_validate.go new file mode 100644 index 0000000..60a0960 --- /dev/null +++ b/examples/structured/user_validate.go @@ -0,0 +1,81 @@ + +// Code generated by validator generator; DO NOT EDIT. +package structured + +import ( + "fmt" + "regexp" +) + +var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + +func (u *User) Validate() error { + + + // Validate ID + + + if u.ID < 1 { + return fmt.Errorf("ID must be at least 1") + } + + + + + + + // Validate Username + + if u.Username == "" { + return fmt.Errorf("Username is required") + } + + + if len(u.Username) < 3 { + return fmt.Errorf("Username must be at least 3") + } + + + if len(u.Username) > 50 { + return fmt.Errorf("Username must be at most 50") + } + + + + + + // Validate Email + + if u.Email == "" { + return fmt.Errorf("Email is required") + } + + + + + if !emailRegex.MatchString(u.Email) { + return fmt.Errorf("Email must be a valid email address") + } + + + + + // Validate Age + + + if u.Age < 0 { + return fmt.Errorf("Age must be at least 0") + } + + + if u.Age > 150 { + return fmt.Errorf("Age must be at most 150") + } + + + + + + + return nil +} diff --git a/gen/generator.go b/gen/generator.go new file mode 100644 index 0000000..ca6250b --- /dev/null +++ b/gen/generator.go @@ -0,0 +1,186 @@ +package gen + +import ( + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + "text/template" +) + +var ( + typeName = flag.String("type", "", "type name to generate validator for") + input = flag.String("input", "", "input file path") +) + +// Helper structs for template +type FieldInfo struct { + Name string + Type string + Rules map[string]string +} + +func parseValidateTag(tag string) map[string]string { + rules := make(map[string]string) + if tag == "" { + return rules + } + + tag = strings.Trim(tag, "`") + for _, tagPart := range strings.Split(tag, " ") { + if strings.HasPrefix(tagPart, `validate:"`) { + validations := strings.Trim(strings.TrimPrefix(tagPart, `validate:"`), `"`) + for _, validation := range strings.Split(validations, ",") { + if strings.Contains(validation, "=") { + parts := strings.Split(validation, "=") + rules[parts[0]] = parts[1] + } else { + rules[validation] = "" + } + } + } + } + return rules +} + +func Run() { + flag.Parse() + + if *typeName == "" { + fmt.Println("Please provide type name with -type flag") + os.Exit(1) + } + + if *input == "" { + fmt.Println("Please provide input file path with -input flag") + os.Exit(1) + } + + // Get working directory + wd, err := os.Getwd() + if err != nil { + fmt.Printf("Error getting working directory: %v\n", err) + os.Exit(1) + } + + // Construct absolute input path + absInputPath := filepath.Join(wd, *input) + + // Parse the input file + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, absInputPath, nil, parser.ParseComments) + if err != nil { + fmt.Printf("Error parsing file: %v\n", err) + os.Exit(1) + } + + // Get package name from input file + packageName := node.Name.Name + + // Get output directory (same as input file) + outDir := filepath.Dir(absInputPath) + + // Find the struct + var structType *ast.StructType + ast.Inspect(node, func(n ast.Node) bool { + if td, ok := n.(*ast.TypeSpec); ok { + if td.Name.Name == *typeName { + structType = td.Type.(*ast.StructType) + return false + } + } + return true + }) + + // Create output file in same directory as input + outPath := filepath.Join(outDir, fmt.Sprintf("%s_validate.go", strings.ToLower(*typeName))) + f, err := os.Create(outPath) + if err != nil { + fmt.Printf("Error creating output file: %v\n", err) + os.Exit(1) + } + defer f.Close() + + // Collect field information + var fields []FieldInfo + for _, field := range structType.Fields.List { + if field.Tag != nil { + fieldInfo := FieldInfo{ + Name: field.Names[0].Name, + Type: fmt.Sprint(field.Type), + Rules: parseValidateTag(field.Tag.Value), + } + fields = append(fields, fieldInfo) + } + } + + // Execute template with helper functions + tmplFuncs := template.FuncMap{ + "hasRule": func(rules map[string]string, rule string) bool { + _, exists := rules[rule] + return exists + }, + "getRule": func(rules map[string]string, rule string) string { + return rules[rule] + }, + } + + t := template.Must(template.New("validator").Funcs(tmplFuncs).Parse(validatorTemplate)) + err = t.Execute(f, struct { + TypeName string + PackageName string + Fields []FieldInfo + }{ + TypeName: *typeName, + PackageName: packageName, + Fields: fields, + }) + if err != nil { + panic(err) + } +} + +var validatorTemplate = ` +// Code generated by validator generator; DO NOT EDIT. +package {{.PackageName}} + +import ( + "fmt" + "regexp" +) + +var emailRegex = regexp.MustCompile(` + "`" + `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` + "`" + `) + +func (u *{{.TypeName}}) Validate() error { + {{range .Fields}} + {{if .Rules}} + // Validate {{.Name}} + {{if hasRule .Rules "required"}} + if {{if eq .Type "string"}}u.{{.Name}} == ""{{else}}u.{{.Name}} == 0{{end}} { + return fmt.Errorf("{{.Name}} is required") + } + {{end}} + {{if hasRule .Rules "min"}} + if {{if eq .Type "string"}}len(u.{{.Name}}) < {{getRule .Rules "min"}}{{else}}u.{{.Name}} < {{getRule .Rules "min"}}{{end}} { + return fmt.Errorf("{{.Name}} must be at least {{getRule .Rules "min"}}") + } + {{end}} + {{if hasRule .Rules "max"}} + if {{if eq .Type "string"}}len(u.{{.Name}}) > {{getRule .Rules "max"}}{{else}}u.{{.Name}} > {{getRule .Rules "max"}}{{end}} { + return fmt.Errorf("{{.Name}} must be at most {{getRule .Rules "max"}}") + } + {{end}} + {{if hasRule .Rules "email"}} + if !emailRegex.MatchString(u.{{.Name}}) { + return fmt.Errorf("{{.Name}} must be a valid email address") + } + {{end}} + {{end}} + {{end}} + return nil +} +` diff --git a/rapidval/main.go b/rapidval/main.go new file mode 100644 index 0000000..88bc52a --- /dev/null +++ b/rapidval/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/9ssi7/rapidval/gen" + +func main() { + gen.Run() +}