apigen/main.go

545 lines
12 KiB
Go

package main
import (
"bytes"
"dc"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"strings"
)
var api API
//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 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 := &APIParamType{}
resType := &APIParamType{}
if len(a.Type.Params.List) > 1 {
switch x := a.Type.Params.List[1].Type.(type) {
case *ast.StarExpr:
reqType.Ispointer = true
switch y := x.X.(type) {
case *ast.Ident:
reqType.Typename = y.Name
case *ast.SelectorExpr:
reqType.Typename = y.X.(*ast.Ident).Name + "." + y.Sel.Name
}
case *ast.Ident:
reqType.Typename = 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:
resType.Ispointer = true
switch y := x.X.(type) {
case *ast.Ident:
resType.Typename = y.Name
case *ast.SelectorExpr:
resType.Typename = y.X.(*ast.Ident).Name + "." + y.Sel.Name
}
case *ast.Ident:
resType.Typename = x.Name
}
}
if md["RAW"] == "true" {
reqType.Typename = md["REQ"]
resType.Typename = 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(api *API) {
APIParamTypeToString := func(t *APIParamType) string {
ret := ""
if t.Ispointer {
ret = ret + "&"
}
ret = ret + t.Typename
return ret
}
b := bytes.Buffer{}
f := config.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
}
}
`, APIParamTypeToString(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 = config.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}))$/
type HTMLMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "TRACE"
async function Invoke(path: string, method: HTMLMethod, body?: any): Promise<Response> {
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<any> {
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<string> {
//@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<boolean> {
//@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<any>{\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.Typename, m.ResType.Typename))
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(api)
processTSClientOutput("", 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, config.Goimpldir, 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() {
loadConfig()
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"
os.Remove(config.Gofname)
load()
process(&api)
}