Skip to content

Commit 2480175

Browse files
authored
integrate request builder into HTTP client for googleapis support (#157)
1 parent b37fca9 commit 2480175

32 files changed

+9486
-1731
lines changed

README.md

Lines changed: 119 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,154 @@
11
# HTTP Client
22
![Coverage](https://img.shields.io/badge/Coverage-22.5%25-red)
33

4-
This plugin is a http client for micro.
4+
This plugin is an HTTP client for [Micro](https://pkg.go.dev/go.unistack.org/micro/v4).
5+
It implements the [micro.Client](https://pkg.go.dev/go.unistack.org/micro/v4/client#Client) interface.
56

67
## Overview
78

8-
The http client wraps `net/http` to provide a robust micro client with service discovery, load balancing and streaming.
9-
It complies with the [micro.Client](https://godoc.org/go.unistack.org/micro-client-http/v3#Client) interface.
9+
The HTTP client wraps `net/http` to provide a robust client with service discovery, load balancing and
10+
implements HTTP rules defined in the [google/api/http.proto](https://github.com/googleapis/googleapis/blob/master/google/api/http.proto) specification.
1011

11-
## Usage
12+
## Limitations
13+
14+
* Streaming is not yet implemented.
15+
* Only protobuf-generated messages are supported.
1216

13-
### Use directly
17+
## Usage
1418

1519
```go
16-
import "go.unistack.org/micro-client-http/v3"
20+
import (
21+
"go.unistack.org/micro/v4"
22+
http "go.unistack.org/micro-client-http/v4"
23+
)
1724

1825
service := micro.NewService(
1926
micro.Name("my.service"),
2027
micro.Client(http.NewClient()),
2128
)
2229
```
2330

24-
### Call Service
31+
### Simple call
2532

26-
Assuming you have a http service "my.service" with path "/foo/bar"
2733
```go
28-
// new client
29-
client := http.NewClient()
34+
import (
35+
"go.unistack.org/micro/v4/client"
36+
http "go.unistack.org/micro-client-http/v4"
37+
jsoncodec "go.unistack.org/micro-codec-json/v4"
38+
)
3039

31-
// create request/response
32-
request := client.NewRequest("my.service", "/foo/bar", protoRequest{})
33-
response := new(protoResponse)
40+
c := http.NewClient(
41+
client.Codec("application/json", jsoncodec.NewCodec()),
42+
)
43+
44+
req := c.NewRequest(
45+
"user-service",
46+
"/user/{user_id}/order/{order_id}",
47+
&protoReq{UserId: "123", OrderId: 456},
48+
)
49+
rsp := new(protoRsp)
3450

35-
// call service
36-
err := client.Call(context.TODO(), request, response)
51+
err := c.Call(
52+
ctx,
53+
req,
54+
rsp,
55+
client.WithAddress("example.com"),
56+
)
3757
```
3858

39-
or you can call any rest api or site and unmarshal to response struct
40-
```go
41-
// new client
42-
client := client.NewClientCallOptions(http.NewClient(), http.Address("https://api.github.com"))
59+
### Call with specific options
4360

44-
req := client.NewRequest("github", "/users/vtolstov", nil)
45-
rsp := make(map[string]interface{})
61+
```go
62+
import (
63+
"go.unistack.org/micro/v4/client"
64+
http "go.unistack.org/micro-client-http/v4"
65+
)
4666

47-
err := c.Call(context.TODO(), req, &rsp, mhttp.Method(http.MethodGet))
67+
err := c.Call(
68+
ctx,
69+
req,
70+
rsp,
71+
client.WithAddress("example.com"),
72+
http.Method("POST"),
73+
http.Path("/user/{user_id}/order/{order_id}"),
74+
http.Body("*"), // <- use all fields from the proto request as HTTP request body or specify a single field name to use only that field (see Google API HTTP spec: google/api/http.proto)
75+
)
4876
```
4977

50-
Look at http_test.go for detailed use.
78+
### Call with request headers
5179

52-
### Encoding
80+
```go
81+
import (
82+
"go.unistack.org/micro/v4/metadata"
83+
http "go.unistack.org/micro-client-http/v4"
84+
)
85+
86+
ctx := metadata.NewOutgoingContext(ctx, metadata.Pairs(
87+
"Authorization", "Bearer token",
88+
"My-Header", "My-Header-Value",
89+
))
90+
91+
err := c.Call(
92+
ctx,
93+
req,
94+
rsp,
95+
http.Header("Authorization", "true", "My-Header", "false"), // <- call option that declares required/optional headers
96+
)
97+
```
98+
99+
### Call with response headers
53100

54-
Default protobuf with content-type application/proto
55101
```go
56-
client.NewRequest("service", "/path", protoRequest{})
102+
import (
103+
"go.unistack.org/micro/v4/metadata"
104+
http "go.unistack.org/micro-client-http/v4"
105+
)
106+
107+
respMetadata := metadata.Metadata{}
108+
109+
err := c.Call(
110+
ctx,
111+
req,
112+
rsp,
113+
client.WithResponseMetadata(&respMetadata), // <- metadata with response headers
114+
)
57115
```
58116

59-
Json with content-type application/json
117+
### Call with cookies
118+
60119
```go
61-
client.NewJsonRequest("service", "/path", jsonRequest{})
120+
import (
121+
"go.unistack.org/micro/v4/metadata"
122+
http "go.unistack.org/micro-client-http/v4"
123+
)
124+
125+
ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs(
126+
"Cookie", "session_id=abc123; theme=dark",
127+
))
128+
129+
err := c.Call(
130+
ctx,
131+
req,
132+
rsp,
133+
http.Cookie("session_id", "true", "theme", "false"), // <- call option that declares required/optional cookies
134+
)
62135
```
63136

137+
### Call with error mapping
138+
139+
```go
140+
import (
141+
http "go.unistack.org/micro-client-http/v4"
142+
jsoncodec "go.unistack.org/micro-codec-json/v4"
143+
)
144+
145+
err := c.Call(
146+
ctx,
147+
req,
148+
rsp,
149+
http.ErrorMap(map[string]any{
150+
"default": &protoDefaultError{}, // <- default case
151+
"403": &protoSpecialError{}, // <- key is the HTTP status code that is mapped to this error
152+
}),
153+
)
154+
```

builder/body.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package builder
2+
3+
import (
4+
"fmt"
5+
6+
"google.golang.org/protobuf/proto"
7+
"google.golang.org/protobuf/reflect/protoreflect"
8+
)
9+
10+
func buildSingleFieldBody(msg proto.Message, fieldName string) (proto.Message, error) {
11+
msgReflect := msg.ProtoReflect()
12+
13+
fd, found := findFieldByName(msgReflect, fieldName)
14+
if !found || fd == nil {
15+
return nil, fmt.Errorf("field %s not found", fieldName)
16+
}
17+
if !msgReflect.Has(fd) {
18+
return nil, fmt.Errorf("field %s is not set", fieldName)
19+
}
20+
21+
val := msgReflect.Get(fd)
22+
23+
if fd.Kind() == protoreflect.MessageKind {
24+
return val.Message().Interface(), nil
25+
}
26+
27+
newMsg := proto.Clone(msg)
28+
newMsgReflect := newMsg.ProtoReflect()
29+
newMsgReflect.Range(func(f protoreflect.FieldDescriptor, _ protoreflect.Value) bool {
30+
if f != fd {
31+
newMsgReflect.Clear(f)
32+
}
33+
return true
34+
})
35+
36+
return newMsg, nil
37+
}
38+
39+
func buildFullBody(msg proto.Message, usedFieldsPath *usedFields) (proto.Message, error) {
40+
var (
41+
msgReflect = msg.ProtoReflect()
42+
newMsg = msgReflect.New().Interface()
43+
newMsgReflect = newMsg.ProtoReflect()
44+
)
45+
46+
fields := msgReflect.Descriptor().Fields()
47+
for i := 0; i < fields.Len(); i++ {
48+
fd := fields.Get(i)
49+
fieldName := fd.JSONName()
50+
51+
if usedFieldsPath.hasTopLevelKey(fieldName) {
52+
continue
53+
}
54+
55+
val := msgReflect.Get(fd)
56+
if !val.IsValid() {
57+
continue
58+
}
59+
60+
// Note: order of the cases is important!
61+
switch {
62+
case fd.IsList():
63+
list := val.List()
64+
newList := newMsgReflect.Mutable(fd).List()
65+
66+
if fd.Kind() == protoreflect.MessageKind {
67+
for j := 0; j < list.Len(); j++ {
68+
elem, err := buildFullBody(list.Get(j).Message().Interface(), usedFieldsPath)
69+
if err != nil {
70+
return nil, fmt.Errorf("recursive build full body: %w", err)
71+
}
72+
newList.Append(protoreflect.ValueOfMessage(elem.ProtoReflect()))
73+
}
74+
} else {
75+
for j := 0; j < list.Len(); j++ {
76+
newList.Append(list.Get(j))
77+
}
78+
}
79+
80+
case fd.IsMap():
81+
var (
82+
m = val.Map()
83+
newMap = newMsgReflect.Mutable(fd).Map()
84+
rangeErr error
85+
)
86+
m.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
87+
if fd.MapValue().Kind() == protoreflect.MessageKind {
88+
elem, err := buildFullBody(v.Message().Interface(), usedFieldsPath)
89+
if err != nil {
90+
rangeErr = fmt.Errorf("recursive build full body: %w", err)
91+
return false
92+
}
93+
newMap.Set(k, protoreflect.ValueOfMessage(elem.ProtoReflect()))
94+
} else {
95+
newMap.Set(k, v)
96+
}
97+
return true
98+
})
99+
if rangeErr != nil {
100+
return nil, fmt.Errorf("map range error: %w", rangeErr)
101+
}
102+
103+
case fd.Kind() == protoreflect.MessageKind:
104+
elem, err := buildFullBody(val.Message().Interface(), usedFieldsPath)
105+
if err != nil {
106+
return nil, fmt.Errorf("recursive build full body: %w", err)
107+
}
108+
newMsgReflect.Set(fd, protoreflect.ValueOfMessage(elem.ProtoReflect()))
109+
110+
default:
111+
newMsgReflect.Set(fd, val)
112+
}
113+
}
114+
115+
return newMsg, nil
116+
}

builder/body_option.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package builder
2+
3+
const (
4+
singleWildcard string = "*"
5+
doubleWildcard string = "**"
6+
)
7+
8+
type bodyOption string
9+
10+
func (o bodyOption) String() string { return string(o) }
11+
12+
func (o bodyOption) isFullBody() bool {
13+
return o.String() == singleWildcard
14+
}
15+
16+
func (o bodyOption) isWithoutBody() bool {
17+
return o == ""
18+
}
19+
20+
func (o bodyOption) isSingleField() bool {
21+
return o != "" && o.String() != singleWildcard
22+
}

0 commit comments

Comments
 (0)