package main import ( _ "embed" "errors" "go/ast" "go/parser" "go/token" "os" "reflect" "strings" "text/template" ) //go:embed templates.go.tmpl var fileTemplateString string 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] } type File struct { PackageName string Functions []Function } type Function struct { Name string Pattern string RequestTypeDef string RequestTypeFields []RequestTypeField DoParseForm bool } type RequestTypeField struct { Name string Extractor string Optional bool NameInReq string TypeDef string } func run() error { fileTemplate, err := template.New("").Funcs(template.FuncMap{ "quote": func(s string) string { return `"` + strings.NewReplacer(`\`, `\\`, `"`, `\"`, "\n", `\n`).Replace(s) + `"` }, "error": func() struct{} { panic("error") }, }).Parse(fileTemplateString) if err != nil { return err } 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 } parsedFile := File{PackageName: f.Name.Name} 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 pattern string if pattern, ok = strings.CutPrefix(hhRoute, "//hh:route "); !ok { continue } parsedRequestType, ok := f.Type.Params.List[1].Type.(*ast.StructType) if !ok { return errors.New("Parsed request type must be a struct") } parsedFunction := Function{ Name: f.Name.Name, Pattern: pattern, RequestTypeDef: slice(fileContents, &fset, parsedRequestType.Pos(), parsedRequestType.End()), } for _, field := range parsedRequestType.Fields.List { for _, nameIdent := range field.Names { typ := field.Type parsedField := RequestTypeField{ Name: nameIdent.Name, Extractor: "", Optional: false, TypeDef: slice(fileContents, &fset, typ.Pos(), typ.End()), } if parsedField.TypeDef == "*http.Request" { parsedFunction.RequestTypeFields = append(parsedFunction.RequestTypeFields, parsedField) continue } var tag string if field.Tag != nil { tag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("hh") } if tag == "" { return errors.New("Don't know what to do with '" + parsedField.Name + "'. You must add a `hh:\"...\"` tag to specify") } tags := strings.Split(tag, ",") if tags[0] == "optional" { parsedField.Optional = true tags = tags[1:] } if len(tags) == 0 { return errors.New("Must specify extractor for '" + parsedField.Name + "' in `" + tag + "`") } parsedField.Extractor = tags[0] tags = tags[1:] switch parsedField.Extractor { case "form": parsedFunction.DoParseForm = true case "cookie": default: return errors.New("Unknown extractor '" + tags[0] + "' on field " + nameIdent.Name) } if len(tags) >= 1 { parsedField.NameInReq = tags[0] tags = tags[1:] } else { parsedField.NameInReq = parsedField.Name } if len(tags) > 0 { return errors.New("Unexpected rest of tag '" + tags[0] + "' in tag `" + tag + "` on field " + nameIdent.Name) } parsedFunction.RequestTypeFields = append(parsedFunction.RequestTypeFields, parsedField) } } parsedFile.Functions = append(parsedFile.Functions, parsedFunction) } fileTemplate.Execute(os.Stdout, parsedFile) return nil }