1st ver
commit
71e7c2f72f
|
@ -0,0 +1,2 @@
|
|||
.idea
|
||||
/tmp
|
|
@ -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<k>.*)\s*:\s*(?P<v>.*)`)
|
||||
var myExp2 = regexp.MustCompile(`@(?P<k>.*?)$`)
|
||||
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<k>.*)\s*:\s*(?P<v>.*)`)
|
||||
var myExp2 = regexp.MustCompile(`@(?P<k>.*?)$`)
|
||||
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<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, 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)
|
||||
}
|
Loading…
Reference in New Issue