commit 71e7c2f72f4ec13531608aafdfb77991e7eba7d5 Author: Paulo Simão Date: Sat Oct 17 19:31:19 2020 -0300 1st ver diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e17d60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +/tmp diff --git a/main.go b/main.go new file mode 100644 index 0000000..4900999 --- /dev/null +++ b/main.go @@ -0,0 +1,624 @@ +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) +}