initial support for path param

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
Василий Толстов 2021-01-19 04:12:31 +03:00
parent 7a29127d17
commit d611a6aed5
7 changed files with 202 additions and 93 deletions

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

40
http.go
View File

@ -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 {

30
http_test.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {

124
util.go Normal file
View File

@ -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
}