diff --git a/.apigen.yaml b/.apigen.yaml index 409008e..ccd157b 100644 --- a/.apigen.yaml +++ b/.apigen.yaml @@ -1,3 +1,4 @@ gofname: test/goapi/api.go goimpldir: test/goapi tsfname: test/tscli/api.ts +goclifname: test/gocli/apicli.go diff --git a/config.go b/config.go index a69f398..47c3b9e 100644 --- a/config.go +++ b/config.go @@ -7,9 +7,10 @@ import ( ) type Config struct { - Gofname string `yaml:"gofname"` - Goimpldir string `yaml:"goimpldir"` - Tsfname string `json:"tsfname"` + Gofname string `yaml:"gofname"` + Goimpldir string `yaml:"goimpldir"` + Tsfname string `json:"tsfname"` + Goclifname string `json:"goclifname"` } var config Config diff --git a/main.go b/main.go index 1d6c641..4a247a7 100644 --- a/main.go +++ b/main.go @@ -1,17 +1,12 @@ package main import ( - "bytes" - "dc" - "fmt" "go/ast" "go/parser" "go/token" - "io/ioutil" "log" "net/http" "os" - "os/exec" "strings" ) @@ -159,275 +154,6 @@ func addFunction(a *ast.FuncDecl) { 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 { - 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.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 { @@ -464,6 +190,7 @@ func process(api *API) { mapHttp(api) processGoServerOutput(api) processTSClientOutput("", api) + processGoClientOutput(api) } func load() { diff --git a/processGoClientOutput.go b/processGoClientOutput.go new file mode 100644 index 0000000..6d8e934 --- /dev/null +++ b/processGoClientOutput.go @@ -0,0 +1,105 @@ +package main + +import ( + "bytes" + "dc" + "fmt" + "io/ioutil" + "strings" +) + +func processGoClientOutput(api *API) { + b := bytes.Buffer{} + f := config.Goclifname + fparts := strings.Split(f, "/") + pkg := fparts[len(fparts)-2] + + b.WriteString(fmt.Sprintf(`package %s + +import ( + "bytes" + "encoding/json" + "net/http" + "time" +) + +var Basepath string = "" +var Host string = "" +var ExtraHeaders map[string]string = make(map[string]string) + +func invoke(m string, path string, bodyo interface{}) (*json.Decoder, error) { + + b := &bytes.Buffer{} + err := json.NewEncoder(b).Encode(bodyo) + if err != nil { + return nil, err + } + body := bytes.NewReader(b.Bytes()) + + req, err := http.NewRequest(m, Host+Basepath+path, body) + if err != nil { + return nil, err + } + + req.Header.Set("Content-type", "application/json") + for k, v := range ExtraHeaders { + req.Header.Set(k, v) + } + cli := http.Client{} + res, err := cli.Do(req) + if err != nil { + return nil, err + } + ret := json.NewDecoder(res.Body) + return ret, nil +} + +func Some() (*struct{}, error) { + dec, err := invoke("a", "/c/d", struct{}{}) + ret := &struct{}{} + dec.Decode(ret) + return ret, err + +} +`, pkg)) + + for k, v := range api.Types { + + b.WriteString(fmt.Sprintf("type %s struct {\n", k)) + + for kf, f := range v.Fields { + + ftype := f.Type + + if f.Array { + ftype = "[]" + ftype + } else if f.Map { + fm := f.Mapkey + fv := f.Mapval + ftype = "map[" + fm + "]" + fv + } + + b.WriteString(fmt.Sprintf("\t%s %s `json:\"%s\"` \n", strings.ToUpper(kf[:1])+strings.ToLower(kf[1:]), ftype, strings.ToLower(kf))) + } + + b.WriteString(fmt.Sprintf("}\n\n")) + } + + for k, m := range api.Methods { + + b.WriteString(fmt.Sprintf("func %s(req *%s) (res *%s, err error){\n", k, m.ReqType.Typename, m.ResType.Typename)) + b.WriteString(fmt.Sprintf(` dec, err := invoke("%s", "%s", req) + ret := &%s{} + err = dec.Decode(ret) + if err != nil{ + return nil,err + } + return ret, err +}`, m.Verb, m.Path, m.ResType.Typename)) + //} + + } + + err := ioutil.WriteFile(f, b.Bytes(), 0600) + dc.Err(err) +} diff --git a/processGoServerOutput.go b/processGoServerOutput.go new file mode 100644 index 0000000..6749c69 --- /dev/null +++ b/processGoServerOutput.go @@ -0,0 +1,132 @@ +package main + +import ( + "bytes" + "dc" + "fmt" + "io/ioutil" + "os" + "os/exec" + "strings" +) + +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)) +} diff --git a/processTSClientOutput.go b/processTSClientOutput.go new file mode 100644 index 0000000..f1a1c99 --- /dev/null +++ b/processTSClientOutput.go @@ -0,0 +1,158 @@ +package main + +import ( + "bytes" + "dc" + "fmt" + "io/ioutil" + "strings" +) + +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 { + 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.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) +} diff --git a/test/goapi/api.go b/test/goapi/api.go index 021d68c..4f2839b 100644 --- a/test/goapi/api.go +++ b/test/goapi/api.go @@ -24,7 +24,7 @@ func Init() API { Perms: make(map[string]string), } - mux.HandleFunc("", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/someapi", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "POST": h_SomeAPI(w, r) diff --git a/test/goapi/test.go b/test/goapi/test.go index 8d48c0d..ba16540 100644 --- a/test/goapi/test.go +++ b/test/goapi/test.go @@ -7,7 +7,7 @@ import ( /*@API*/ type ARequestStruct struct { - A *string + A *string `json:"a"` B int64 C time.Time D *string @@ -23,7 +23,7 @@ type AResponseStruct struct { /* @API -PATH: /someapi +@PATH: /someapi */ func SomeAPI(ctx context.Context, a *ARequestStruct) (*AResponseStruct, error) { return nil, nil diff --git a/test/gocli/apicli.go b/test/gocli/apicli.go new file mode 100644 index 0000000..ab536fe --- /dev/null +++ b/test/gocli/apicli.go @@ -0,0 +1,71 @@ +package gocli + +import ( + "bytes" + "encoding/json" + "net/http" + "time" +) + +var Basepath string = "" +var Host string = "" +var ExtraHeaders map[string]string = make(map[string]string) + +func invoke(m string, path string, bodyo interface{}) (*json.Decoder, error) { + + b := &bytes.Buffer{} + err := json.NewEncoder(b).Encode(bodyo) + if err != nil { + return nil, err + } + body := bytes.NewReader(b.Bytes()) + + req, err := http.NewRequest(m, Host+Basepath+path, body) + if err != nil { + return nil, err + } + + req.Header.Set("Content-type", "application/json") + for k, v := range ExtraHeaders { + req.Header.Set(k, v) + } + cli := http.Client{} + res, err := cli.Do(req) + if err != nil { + return nil, err + } + ret := json.NewDecoder(res.Body) + return ret, nil +} + +func Some() (*struct{}, error) { + dec, err := invoke("a", "/c/d", struct{}{}) + ret := &struct{}{} + dec.Decode(ret) + return ret, err + +} + +type ARequestStruct struct { + A string `json:"a"` + B int64 `json:"b"` + C time.Time `json:"c"` + D string `json:"d"` +} + +type AResponseStruct struct { + A string `json:"a"` + B int64 `json:"b"` + C time.Time `json:"c"` + D string `json:"d"` +} + +func SomeAPI(req *ARequestStruct) (res *AResponseStruct, err error) { + dec, err := invoke("POST", "/someapi", req) + ret := &AResponseStruct{} + err = dec.Decode(ret) + if err != nil { + return nil, err + } + return ret, err +} diff --git a/test/tscli/api.ts b/test/tscli/api.ts index e6ed067..ec85029 100644 --- a/test/tscli/api.ts +++ b/test/tscli/api.ts @@ -82,10 +82,10 @@ async function InvokeOk(path: string, method: HTMLMethod, body?: any): Promise{ - return InvokeJSON("","POST",req) + return InvokeJSON("/someapi","POST",req) } //#endregion