diff --git a/http.go b/http.go index d5084f6..aac87f8 100644 --- a/http.go +++ b/http.go @@ -36,9 +36,11 @@ type httpClient struct { httpcli *http.Client } -func newRequest(addr string, req client.Request, cf codec.Codec, msg interface{}, opts client.CallOptions) (*http.Request, error) { +func newRequest(addr string, req client.Request, ct string, cf codec.Codec, msg interface{}, opts client.CallOptions) (*http.Request, error) { hreq := &http.Request{Method: http.MethodPost} body := "*" // as like google api http annotation + + var tags []string u, err := url.Parse(addr) if err != nil { hreq.URL = &url.URL{ @@ -59,6 +61,10 @@ func newRequest(addr string, req client.Request, cf codec.Codec, msg interface{} if b, ok := opts.Context.Value(bodyKey{}).(string); ok { body = b } + if t, ok := opts.Context.Value(structTagsKey{}).([]string); ok && len(t) > 0 { + tags = t + } + } hreq.URL, err = u.Parse(ep) if err != nil { @@ -66,7 +72,16 @@ func newRequest(addr string, req client.Request, cf codec.Codec, msg interface{} } } - path, nmsg, err := newPathRequest(hreq.URL.Path, hreq.Method, body, msg) + if len(tags) == 0 { + switch ct { + default: + tags = append(tags, "json", "protobuf") + case "text/xml": + tags = append(tags, "xml") + } + } + + path, nmsg, err := newPathRequest(hreq.URL.Path, hreq.Method, body, msg, tags) if err != nil { return nil, errors.BadRequest("go.micro.client", err.Error()) } @@ -100,18 +115,20 @@ func (h *httpClient) call(ctx context.Context, addr string, req client.Request, } } + ct := req.ContentType() + // set timeout in nanoseconds header.Set("Timeout", fmt.Sprintf("%d", opts.RequestTimeout)) // set the content type for the request - header.Set("Content-Type", req.ContentType()) + header.Set("Content-Type", ct) // get codec - cf, err := h.newCodec(req.ContentType()) + cf, err := h.newCodec(ct) if err != nil { return errors.InternalServerError("go.micro.client", err.Error()) } - hreq, err := newRequest(addr, req, cf, req.Body(), opts) + hreq, err := newRequest(addr, req, ct, cf, req.Body(), opts) if err != nil { return err } @@ -151,10 +168,11 @@ func (h *httpClient) stream(ctx context.Context, addr string, req client.Request header = make(http.Header, 2) } + ct := req.ContentType() // set timeout in nanoseconds header.Set("Timeout", fmt.Sprintf("%d", opts.RequestTimeout)) // set the content type for the request - header.Set("Content-Type", req.ContentType()) + header.Set("Content-Type", ct) // get codec cf, err := h.newCodec(req.ContentType()) @@ -178,7 +196,8 @@ func (h *httpClient) stream(ctx context.Context, addr string, req client.Request closed: make(chan bool), opts: opts, conn: cc, - codec: cf, + ct: ct, + cf: cf, header: header, reader: bufio.NewReader(cc), request: req, @@ -560,38 +579,3 @@ func NewClient(opts ...client.Option) client.Client { return c } - -func parseRsp(ctx context.Context, hrsp *http.Response, cf codec.Codec, rsp interface{}, opts client.CallOptions) error { - b, err := ioutil.ReadAll(hrsp.Body) - if err != nil { - return errors.InternalServerError("go.micro.client", err.Error()) - } - - if hrsp.StatusCode < 400 { - // unmarshal - if err := cf.Unmarshal(b, rsp); err != nil { - return errors.InternalServerError("go.micro.client", err.Error()) - } - return nil - } - - errmap, ok := opts.Context.Value(errorMapKey{}).(map[string]interface{}) - if !ok || errmap == nil { - // user not provide map of errors - // id: req.Service() ?? - return errors.New("go.micro.client", string(b), int32(hrsp.StatusCode)) - } - - if err, ok = errmap[fmt.Sprintf("%d", hrsp.StatusCode)].(error); !ok { - err, ok = errmap["default"].(error) - } - if !ok { - return errors.New("go.micro.client", string(b), int32(hrsp.StatusCode)) - } - - if cerr := cf.Unmarshal(b, err); cerr != nil { - return errors.InternalServerError("go.micro.client", cerr.Error()) - } - - return err -} diff --git a/http_test.go b/http_test.go index 1518ab5..3a9492b 100644 --- a/http_test.go +++ b/http_test.go @@ -15,7 +15,7 @@ type Request struct { 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) + p, m, err := newPathRequest("/api/v1/{name}/list", "GET", "", req, nil) if err != nil { t.Fatal(err) } @@ -32,7 +32,7 @@ func TestValidPath(t *testing.T) { 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) + p, m, err := newPathRequest("/api/v1/{xname}/list", "GET", "", req, nil) if err == nil { t.Fatalf("path param must not be filled") } diff --git a/options.go b/options.go index df44ee1..b80afc4 100644 --- a/options.go +++ b/options.go @@ -95,3 +95,9 @@ type errorMapKey struct{} func ErrorMap(m map[string]interface{}) client.CallOption { return client.SetCallOption(errorMapKey{}, m) } + +type structTagsKey struct{} + +func StructTags(tags []string) client.CallOption { + return client.SetCallOption(structTagsKey{}, tags) +} diff --git a/stream.go b/stream.go index 304ac99..83bcf47 100644 --- a/stream.go +++ b/stream.go @@ -18,7 +18,8 @@ type httpStream struct { sync.RWMutex address string opts client.CallOptions - codec codec.Codec + ct string + cf codec.Codec context context.Context header http.Header seq uint64 @@ -63,7 +64,7 @@ func (h *httpStream) Send(msg interface{}) error { return errShutdown } - hreq, err := newRequest(h.address, h.request, h.codec, msg, h.opts) + hreq, err := newRequest(h.address, h.request, h.ct, h.cf, msg, h.opts) if err != nil { return err } @@ -88,7 +89,7 @@ func (h *httpStream) Recv(msg interface{}) error { } defer hrsp.Body.Close() - return parseRsp(h.context, hrsp, h.codec, msg, h.opts) + return parseRsp(h.context, hrsp, h.cf, msg, h.opts) } func (h *httpStream) Error() error { diff --git a/util.go b/util.go index 9c6564a..3db85fe 100644 --- a/util.go +++ b/util.go @@ -1,12 +1,17 @@ package http import ( + "context" "fmt" + "io/ioutil" "net/http" "reflect" "strings" "sync" + "github.com/unistack-org/micro/v3/client" + "github.com/unistack-org/micro/v3/codec" + "github.com/unistack-org/micro/v3/errors" rutil "github.com/unistack-org/micro/v3/util/reflect" util "github.com/unistack-org/micro/v3/util/router" ) @@ -16,7 +21,7 @@ var ( mu sync.RWMutex ) -func newPathRequest(path string, method string, body string, msg interface{}) (string, interface{}, error) { +func newPathRequest(path string, method string, body string, msg interface{}, tags []string) (string, interface{}, error) { // parse via https://github.com/googleapis/googleapis/blob/master/google/api/http.proto definition tpl, err := newTemplate(path) if err != nil { @@ -52,13 +57,38 @@ func newPathRequest(path string, method string, body string, msg interface{}) (s 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 { + + t := &tag{} + for _, tn := range tags { + ts, ok := fld.Tag.Lookup(tn) + if !ok { + continue + } + + tp := strings.Split(ts, ",") + // special + switch tn { + case "protobuf": // special + t = &tag{key: tn, name: tp[3][5:], opts: append(tp[:3], tp[4:]...)} + default: + t = &tag{key: tn, name: tp[0], opts: tp[1:]} + } + if t.name != "" { + break + } + } + + if t.name == "" { + // fallback to lowercase + t.name = strings.ToLower(fld.Name) + } + + if _, ok := fieldsmap[t.name]; ok { + fieldsmap[t.name] = fmt.Sprintf("%v", val.Interface()) + } else if (body == "*" || body == t.name) && method != http.MethodGet { tnmsg.Field(i).Set(val) } else { - values[lfield] = fmt.Sprintf("%v", val.Interface()) + values[t.name] = fmt.Sprintf("%v", val.Interface()) } } @@ -120,3 +150,44 @@ func newTemplate(path string) (util.Template, error) { return tpl, nil } + +func parseRsp(ctx context.Context, hrsp *http.Response, cf codec.Codec, rsp interface{}, opts client.CallOptions) error { + b, err := ioutil.ReadAll(hrsp.Body) + if err != nil { + return errors.InternalServerError("go.micro.client", err.Error()) + } + + if hrsp.StatusCode < 400 { + // unmarshal + if err := cf.Unmarshal(b, rsp); err != nil { + return errors.InternalServerError("go.micro.client", err.Error()) + } + return nil + } + + errmap, ok := opts.Context.Value(errorMapKey{}).(map[string]interface{}) + if !ok || errmap == nil { + // user not provide map of errors + // id: req.Service() ?? + return errors.New("go.micro.client", string(b), int32(hrsp.StatusCode)) + } + + if err, ok = errmap[fmt.Sprintf("%d", hrsp.StatusCode)].(error); !ok { + err, ok = errmap["default"].(error) + } + if !ok { + return errors.New("go.micro.client", string(b), int32(hrsp.StatusCode)) + } + + if cerr := cf.Unmarshal(b, err); cerr != nil { + return errors.InternalServerError("go.micro.client", cerr.Error()) + } + + return err +} + +type tag struct { + key string + name string + opts []string +} diff --git a/util_test.go b/util_test.go index 3eaab4b..56e2d77 100644 --- a/util_test.go +++ b/util_test.go @@ -5,10 +5,19 @@ import ( "testing" ) +func TestTemplate(t *testing.T) { + tpl, err := newTemplate("/v1/{ClientID}/list") + if err != nil { + t.Fatal(err) + } + _ = tpl + // fmt.Printf("%#+v\n", tpl.Pool) +} + func TestNewPathRequest(t *testing.T) { type Message struct { - Name string - Val1 string + Name string `json:"name"` + Val1 string `protobuf:"bytes,1,opt,name=val1,proto3" json:"val1"` Val2 int64 Val3 []string } @@ -16,7 +25,8 @@ func TestNewPathRequest(t *testing.T) { omsg := &Message{Name: "test_name", Val1: "test_val1", Val2: 100, Val3: []string{"slice"}} for _, m := range []string{"POST", "PUT", "PATCH", "GET", "DELETE"} { - path, nmsg, err := newPathRequest("/v1/test", m, "", omsg) + body := "" + path, nmsg, err := newPathRequest("/v1/test", m, body, omsg, []string{"protobuf", "json"}) if err != nil { t.Fatal(err) } @@ -26,7 +36,46 @@ func TestNewPathRequest(t *testing.T) { } vals := u.Query() if v, ok := vals["name"]; !ok || v[0] != "test_name" { - t.Fatalf("invlid path: %v nmsg: %v", path, nmsg) + t.Fatalf("invalid path: %v nmsg: %v", path, nmsg) + } + } +} + +func TestNewPathVarRequest(t *testing.T) { + type Message struct { + Name string `json:"name"` + Val1 string `protobuf:"bytes,1,opt,name=val1,proto3" json:"val1"` + Val2 int64 + Val3 []string + } + + omsg := &Message{Name: "test_name", Val1: "test_val1", Val2: 100, Val3: []string{"slice"}} + + for _, m := range []string{"POST", "PUT", "PATCH", "GET", "DELETE"} { + body := "" + if m != "GET" { + body = "*" + } + path, nmsg, err := newPathRequest("/v1/test/{val1}", m, body, omsg, []string{"protobuf", "json"}) + if err != nil { + t.Fatal(err) + } + u, err := url.Parse(path) + if err != nil { + t.Fatal(err) + } + if m != "GET" { + if _, ok := nmsg.(*Message); !ok { + t.Fatalf("invalid nmsg: %#+v\n", nmsg) + } + if nmsg.(*Message).Name != "test_name" { + t.Fatalf("invalid nmsg: %v nmsg: %v", path, nmsg) + } + } else { + vals := u.Query() + if v, ok := vals["val2"]; !ok || v[0] != "100" { + t.Fatalf("invalid path: %v nmsg: %v", path, nmsg) + } } } }