package main import ( "bytes" "errors" "fmt" "go/ast" "go/parser" "go/scanner" "go/token" "net/http" "strconv" "strings" ) func main() { if err := run(); err != nil { panic(err) } } func run() error { var fset token.FileSet f, err := parser.ParseFile(&fset, "examples/basic.go", nil, parser.ParseComments|parser.SkipObjectResolution) if err != nil { return err } for _, decl := range f.Decls { f, ok := decl.(*ast.FuncDecl) if !ok { continue } hhRoute := f.Doc.List[len(f.Doc.List)-1].Text var routeSpec string if routeSpec, ok = strings.CutPrefix(hhRoute, "//hh:route "); !ok { continue } rs, err := parseRouteSpec(routeSpec) if err != nil { return err } var wrapper bytes.Buffer wrapper.WriteString("func httpWrapper_") wrapper.WriteString(f.Name.String()) wrapper.WriteString("[S any](s S, r *http.Request, w http.ResponseWriter) {") i := 0 for _, p := range f.Type.Params.List { for _, n := range p.Names { wrapper.WriteString("\n\tvar") wrapper.WriteString(strconv.Itoa(i)) wrapper.WriteString(" := ") wrapper.WriteString(n.Name) i++ } } wrapper.WriteString("\t") wrapper.WriteString(f.Name.Name) wrapper.WriteString("(") i = 0 for _, p := range f.Type.Params.List { for range p.Names { if i > 0 { wrapper.WriteString(", ") } wrapper.WriteString("var") wrapper.WriteString(strconv.Itoa(i)) i++ } } wrapper.WriteString(")\n") wrapper.WriteString("}\n") fmt.Printf("`%v`\n`%v`\n`%v`\n", routeSpec, f.Name.Name, rs) } return nil } type routeSpec struct { method string path string parameters []routeSpecParam } type routeSpecParam struct { name string extractor string } func parseRouteSpec(s string) (routeSpec, error) { s = strings.TrimRight(s, " \n") var rs routeSpec for _, method := range []string{ http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodConnect, http.MethodOptions, http.MethodTrace, } { if rest, ok := strings.CutPrefix(s, method+" "); ok { s = rest rs.method = method break } } for { if commaPos := strings.IndexByte(s, ','); commaPos != -1 { rs.path = s[:commaPos] s = s[commaPos+1:] } else { rs.path = s return rs, nil } s = strings.TrimLeft(s, " ") if s == "" { break } end := -1 for i := 0; i < len(s); i++ { if 'a' <= s[i] && s[i] <= 'z' || 'A' <= s[i] && s[i] <= 'Z' || s[i] == '_' { continue } if s[i] == ':' { end = i break } return rs, errors.New("Expected ':' to mark end of parameter name, got " + s[i:i+1]) } if end == -1 { return rs, errors.New("Expected ':' to mark end of parameter name, got end of line") } var p routeSpecParam p.name = s[:end] s = s[end+1:] s = strings.TrimLeft(s, " ") expr, err := parser.ParseExpr(s) if el, ok := err.(scanner.ErrorList); err == nil || ok && el[0].Msg == "expected 'EOF', found ','" { switch expr := expr.(type) { case *ast.Ident: switch expr.Name { case "query": p.extractor = ":" + expr.Name default: return rs, errors.New("Unexpected extractor " + expr.Name) } case *ast.CallExpr: p.extractor = s[expr.Pos()-1 : expr.End()-1] default: return rs, errors.New("Unexpected extractor" + s[expr.Pos()-1:expr.End()-1]) } } else { return rs, err } s = s[expr.End()-1:] rs.parameters = append(rs.parameters, p) } return rs, nil }