From d611a6aed58f87096e05dd823c6bfa18ba0c15a8 Mon Sep 17 00:00:00 2001 From: Vasiliy Tolstov Date: Tue, 19 Jan 2021 04:12:31 +0300 Subject: [PATCH] initial support for path param Signed-off-by: Vasiliy Tolstov --- go.mod | 2 +- go.sum | 4 +- http.go | 40 +++++++++++------ http_test.go | 30 +++++++++++++ options.go | 79 +++++++------------------------- stream.go | 16 ++----- util.go | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 202 insertions(+), 93 deletions(-) create mode 100644 http_test.go create mode 100644 util.go diff --git a/go.mod b/go.mod index c3cd3b7..9a77e03 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/unistack-org/micro-client-http/v3 go 1.14 -require github.com/unistack-org/micro/v3 v3.1.1 +require github.com/unistack-org/micro/v3 v3.1.2 diff --git a/go.sum b/go.sum index 1565fd7..8e57232 100644 --- a/go.sum +++ b/go.sum @@ -257,8 +257,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY= github.com/transip/gotransip v0.0.0-20190812104329-6d8d9179b66f/go.mod h1:i0f4R4o2HM0m3DZYQWsj6/MEowD57VzoH0v3d7igeFY= github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= -github.com/unistack-org/micro/v3 v3.1.1 h1:kWL0BVzUBdotjfDbl1qL9lNYmZqvebQWPNCyqrjUSAk= -github.com/unistack-org/micro/v3 v3.1.1/go.mod h1:0DgOy4OdJxQCDER8YSKitZugd2+1bddrRSNfeooTHDc= +github.com/unistack-org/micro/v3 v3.1.2 h1:NZnO6uhdRmoW/IhbWT1HWRCNWwgKbLlX4XikNx1cMzI= +github.com/unistack-org/micro/v3 v3.1.2/go.mod h1:0DgOy4OdJxQCDER8YSKitZugd2+1bddrRSNfeooTHDc= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= diff --git a/http.go b/http.go index 0877da3..a77a68a 100644 --- a/http.go +++ b/http.go @@ -32,8 +32,9 @@ type httpClient struct { httpcli *http.Client } -func newRequest(addr string, req client.Request, opts client.CallOptions) (*http.Request, error) { +func newRequest(addr string, req client.Request, cf codec.Codec, msg interface{}, opts client.CallOptions) (*http.Request, error) { hreq := &http.Request{Method: http.MethodPost} + body := "*" // as like google api http annotation u, err := url.Parse(addr) if err != nil { hreq.URL = &url.URL{ @@ -51,18 +52,36 @@ func newRequest(addr string, req client.Request, opts client.CallOptions) (*http if p, ok := opts.Context.Value(pathKey{}).(string); ok { ep = p } + if b, ok := opts.Context.Value(bodyKey{}).(string); ok { + body = b + } } - hreq.URL, err = u.Parse(ep) if err != nil { - return nil, err + return nil, errors.BadRequest("go.micro.client", err.Error()) } } + + path, nmsg, err := newPathRequest(hreq.URL.Path, hreq.Method, body, msg) + if err != nil { + return nil, errors.BadRequest("go.micro.client", err.Error()) + } + + hreq.URL.Path = path + // marshal request + b, err := cf.Marshal(nmsg) + if err != nil { + return nil, errors.BadRequest("go.micro.client", err.Error()) + } + + hreq.Body = ioutil.NopCloser(bytes.NewBuffer(b)) + hreq.ContentLength = int64(len(b)) + return hreq, nil } func (h *httpClient) call(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error { - header := make(http.Header) + header := make(http.Header, 2) if md, ok := metadata.FromContext(ctx); ok { for k, v := range md { header.Set(k, v) @@ -80,20 +99,13 @@ func (h *httpClient) call(ctx context.Context, addr string, req client.Request, return errors.InternalServerError("go.micro.client", err.Error()) } - // marshal request - b, err := cf.Marshal(req.Body()) + hreq, err := newRequest(addr, req, cf, req.Body(), opts) if err != nil { - return errors.InternalServerError("go.micro.client", err.Error()) - } - - hreq, err := newRequest(addr, req, opts) - if err != nil { - return errors.InternalServerError("go.micro.client", err.Error()) + return err } hreq.Header = header - hreq.Body = ioutil.NopCloser(bytes.NewBuffer(b)) - hreq.ContentLength = int64(len(b)) + // make the request hrsp, err := h.httpcli.Do(hreq.WithContext(ctx)) if err != nil { diff --git a/http_test.go b/http_test.go new file mode 100644 index 0000000..3c13127 --- /dev/null +++ b/http_test.go @@ -0,0 +1,30 @@ +package http + +import ( + "testing" +) + +type Request struct { + Name string `json:"name"` + Field1 string + Field2 string + Field3 int64 +} + +func TestValidPath(t *testing.T) { + req := &Request{Name: "vtolstov", Field1: "field1", Field2: "field2", Field3: 10} + p, m, err := newPathRequest("/api/v1/{name}/list", "GET", "", req) + if err != nil { + t.Fatal(err) + } + _, _ = p, m +} + +func TestInvalidPath(t *testing.T) { + req := &Request{Name: "vtolstov", Field1: "field1", Field2: "field2", Field3: 10} + p, m, err := newPathRequest("/api/v1/{xname}/list", "GET", "", req) + if err == nil { + t.Fatalf("path param must not be filled") + } + _, _ = p, m +} diff --git a/options.go b/options.go index 79dd9fe..df44ee1 100644 --- a/options.go +++ b/options.go @@ -1,7 +1,6 @@ package http import ( - "context" "crypto/tls" "net" "net/http" @@ -36,109 +35,63 @@ type maxSendMsgSizeKey struct{} // maximum streams on a connectioin func PoolMaxStreams(n int) client.Option { - return func(o *client.Options) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, poolMaxStreams{}, n) - } + return client.SetOption(poolMaxStreams{}, n) } // maximum idle conns of a pool func PoolMaxIdle(d int) client.Option { - return func(o *client.Options) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, poolMaxIdle{}, d) - } + return client.SetOption(poolMaxIdle{}, d) } // AuthTLS should be used to setup a secure authentication using TLS func AuthTLS(t *tls.Config) client.Option { - return func(o *client.Options) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, tlsAuth{}, t) - } + return client.SetOption(tlsAuth{}, t) } // // MaxRecvMsgSize set the maximum size of message that client can receive. -// func MaxRecvMsgSize(s int) client.Option { - return func(o *client.Options) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, maxRecvMsgSizeKey{}, s) - } + return client.SetOption(maxRecvMsgSizeKey{}, s) } // // MaxSendMsgSize set the maximum size of message that client can send. -// func MaxSendMsgSize(s int) client.Option { - return func(o *client.Options) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, maxSendMsgSizeKey{}, s) - } + return client.SetOption(maxSendMsgSizeKey{}, s) } type httpClientKey struct{} func HTTPClient(c *http.Client) client.Option { - return func(o *client.Options) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, httpClientKey{}, c) - } + return client.SetOption(httpClientKey{}, c) } type httpDialerKey struct{} func HTTPDialer(d *net.Dialer) client.Option { - return func(o *client.Options) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, httpDialerKey{}, d) - } + return client.SetOption(httpDialerKey{}, d) } type methodKey struct{} func Method(m string) client.CallOption { - return func(o *client.CallOptions) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, methodKey{}, m) - } + return client.SetCallOption(methodKey{}, m) } type pathKey struct{} func Path(p string) client.CallOption { - return func(o *client.CallOptions) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, pathKey{}, p) - } + return client.SetCallOption(pathKey{}, p) +} + +type bodyKey struct{} + +func Body(b string) client.CallOption { + return client.SetCallOption(bodyKey{}, b) } type errorMapKey struct{} func ErrorMap(m map[string]interface{}) client.CallOption { - return func(o *client.CallOptions) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, errorMapKey{}, m) - } + return client.SetCallOption(errorMapKey{}, m) } diff --git a/stream.go b/stream.go index 5f0d109..304ac99 100644 --- a/stream.go +++ b/stream.go @@ -2,10 +2,8 @@ package http import ( "bufio" - "bytes" "context" "fmt" - "io/ioutil" "net" "net/http" "sync" @@ -65,22 +63,14 @@ func (h *httpStream) Send(msg interface{}) error { return errShutdown } - b, err := h.codec.Marshal(msg) + hreq, err := newRequest(h.address, h.request, h.codec, msg, h.opts) if err != nil { return err } - buf := bytes.NewBuffer(b) - req, err := newRequest(h.address, h.request, h.opts) - if err != nil { - return err - } + hreq.Header = h.header - req.Header = h.header - req.Body = ioutil.NopCloser(buf) - req.ContentLength = int64(len(b)) - - return req.Write(h.conn) + return hreq.Write(h.conn) } func (h *httpStream) Recv(msg interface{}) error { diff --git a/util.go b/util.go new file mode 100644 index 0000000..8f2244e --- /dev/null +++ b/util.go @@ -0,0 +1,124 @@ +package http + +import ( + "fmt" + "net/http" + "reflect" + "strings" + "sync" + + rutil "github.com/unistack-org/micro/v3/util/reflect" + util "github.com/unistack-org/micro/v3/util/router" +) + +var ( + templateCache = make(map[string]util.Template) + mu sync.RWMutex +) + +func newPathRequest(path string, method string, body string, msg interface{}) (string, interface{}, error) { + // parse via https://github.com/googleapis/googleapis/blob/master/google/api/http.proto definition + tpl, err := newTemplate(path) + if err != nil { + return "", nil, err + } + + if len(tpl.Fields) == 0 { + return path, msg, nil + } + + fieldsmap := make(map[string]string, len(tpl.Fields)) + for _, v := range tpl.Fields { + fieldsmap[v] = "" + } + + nmsg, err := rutil.Zero(msg) + if err != nil { + return "", nil, err + } + + // we cant switch on message and use proto helpers, to avoid dependency to protobuf + tmsg := reflect.ValueOf(msg) + if tmsg.Kind() == reflect.Ptr { + tmsg = tmsg.Elem() + } + + tnmsg := reflect.ValueOf(nmsg) + if tnmsg.Kind() == reflect.Ptr { + tnmsg = tnmsg.Elem() + } + + values := make(map[string]string) + // copy cycle + for i := 0; i < tmsg.NumField(); i++ { + val := tmsg.Field(i) + if val.IsZero() { + continue + } + fld := tmsg.Type().Field(i) + lfield := strings.ToLower(fld.Name) + if _, ok := fieldsmap[lfield]; ok { + fieldsmap[lfield] = fmt.Sprintf("%v", val.Interface()) + } else if body == "*" || body == lfield && method != http.MethodGet { + tnmsg.Field(i).Set(val) + } else if method == http.MethodGet { + values[lfield] = fmt.Sprintf("%v", val.Interface()) + } + } + + // check not filled stuff + for k, v := range fieldsmap { + if v == "" { + return "", nil, fmt.Errorf("path param %s not filled %s", k, v) + } + } + + var b strings.Builder + for _, fld := range tpl.Pool { + _, _ = b.WriteRune('/') + if v, ok := fieldsmap[fld]; ok { + _, _ = b.WriteString(v) + } else { + _, _ = b.WriteString(fld) + } + } + + if method == http.MethodGet { + idx := 0 + for k, v := range values { + if idx == 0 { + _, _ = b.WriteRune('?') + } else { + _, _ = b.WriteRune('&') + } + _, _ = b.WriteString(k) + _, _ = b.WriteRune('=') + _, _ = b.WriteString(v) + idx++ + } + } + + return b.String(), nmsg, nil +} + +func newTemplate(path string) (util.Template, error) { + mu.RLock() + tpl, ok := templateCache[path] + if ok { + mu.RUnlock() + return tpl, nil + } + mu.RUnlock() + + rule, err := util.Parse(path) + if err != nil { + return tpl, err + } + + tpl = rule.Compile() + mu.Lock() + templateCache[path] = tpl + mu.Unlock() + + return tpl, nil +}