package main import ( "bytes" "dc" "flag" "fmt" "go/ast" "go/parser" "go/token" "io/ioutil" "log" "net/http" "os" "os/exec" "path" "regexp" "strings" ) type API struct { BasePath string `yaml:"basepath"` Host string `yaml:"host"` Types map[string]*APIType `yaml:"types"` Methods map[string]*APIMethod `yaml:"methods"` Namespace string } type APIField struct { Type string `yaml:"type"` Array bool `yaml:"array"` Desc string `yaml:"desc"` Map bool `yaml:"map"` Mapkey string `yaml:"mapkey"` Mapval string `yaml:"mapval"` } type APIType struct { Name string Desc string `yaml:"desc"` Fields map[string]*APIField `yaml:"fields"` Col string `yaml:"col"` } type APIMethod struct { Desc string `yaml:"desc"` Verb string `yaml:"verb"` Path string `yaml:"path"` Perm string `yaml:perm` ReqType string `yaml:"reqtype"` ResType string `yaml:"restype"` Raw bool `yaml:"raw"` } var api API var gofname string var goimplfdir string var tsfname string var httptestdir string var tstypemapper map[string]string = make(map[string]string) var knownMethods map[string]bool = make(map[string]bool) var httpMapper map[string]map[string]string = make(map[string]map[string]string) var packageName string = "main" func manageComments(a *ast.CommentGroup) map[string]string { ret := make(map[string]string) if a == nil { return ret } var myExp = regexp.MustCompile(`@(?P.*)\s*:\s*(?P.*)`) var myExp2 = regexp.MustCompile(`@(?P.*?)$`) for _, v := range a.List { lines := strings.Split(v.Text, "\n") for _, l := range lines { m := myExp.FindStringSubmatch(l) if m != nil { ret[m[1]] = m[2] } else { m := myExp2.FindStringSubmatch(l) if m != nil { ret[m[1]] = "OK" } } } } return ret } func manageCommentsGroups(as []*ast.CommentGroup) map[string]string { ret := make(map[string]string) if as == nil { return ret } for _, a := range as { var myExp = regexp.MustCompile(`@(?P.*)\s*:\s*(?P.*)`) var myExp2 = regexp.MustCompile(`@(?P.*?)$`) for _, v := range a.List { lines := strings.Split(v.Text, "\n") for _, l := range lines { m := myExp.FindStringSubmatch(l) if m != nil { ret[m[1]] = m[2] } else { m := myExp2.FindStringSubmatch(l) if m != nil { ret[m[1]] = "OK" } } } } } return ret } func addStruct(a *ast.GenDecl) { md := manageComments(a.Doc) if md["API"] == "" { return } tp := APIType{ Name: "", Desc: "", Fields: make(map[string]*APIField), Col: md["COL"], } tp.Name = a.Specs[0].(*ast.TypeSpec).Name.Name log.Printf("Type:" + tp.Name) for _, v := range a.Specs[0].(*ast.TypeSpec).Type.(*ast.StructType).Fields.List { tp.Fields[v.Names[0].Name] = &APIField{} switch x := v.Type.(type) { case *ast.Ident: tp.Fields[v.Names[0].Name].Type = x.Name case *ast.ArrayType: switch z := x.Elt.(type) { case *ast.Ident: tp.Fields[v.Names[0].Name].Type = z.Name tp.Fields[v.Names[0].Name].Array = true case *ast.InterfaceType: tp.Fields[v.Names[0].Name].Type = "interface{}" tp.Fields[v.Names[0].Name].Array = true case *ast.SelectorExpr: tp.Fields[v.Names[0].Name].Type = z.X.(*ast.Ident).Name + "." + z.Sel.Name tp.Fields[v.Names[0].Name].Array = true } case *ast.StarExpr: switch y := x.X.(type) { case *ast.Ident: tp.Fields[v.Names[0].Name].Type = y.Name case *ast.SelectorExpr: switch z := y.X.(type) { case *ast.Ident: tp.Fields[v.Names[0].Name].Type = z.Name + "." + y.Sel.Name } } case *ast.InterfaceType: tp.Fields[v.Names[0].Name].Type = "interface{}" case *ast.SelectorExpr: switch z := x.X.(type) { case *ast.Ident: tp.Fields[v.Names[0].Name].Type = z.Name + "." + x.Sel.Name } case *ast.MapType: switch z := x.Value.(type) { case *ast.Ident: tp.Fields[v.Names[0].Name].Type = "" tp.Fields[v.Names[0].Name].Mapkey = x.Key.(*ast.Ident).Name tp.Fields[v.Names[0].Name].Mapval = z.Name tp.Fields[v.Names[0].Name].Map = true case *ast.InterfaceType: tp.Fields[v.Names[0].Name].Type = "interface{}" tp.Fields[v.Names[0].Name].Array = true } default: log.Printf("%#v", x) } } api.Types[tp.Name] = &tp } func addFunction(a *ast.FuncDecl) { md := manageComments(a.Doc) if md["API"] == "" { return } reqType := "" resType := "" if len(a.Type.Params.List) > 1 { switch x := a.Type.Params.List[1].Type.(type) { case *ast.StarExpr: switch y := x.X.(type) { case *ast.Ident: reqType = y.Name case *ast.SelectorExpr: reqType = y.X.(*ast.Ident).Name + "." + y.Sel.Name } case *ast.Ident: reqType = x.Name } } if a.Type.Results != nil && len(a.Type.Results.List) > 0 { switch x := a.Type.Results.List[0].Type.(type) { case *ast.StarExpr: switch y := x.X.(type) { case *ast.Ident: resType = y.Name case *ast.SelectorExpr: resType = y.X.(*ast.Ident).Name + "." + y.Sel.Name } case *ast.Ident: resType = x.Name } } if md["RAW"] == "true" { reqType = md["REQ"] resType = md["RES"] } verb := md["VERB"] if verb == "" { verb = http.MethodPost } fn := APIMethod{ Desc: a.Name.Name, Verb: verb, Path: md["PATH"], Perm: md["PERM"], ReqType: reqType, ResType: resType, Raw: md["RAW"] == "true", } api.Methods[a.Name.Name] = &fn } func processGoServerOutput(f string, api *API) { b := bytes.Buffer{} if f == "" { f = gofname } os.Remove(f) b.WriteString(fmt.Sprintf(`package %s import ( "context" "encoding/json" "strings" "net/http" ) `, packageName)) b.WriteString(` type API struct { Mux *http.ServeMux Perms map[string]string } func (a *API) GetPerm(r *http.Request) string { return a.Perms[r.Method+"_"+strings.Split(r.RequestURI, "?")[0]] } func Init() API{ mux := &http.ServeMux{} ret := API{ Mux: mux, Perms: make(map[string]string), } `) for _, m := range api.Methods { if m.Perm != "" { b.WriteString(fmt.Sprintf(` ret.Perms["%s_%s"]="%s" `, m.Verb, m.Path, m.Perm)) } } b.WriteString("\n\n") for p, mv := range httpMapper { b.WriteString(fmt.Sprintf(" mux.HandleFunc(\"%s\",func(w http.ResponseWriter, r *http.Request) {\n", strings.Replace(p, "//", "/", -1))) b.WriteString(" switch r.Method{\n") for v, id := range mv { b.WriteString(fmt.Sprintf(" case \"%s\":", v)) if api.Methods[id].Raw { b.WriteString(fmt.Sprintf(" %s(w , r)\n", id)) } else { b.WriteString(fmt.Sprintf(" h_%s(w , r)\n", id)) } } b.WriteString(" default:") b.WriteString(" http.Error(w,\"Method not allowed\",500)") b.WriteString(" }\n") b.WriteString("})\n") } b.WriteString("return ret\n }\n") for k, m := range api.Methods { if !m.Raw { b.WriteString(fmt.Sprintf("\n func h_%s(w http.ResponseWriter, r *http.Request) {\n", k)) b.WriteString(fmt.Sprintf( ` ctx := r.Context() ctx = context.WithValue(r.Context(), "REQ", r) ctx = context.WithValue(ctx, "RES", w) req := %s{} if r.Method!=http.MethodGet && r.Method!=http.MethodHead { err := json.NewDecoder(r.Body).Decode(&req) if err != nil { http.Error(w, err.Error(), 500) return } } res, err := %s(ctx,req) if err != nil { http.Error(w, err.Error(), 500) return } w.Header().Add("Content-Type","Application/json") err=json.NewEncoder(w).Encode(res) if err != nil { http.Error(w, err.Error(), 500) return } } `, m.ReqType, k)) } } err := ioutil.WriteFile(f, b.Bytes(), 0600) dc.Err(err) cmd := exec.Command("/bin/sh", "-c", "go fmt "+f) bs, err := cmd.Output() //dc.Err(err) dc.Log(string(bs)) } func processTSClientOutput(f string, api *API) { b := bytes.Buffer{} if f == "" { f = tsfname } b.WriteString("//#region Base\n") b.WriteString(fmt.Sprintf(` var apibase="%s"; export function SetAPIBase(s:string){ apibase=s; } export function GetAPIBase(): string{ return apibase; } let REGEX_DATE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)(Z|([+\-])(\d{2}):(\d{2}))$/ async function Invoke(path: string, method: HTMLMethod, body?: any): Promise { let jbody = undefined let init = {method: method, mode: "cors", credentials: "include", withCredentials: true} if (!!body) { let jbody = JSON.stringify(body) //@ts-ignore init.body = jbody } if (apibase.endsWith("/") && path.startsWith("/")) { path = path.substr(1, path.length) } let fpath = (apibase + path) //@ts-ignore let res = await fetch(fpath, init) return res } async function InvokeJSON(path: string, method: HTMLMethod, body?: any): Promise { let txt = await InvokeTxt(path, method, body) if (txt == "") { txt = "{}" } let ret = JSON.parse(txt, (k: string, v: string) => { if (REGEX_DATE.exec(v)) { return new Date(v) } return v }) return ret } async function InvokeTxt(path: string, method: HTMLMethod, body?: any): Promise { //@ts-ignore let res = await Invoke(path, method, body) let txt = await res.text() if (res.status < 200 || res.status >= 400) { // webix.alert("API Error:" + res.status + "\n" + txt) console.error("API Error:" + res.status + "\n" + txt) let e = new Error(txt) throw e } return txt } async function InvokeOk(path: string, method: HTMLMethod, body?: any): Promise { //@ts-ignore let res = await Invoke(path, method, body) let txt = await res.text() if (res.status >= 400) { console.error("API Error:" + res.status + "\n" + txt) return false } return true } `, api.BasePath)) b.WriteString("//#endregion\n\n") b.WriteString("//#region Types\n") for k, v := range api.Types { if v.Desc != "" { b.WriteString(fmt.Sprintf("/**\n%s*/\n", v.Desc)) } b.WriteString(fmt.Sprintf("export interface %s {\n", k)) for kf, f := range v.Fields { ftype, ok := tstypemapper[f.Type] if !ok { ftype = f.Type } if f.Array { ftype = ftype + "[]" } else if f.Map { fm, ok := tstypemapper[f.Mapkey] if !ok { fm = f.Mapkey } fv, ok := tstypemapper[f.Mapval] if !ok { fv = f.Mapval } ftype = "{[s:" + fm + "]:" + fv + "}" } if f.Desc != "" { b.WriteString(fmt.Sprintf("\t/**\n%s*/\n", f.Desc)) } b.WriteString(fmt.Sprintf("\t%s:%s\n", strings.ToLower(kf), ftype)) } b.WriteString(fmt.Sprintf("}\n\n")) } b.WriteString("//#endregion\n\n") b.WriteString("//#region Methods\n") for k, m := range api.Methods { if m.Desc != "" { b.WriteString(fmt.Sprintf("/**\n%s*/\n", m.Desc)) } //if m.Raw { // { // b.WriteString(fmt.Sprintf("export async function API_%s(req:any):Promise{\n", k, m.ReqType)) // b.WriteString(fmt.Sprintf("\treturn InvokeJSON(\"%s\",\"%s\",req)\n", m.Path, m.Verb)) // b.WriteString(fmt.Sprintf("}\n\n")) // } // //} else { b.WriteString(fmt.Sprintf("export async function %s(req:%s):Promise<%s>{\n", k, m.ReqType, m.ResType)) b.WriteString(fmt.Sprintf("\treturn InvokeJSON(\"%s\",\"%s\",req)\n", m.Path, m.Verb)) b.WriteString(fmt.Sprintf("}\n\n")) //} } b.WriteString("//#endregion\n") err := ioutil.WriteFile(f, b.Bytes(), 0600) dc.Err(err) } func processHTTPTestOutput(f string, api *API) { for k, m := range api.Methods { fname := path.Join(httptestdir, k+".http") if dc.FileExists(fname) { continue } b := bytes.Buffer{} b.WriteString(fmt.Sprintf(`%s{{BASEURL}}%s Content-Type: application/json Cookie: dc={{COOKIE}} {} ### `, m.Verb, api.BasePath+m.Path)) err := ioutil.WriteFile(fname, b.Bytes(), 0600) dc.Err(err) } } func mapHttp(api *API) { for k, v := range api.Methods { pathmap, ok := httpMapper[v.Path] if !ok { httpMapper[v.Path] = make(map[string]string) pathmap = httpMapper[v.Path] } pathmap[v.Verb] = k } } func process(api *API) { mapHttp(api) processGoServerOutput(gofname, api) //processGoMongoOutput(strings.Replace(gofname, ".go", "_mongo.go", 1), api) processTSClientOutput("", api) processHTTPTestOutput("", api) } func load() { api.Types = (make(map[string]*APIType)) api.Methods = (make(map[string]*APIMethod)) fset := token.NewFileSet() // positions are relative to fset f, err := parser.ParseDir(fset, goimplfdir, nil, parser.ParseComments) if err != nil { panic(err) } for _, v := range f { // Print the AST. ast.Inspect(v, func(n ast.Node) bool { switch x := n.(type) { case *ast.GenDecl: if x.Tok == token.TYPE { addStruct(x) } else { return true } case *ast.File: c := manageCommentsGroups(x.Comments) log.Printf("%+v", c) case *ast.FuncDecl: addFunction(x) case *ast.ValueSpec: if x.Names[0].Name == "BASEPATH" { api.BasePath = strings.Replace(x.Values[0].(*ast.BasicLit).Value, "\"", "", -1) } if x.Names[0].Name == "NAMESPACE" { api.Namespace = strings.Replace(x.Values[0].(*ast.BasicLit).Value, "\"", "", -1) } log.Printf("%#v", x) case *ast.Package: packageName = x.Name default: //log.Printf("%#v", x) return true } return true }) } } func main() { tstypemapper["time.Time"] = "Date" tstypemapper["primitive.ObjectID"] = "string" tstypemapper["time.Duration"] = "Date" tstypemapper["int"] = "number" tstypemapper["int32"] = "number" tstypemapper["int64"] = "number" tstypemapper["float"] = "number" tstypemapper["float64"] = "number" tstypemapper["uint8"] = "number" tstypemapper["uint16"] = "number" tstypemapper["uint32"] = "number" tstypemapper["error"] = "Error" tstypemapper["bool"] = "boolean" tstypemapper["interface{}"] = "any" tstypemapper["bson.M"] = "any" flag.StringVar(&gofname, "g", "api.go", "Go API Handle File") flag.StringVar(&goimplfdir, "I", "src", "Go API Impl Dir") flag.StringVar(&tsfname, "t", "api.ts", "TS File") flag.StringVar(&httptestdir, "h", "test.http", "HTTP Test Dir") flag.Parse() os.Remove(gofname) load() process(&api) }