Toolkit provides a means to have a generated code that supports a certain common functionality that is typicall requesred for any service. Toolkit declares its own format for Resposes, Errors, Long Running operations, Collection operators. More details on this can be found in appropriate section on this page.
Tollkit approach provides following features:
- Application may be composed from one or more independent services (micro-service architecture)
- Service is supposed to be a gRPC service
- REST API is presented by a separate service (gRPC Gateway) that serves as a reverse-proxy and forwards incoming HTTP requests to gRPC services
API Toolkit is not a framework it is a set of plugins for Google Protocol Buffer compiler.
See official documentation for Protocol Buffer and for gRPC
As an alternative you may use this plugin to generate Golang code. That is the same as official plugin but with gadgets.
See official documentation
One of the requirements to the API Toolkit is to support a Pipeline model. We recommend to use gRPC server interceptor as middleware. See examples
We offer a convenient way to extract the AccountID field from an incoming authorization token.
For this purpose auth.GetAccountID(ctx, nil)
function can be used:
func (s *contactsServer) Read(ctx context.Context, req *ReadRequest) (*ReadResponse, error) {
input := req.GetContact()
accountID, err := mw.GetAccountID(ctx, nil)
if err == nil {
input.AccountId = accountID
} else if input.GetAccountId() == "" {
return nil, err
}
c, err := DefaultReadContact(ctx, input, s.db)
if err != nil {
return nil, err
}
return &ReadResponse{Contact: c}, nil
}
When bootstrapping a gRPC server, add middleware that will extract the account_id token from the request context and set it in the request struct. The middleware will have to navigate the request struct via reflection, in the case that the account_id field is nested within the request (like if it's in a request wrapper as per our example above)
We recommend to use this validation plugin to generate
Validate
method for your gRPC requests.
As an alternative you may use this plugin too.
Validation can be invoked "automatically" if you add this middleware as a gRPC server interceptor.
We recommend to use this plugin to generate documentation.
Documentation can be generated in different formats.
Here are several most used instructions used in documentation generation:
Leading comments can be used everywhere.
/**
* This is a leading comment for a message
*/
message SomeMessage {
// this is another leading comment
string value = 1;
}
Fields, Service Methods, Enum Values and Extensions support trailing comments.
enum MyEnum {
DEFAULT = 0; // the default value
OTHER = 1; // the other value
}
If you want to have some comment in your proto files, but don't want them to be part of the docs, you can simply prefix the comment with @exclude.
Example: include only the comment for the id field
/**
* @exclude
* This comment won't be rendered
*/
message ExcludedMessage {
string id = 1; // the id of this message.
string name = 2; // @exclude the name of this message
/* @exclude the value of this message. */
int32 value = 3;
}
Optionally you may generate Swagger schema from your proto file. To do so install this plugin.
go get -u github.com/golang/protobuf/protoc-gen-go
Then invoke it as a plugin for Proto Compiler
protoc -I/usr/local/include -I. \
-I$GOPATH/src \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--swagger_out=logtostderr=true:. \
path/to/your_service.proto
import "protoc-gen-swagger/options/annotations.proto";
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
info: {
title: "My Service";
version: "1.0";
};
schemes: HTTP;
schemes: HTTPS;
consumes: "application/json";
produces: "application/json";
};
message MyMessage {
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = {
external_docs: {
url: "https://infoblox.com/docs/mymessage";
description: "MyMessage description";
}
};
For more Swagger options see this scheme
See example contacts app. Here is a generated Swagger schema.
NOTE Well Known Types are generated in a bit unusual way:
"protobufEmpty": {
"type": "object",
"description": "service Foo {\n rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);\n }\n\nThe JSON representation for `Empty` is empty JSON object `{}`.",
"title": "A generic empty message that you can re-use to avoid defining duplicated\nempty messages in your APIs. A typical example is to use it as the request\nor the response type of an API method. For instance:"
},
For convenience purposes there is an atlas-gentool image available which contains a pre-installed set of often used plugins. For more details see infobloxopen/atlas-gentool repository.
An example app that is based on api-toolkit can be found here
Toolkit enforces some of the API syntax requirements that are common for applications that are written by Infoblox. All public REST API endpoints must follow the same guidelines mentioned below.
You can map your gRPC service methods to one or more REST API endpoints. See this reference how to do it.
// It is possible to define multiple HTTP methods for one RPC by using
// the `additional_bindings` option. Example:
//
// service Messaging {
// rpc GetMessage(GetMessageRequest) returns (Message) {
// option (google.api.http) = {
// get: "/v1/messages/{message_id}"
// additional_bindings {
// get: "/v1/users/{user_id}/messages/{message_id}"
// }
// };
// }
// }
// message GetMessageRequest {
// string message_id = 1;
// string user_id = 2;
// }
//
//
// This enables the following two alternative HTTP JSON to RPC
// mappings:
//
// HTTP | RPC
// -----|-----
// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")`
// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")`
To extract headers from metadata all you need is to use FromIncomingContext function
import (
"context"
"google.golang.org/grpc/metadata"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
)
func (s *myServiceImpl) MyMethod(ctx context.Context, req *MyRequest) (*MyResponse, error) {
var userAgent string
if md, ok := metadata.FromIncomingContext(ctx); ok {
// Uppercase letters are automatically converted to lowercase, see metadata.New
if u, ok [runtime.MetadataPrefix+"user-agent"]; ok {
userAgen = u[0]
}
}
}
Also you can use our helper function gw.Header()
import (
"context"
"github.com/infobloxopen/atlas-app-toolkit/gw"
)
func (s *myServiceImpl) MyMethod(ctx context.Context, req *MyRequest) (*MyResponse, error) {
var userAgent string
if h, ok := gw.Header(ctx, "user-agent"); ok {
userAgent = h
}
}
To send metadata to gRPC-Gateway from your gRPC service you need to use SetHeader function.
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func (s *myServiceImpl) MyMethod(ctx context.Context, req *MyRequest) (*MyResponse, error) {
md := metadata.Pairs("myheader", "myvalue")
if err := grpc.SetHeader(ctx, md); err != nil {
return nil, err
}
return nil, nil
}
If you do not use any custom outgoing header matcher you would see something like that:
> curl -i http://localhost:8080/contacts/v1/contacts
HTTP/1.1 200 OK
Content-Type: application/json
Grpc-Metadata-Myheader: myvalue
Date: Wed, 31 Jan 2018 15:28:52 GMT
Content-Length: 2
{}
By default gRPC-Gateway translates non-error gRPC response into HTTP response
with status code set to 200 - OK
.
A HTTP response returned from gRPC-Gateway does not comform REST API Syntax
and has no success
section.
In order to override this behavior gRPC-Gateway wiki recommends to overwrite
ForwardResponseMessage
and ForwardResponseStream
functions correspondingly.
See this article
import (
"github.com/infobloxopen/atlas-app-toolkit/gw"
)
func init() {
forward_App_ListObjects_0 = gw.ForwardResponseMessage
}
You can also refer example app
We made default ForwardResponseMessage and ForwardResponseMessage implementations that conform REST API Syntax.
NOTE the forwarders still set 200 - OK
as HTTP status code if no errors encountered.
In order to set HTTP status codes propely you need to send metadata from your gRPC service so that default forwarders will be able to read them and set codes. That is a common approach in gRPC to send extra information for response as metadata.
We recommend use gRPC status package and our custom function SetStatus to add extra metadata to the gRPC response.
See documentation in package status.
Also you may use shortcuts like: SetCreated
, SetUpdated
and SetDeleted
.
import (
"github.com/infobloxopen/atlas-app-toolkit/gw"
)
func (s *myService) MyMethod(req *MyRequest) (*MyResponse, error) {
err := gw.SetCreated(ctx, "created 1 item")
return &MyResponse{Result: []*Item{item}}, err
}
Services render resources in responses in JSON format by default unless another format is specified in the request Accept header that the service supports.
Services must embed their response in a Success JSON structure.
The Success JSON structure provides a uniform structure for expressing normal responses using a structure similar to the Error JSON structure used to render errors. The structure provides an enumerated set of codes and associated HTTP statuses (see Errors below) along with a message.
The Success JSON structure has the following format. The results tag is optional and appears when the response contains one or more resources.
{
"success": {
"status": <http-status-code>,
"code": <enumerated-error-code>,
"message": <message-text>
},
"results": <service-response>
}
Method error responses are rendered in the Error JSON format. The Error JSON format is similar to the Success JSON format for error responses using a structure similar to the Success JSON structure for consistency.
The Error JSON structure has the following format. The details tag is optional and appears when the service provides more details about the error.
{
"error": {
"status": <http-status-code>,
"code": <enumerated-error-code>,
"message": <message-text>
},
"details": [
{
"message": <message-text>,
"code": <enumerated-error-code>,
"target": <resource-name>,
},
...
]
}
How can I convert a gRPC error to a HTTP error response in accordance with REST API Syntax Specification?
You can write your own ProtoErrorHandler
or use gw.DefaultProtoErrorHandler
one.
How to handle error on gRPC-Gateway see article
How to use gw.DefaultProtoErrorHandler see example below:
import (
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/infobloxopen/atlas-app-toolkit/gw"
"github.com/yourrepo/yourapp"
)
func main() {
// create error handler option
errHandler := runtime.WithProtoErrorHandler(gw.DefaultProtoErrorHandler)
// pass that option as a parameter
mux := runtime.NewServeMux(errHandler)
// register you app handler
yourapp.RegisterAppHandlerFromEndpoint(ctx, mux, addr)
...
// Profit!
}
You can find sample in example folder. See code
The idiomatic way to send an error from you gRPC service is to simple return
it from you gRPC handler either as status.Errorf()
or errors.New()
.
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *myServiceImpl) MyMethod(req *MyRequest) (*MyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method is not implemented: %v", req)
}
To attach details to your error you have to use grpc/status
package.
You can use our default implementation of error details (rpc/errdetails
) or your own one.
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/infobloxopen/atlas-app-toolkit/rpc/errdetails"
)
func (s *myServiceImpl) MyMethod(req *MyRequest) (*MyResponse, error) {
s := status.New(codes.Unimplemented, "MyMethod is not implemented")
s = s.WithDetails(errdetails.New(codes.Internal), "myservice", "in progress")
return nil, s.Err()
}
With gw.DefaultProtoErrorHandler
enabled JSON response will look like:
{
"error": {
"status": 501,
"code": "NOT_IMPLEMENTED",
"message": "MyMethod is not implemented"
},
"details": [
{
"code": "INTERNAL",
"message": "in progress",
"target": "myservice"
}
]
}
For methods that return collections, operations may be implemented using the following conventions. The operations are implied by request parameters in query strings. In some cases, stateful operational information may be passed in responses. Toolkit introduces a set of common request parameters that can be used to control the way collections are returned. API toolkit provides some convenience methods to support these parameters in your application.
You can enable support of collection operators in your gRPC-Gateway by adding
a runtime.ServeMuxOption
using runtime.WithMetadata(gw.MetadataAnnotator)
.
import (
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/infobloxopen/atlas-app-toolkit/gw"
"github.com/yourrepo/yourapp"
)
func main() {
// create collection operator handler
opHandler := runtime.WithMetadata(gw.MetadataAnnotator)
// pass that option as a parameter
mux := runtime.NewServeMux(opHandler)
// register you app handler
yourapp.RegisterAppHandlerFromEndpoint(ctx, mux, addr)
...
}
If you want to explicitly declare one of collection operators in your proto
scheme, to do so just import collection_operators.proto
.
import "github.com/infobloxopen/atlas-app-toolkit/op/collection_operators.proto";
message MyRequest {
infoblox.api.Sorting sorting = 1;
}
After you declare one of collection operator in your proto
message you need
to add mw.WithCollectionOperator
server interceptor to the chain in your
gRPC service.
server := grpc.NewServer(
grpc.UnaryInterceptor(
grpc_middleware.ChainUnaryServer( // middleware chain
...
mw.WithCollectionOperator(), // collection operators
...
),
),
)
Doing so all collection operators that defined in your proto message will be populated in case if they provided in incoming HTTP request.
You can use ApplyCollectionOperators
method from op/gorm package.
...
gormDB, err = ApplyCollectionOperators(gormDB, ctx)
if err != nil {
...
}
var people []Person
gormDB.Find(&people)
...
Separate methods per each collection operator are also available.
Check out example and implementation.
A service may implement field selection of collection data to reduce the volume of data in the result. A collection of response resources can be transformed by specifying a set of JSON tags to be returned. For a “flat” resource, the tag name is straightforward. If field selection is allowed on non-flat hierarchical resources, the service should implement a qualified naming scheme such as dot-qualification to reference data down the hierarchy. If a resource does not have the specified tag, the tag does not appear in the output resource.
Request Parameter | Description |
---|---|
_fields | A comma-separated list of JSON tag names. |
API toolkit provides a default support to strip fields in response. As it is not possible to completely remove all the fields
(such as primitives) from proto.Message
. Because of this fields are additionally truncated on grpc-gateway
. From gRPC it is also possible
to access _fields
from request, use them to perform data fetch operations and control output. This can be done by setting
appropriate metadata keys that will be handled by grpc-gateway
. See example below:
fields := gw.FieldSelection(ctx)
if fields != nil {
// ... work with fields
gw.SetFieldSelection(ctx, fields) //in case fields were changed comparing to what was in request
}
import "github.com/infobloxopen/atlas-app-toolkit/op/collection_operators.proto";
message MyRequest {
infoblox.api.FieldSelection fields = 1;
}
A service may implement collection sorting. A collection of response resources can be sorted by their JSON tags. For a “flat” resource, the tag name is straightforward. If sorting is allowed on non-flat hierarchical resources, the service should implement a qualified naming scheme such as dot-qualification to reference data down the hierarchy. If a resource does not have the specified tag, its value is assumed to be null.
Request Parameter | Description |
---|---|
_order_by | A comma-separated list of JSON tag names. The sort direction can be specified by a suffix separated by whitespace before the tag name. The suffix “asc” sorts the data in ascending order. The suffix “desc” sorts the data in descending order. If no suffix is specified the data is sorted in ascending order. |
import "github.com/infobloxopen/atlas-app-toolkit/op/collection_operators.proto";
message MyRequest {
infoblox.api.Sorting sort = 1;
}
You may get it by using gw.Sorting
function. Please note that if _order_by
has not been specified in an incoming HTTP request gw.Sorting
returns nil, nil
.
import (
"context"
"github.com/infobloxopen/atlas-app-toolkit/gw"
"github.com/infobloxopen/atlas-app-toolkit/op"
)
func (s *myServiceImpl) MyMethod(ctx context.Context, req *MyRequest) (*MyResponse, error) {
if sort, err := gw.Sorting(ctx); err != nil {
return nil, err
// check if sort has been specified!!!
} else if sort != nil {
// do sorting
//
// if you use gORM you may do the following
// db.Order(sort.GoString())
}
}
Also you may want to declare sorting parameter in your proto
message.
In this case it will be populated automatically if you using
mw.WithCollectionOperator
server interceptor.
See documentation in op package
A service may implement filtering. A collection of response resources can be filtered by a logical expression string that includes JSON tag references to values in each resource, literal values, and logical operators. If a resource does not have the specified tag, its value is assumed to be null.
Request Parameter | Description |
---|---|
_filter | A string expression containing JSON tags, literal values, and logical operators. |
Literal values include numbers (integer and floating-point), and quoted (both single- or double-quoted) literal strings, and “null”. The following operators are commonly used in filter expressions.
Operator | Description | Example |
---|---|---|
== | eq | Equal |
!= | ne | Not Equal |
> | gt | Greater Than |
>= | ge | Greater Than or Equal To |
< | lt | Less Than |
<= | le | Less Than or Equal To |
and | Logical AND | price <= 200 and price > 3.5 |
~ | match | Matches Regex |
!~ | nomatch | Does Not Match Regex |
or | Logical OR | price <= 3.5 or price > 200 |
not | Logical NOT | not price <= 3.5 |
() | Grouping | (priority == 1 or city == ‘Santa Clara’) and price > 100 |
Usage of filtering features from the toolkit is similar to sorting.
Note: if you decide to use toolkit provided infoblox.api.Filtering
proto type, then you'll not be able to use swagger schema generation, since it's plugin doesn't work with recursive nature of infoblox.api.Filtering
.
import "github.com/infobloxopen/atlas-app-toolkit/op/collection_operators.proto";
message MyRequest {
infoblox.api.Filtering filter = 1;
}
A service may implement pagination of collections. Pagination of response resources can be client-driven, server-driven, or both.
Client-driven pagination is a model in which rows are addressable by offset and page size. This scheme is similar to SQL query pagination where row offset and page size determine the rows in the query response.
Server-driven pagination is a model in which the server returns some amount of data along with a token indicating there is more data and where subsequent queries can get the next page of data. This scheme is used by AWS Dynamo where, depending on the individual resource size, pages can run into thousands of resources.
Some data sources can provide the number of resources a query will generate, while others cannot.
The paging model provided by the service is influenced by the expectations of the client. GUI clients prefer moderate page sizes, say no more than 1,000 resources per page. A “streaming” client may be able to consume tens of thousands of resources per page.
Consider the service behavior when no paging parameters are in the request. Some services may provide all the resources unpaged, while other services may have a default page size and provide the first page of data. In either case, the service should document its paging behavior in the absence of paging parameters.
Consider the service behavior when the query sorts or filters the data, and the underlying data is changing over time. A service may cache some amount of sorted and filtered data to be paged using client-driven paging, particularly in a GUI context. There is a trade-off between paged data coherence and changes to the data. The cache expiration time attempts to balance these competing factors.
Paging Mode | Request Parameters | Response Parameters | Description |
---|---|---|---|
Client-driven paging | _offset | The integer index (zero-origin) of the offset into a collection of resources. If omitted or null the value is assumed to be “0”. | |
_limit | The integer number of resources to be returned in the response. The service may impose maximum value. If omitted the service may impose a default value. | ||
_offset | The service may optionally* include the offset of the next page of resources. A null value indicates no more pages. | ||
_size | The service may optionally include the total number of resources being paged. | ||
Server-driven paging | _page_token | The service-defined string used to identify a page of resources. A null value indicates the first page. | |
_page_token | The service response should contain a string to indicate the next page of resources. A null value indicates no more pages. | ||
_size | The service may optionally include the total number of resources being paged. | ||
Composite paging | _page_token | The service-defined string used to identify a page of resources. A null value indicates the first page. | |
_offset | The integer index (zero-origin) of the offset into a collection of resources in the page defined by the page token. If omitted or null the value is assumed to be “0”. | ||
_limit | The integer number of resources to be returned in the response. The service may impose maximum value. If omitted the service may impose a default value. | ||
_page_token | The service response should contain a string to indicate the next page of resources. A null value indicates no more pages. | ||
_offset | The service should include the offset of the next page of resources in the page defined by the page token. A null value indicates no more pages, at which point the client should request the page token in the response to get the next page. | ||
_size | The service may optionally include the total number of resources being paged. |
Note: Response offsets are optional since the client can often keep state on the last offset/limit request.
import "github.com/infobloxopen/atlas-app-toolkit/op/collection_operators.proto";
message MyRequest {
infoblox.api.Pagination paging = 1;
}
message MyResponse {
infoblox.api.PageInfo page = 1;
}