-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathdoc.go
184 lines (144 loc) · 6.46 KB
/
doc.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/*
Package api2 provides types and functions used to define interfaces of
client-server API and facilitate creation of server and client for it.
How to use this package. Organize your code in services. Each service provides
some domain specific functionality. It is a Go type whose methods correspond
to exposed RPC's of the API. Each method has the following signature:
func(ctx, *Request) (*Response, error)
Let's define a service Foo with method Bar.
type Foo struct {
...
}
type BarRequest struct {
// These fields are stored in JSON format in body.
Name string `json:"name"`
// These fields are GET parameters.
UserID int `query:"user_id"`
// These fields are headers.
FileHash string `header:"file_hash"`
// These fields are cookies.
Foo string `cookie:"foo"`
// URL parameters present in URL template like "/path/:product".
Product string `url:"product"`
// These fields are skipped.
SkippedField int `json:"-"`
}
type BarResponse struct {
// These fields are stored in JSON format in body.
FileSize int `json:"file_size"`
// These fields are headers.
FileHash string `header:"file_hash"`
// These fields are skipped.
SkippedField int `json:"-"`
}
func (s *Foo) Bar(ctx context.Context, req *BarRequest) (*BarResponse, error) {
...
}
A field must not have more than one of tags: json, query, header, cookie.
Fields in query, header and cookie parts are encoded and decoded with
fmt.Sprintf and fmt.Sscanf. Strings are not decoded with fmt.Sscanf,
but passed as is. Types implementing encoding.TextMarshaler and
encoding.TextUnmarshaler are encoded and decoded using it.
Cookie in Response part must be of type http.Cookie.
If no field is no JSON field in the struct, then HTTP body is skipped.
You can also set HTTP status code of response by adding a field of type
`int` with tag `use_as_status:"true"` to Response. 0 is interpreted as 200.
If Response has status field, no HTTP statuses are considered errors.
If you need the top-level type matching body JSON to be not a struct,
but of some other kind (e.g. slice or map), you should provide a field
in your struct with tag `use_as_body:"true"`:
type FooRequest struct {
// Body of the request is JSON array of strings: ["abc", "eee", ...].
Body []string `use_as_body:"true"`
// You can add 'header', 'query' and 'cookie' fields here, but not 'json'.
}
If you use `use_as_body:"true"`, you can also set `is_protobuf:"true"`
and put a protobuf type (convertible to proto.Message) in that field.
It will be sent over wire as protobuf binary form.
You can add `use_as_body:"true" is_raw:"true"` to a `[]byte` field,
then it will keep the whole HTTP body.
Streaming. If you use `use_as_body:"true"`, you can also set
`is_stream:"true"`. In this case the field must be of type `io.ReadCloser`.
On the client side put any object implementing `io.ReadCloser` to such
a field in Request. It will be read and closed by the library and used
as HTTP request body. On the server side your handler
should read from the reader passed in that field of Request.
(You don't have to read the entire body and to close it.)
For Response, on the server side, the handler must put any object
implementing `io.ReadCloser` to such a field of Response.
The library will use it to generate HTTP response's body and close it.
On the client side your code must read from that reader the entire response
and then close it. If a streaming field is left `nil`, it is interpreted
as empty body.
Now let's write the function that generates the table of routes:
func GetRoutes(s *Foo) []api2.Route {
return []api2.Route{
{
Method: http.MethodPost,
Path: "/v1/foo/bar/:product",
Handler: s.Bar,
Transport: &api2.JsonTransport{},
},
}
}
You can add multiple routes with the same path, but in this case their
HTTP methods must be different so that they can be distinguished.
If Transport is not set, DefaultTransport is used which is defined as
&api2.JsonTransport{}.
**Error handling**. A handler can return any Go error. `JsonTransport`
by default returns JSON. `Error()` value is put into "error" field of
that JSON. If the error has `HttpCode() int` method, it is called and
the result is used as HTTP return code.
You can pass error details (any struct). For that the error must be of a
custom type. You should register the error type in `JsonTransport.Errors`
map. The key used for that error is put into "code" key of JSON and the
object of the registered type - into "detail" field. The error can be
wrapped using `fmt.Errorf("%w" ...)`. See
test/custom_error_test.go for an example.
In the server you need a real instance of service Foo to pass to GetRoutes.
Then just bind the routes to http.ServeMux and run the server:
// Server.
foo := NewFoo(...)
routes := GetRoutes(foo)
api2.BindRoutes(http.DefaultServeMux, routes)
log.Fatal(http.ListenAndServe(":8080", nil))
The server is running.
It serves foo.Bar function on path /v1/foo/bar with HTTP method Post.
Now let's create the client:
// Client.
routes := GetRoutes(nil)
client := api2.NewClient(routes, "http://127.0.0.1:8080")
barRes := &BarResponse{}
err := client.Call(context.Background(), barRes, &BarRequest{
Product: "product1",
...
})
if err != nil {
panic(err)
}
// Server's response is in variable barRes.
The client sent request to path "/v1/foo/bar/product1", from which
the server understood that product=product1.
Note that you don't have to pass a real service object to GetRoutes
on client side. You can pass nil, it is sufficient to pass all needed
information about request and response types in the routes table, that
is used by client to find a proper route.
You can make GetRoutes accepting an interface instead of a concrete
Service type. In this case you can not get method handlers by s.Bar,
because this code panics if s is nil interface. As a workaround api2
provides function Method(service pointer, methodName) which you can use:
type Service interface {
Bar(ctx context.Context, req *BarRequest) (*BarResponse, error)
}
func GetRoutes(s Service) []api2.Route {
return []api2.Route{
{Method: http.MethodPost, Path: "/v1/foo/bar/:product", Handler: api2.Method(&s, "Bar"), Transport: &api2.JsonTransport{}},
}
}
If you have function GetRoutes in package foo as above you can generate static client
for it in file client.go located near the file in which GetRoutes is defined:
api2.GenerateClient(foo.GetRoutes)
GenerateClient can accept multiple GetRoutes functions, but they must
be located in the same package.
*/
package api2