package main import ( "bytes" "errors" "fmt" "go/ast" "go/parser" "go/token" "io" "os" "reflect" "strings" ) func main() { if err := run(); err != nil { panic(err) } } func slice(fileContents string, fset *token.FileSet, start token.Pos, end token.Pos) string { return fileContents[fset.Position(start).Offset:fset.Position(end).Offset] } func indent(s string) string { var output bytes.Buffer for _, line := range strings.SplitAfter(s, "\n") { output.WriteByte('\t') output.WriteString(line) } return output.String() } func run() error { filename := "examples/basic.go" fileBytes, err := os.ReadFile(filename) if err != nil { return err } fileContents := string(fileBytes) var fset token.FileSet f, err := parser.ParseFile(&fset, "examples/basic.go", fileBytes, parser.ParseComments|parser.SkipObjectResolution) if err != nil { return err } var output bytes.Buffer output.WriteString("package ") output.WriteString(f.Name.Name) output.WriteByte('\n') handlers := map[string]string{} for _, decl := range f.Decls { f, ok := decl.(*ast.FuncDecl) if !ok { continue } if f.Doc == nil { continue } hhRoute := f.Doc.List[len(f.Doc.List)-1].Text var routeSpec string if routeSpec, ok = strings.CutPrefix(hhRoute, "//hh:route "); !ok { continue } handlers[routeSpec] = "hh_" + f.Name.String() output.WriteString("\nfunc hh_") output.WriteString(f.Name.String()) output.WriteString("[S any](s S, w http.ResponseWriter, r *http.Request) {") parsedRequestType, ok := f.Type.Params.List[1].Type.(*ast.StructType) if !ok { return errors.New("Parsed request type must be a struct") } for _, field := range parsedRequestType.Fields.List { for _, nameIdent := range field.Names { typ := field.Type name := nameIdent.Name var tag string if field.Tag != nil { tag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("hh") } fmt.Println(typ, name, tag) if t1, ok := typ.(*ast.StarExpr); ok { if t2, ok := t1.X.(*ast.SelectorExpr); ok && t2.Sel.Name == "Request" { if id, ok := t2.X.(*ast.Ident); ok && id.Name == "http" { continue } } } if t1, ok := typ.(*ast.SelectorExpr); ok && t1.Sel.Name == "ResponseWriter" { if id, ok := t1.X.(*ast.Ident); ok && id.Name == "http" { continue } } if tag == "" { return errors.New("Don't know what to do with '" + name + "'. You must add a tag to specify") } tags := strings.Split(tag, ",") // TODO: handle raw request. Or maybe that should be a separate parameter optional := false if tags[0] == "optional" { optional = true tags = tags[1:] } switch tags[0] { case "form": output.WriteString("\n\t") output.WriteString(name) output.WriteString(" := r.FormValue(\"") output.WriteString(name) output.WriteString("\")") case "cookie": output.WriteString("\n\tvar ") output.WriteString(name) output.WriteString(" string\n\t") output.WriteString(name) output.WriteString("0, _ := r.Cookie(\"") output.WriteString(name) // TODO: optionally get cookie name from tags[1] output.WriteString("\")\n\tif ") output.WriteString(name) output.WriteString("0 != nil {\n\t\t") output.WriteString(name) output.WriteString(" = ") output.WriteString(name) output.WriteString("0.Value\n\t}") if !optional { output.WriteString(" else {\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\tw.Write([]byte(`Bad request. Missing cookie '") output.WriteString(name) output.WriteString("'`))\n\t}") } default: return errors.New("Unknown extractor " + tags[0]) } output.WriteString("\n") } } output.WriteString("\n\t") output.WriteString(f.Name.Name) output.WriteString("(w, ") structDef := slice(fileContents, &fset, parsedRequestType.Pos(), parsedRequestType.End()) output.WriteString(indent(structDef)[1:]) output.WriteString("{") for i, field := range parsedRequestType.Fields.List { for j, nameIdent := range field.Names { if i+j > 0 { output.WriteString(", ") } typ := field.Type name := nameIdent.Name _, _ = typ, name output.WriteString(name) output.WriteString(": ") output.WriteString(name) } } output.WriteString("})\n") output.WriteString("}\n") } io.Copy(os.Stdout, &output) fmt.Println(handlers) return nil }