Я бы хотел в этой статье рассказать вам о том как можно быстро и просто сделать веб сервер на языке Golang с документацией к нему. И о том какие есть подходы и инструменты для их реализации
Сегодня мы разберем эти готовые инструменты:
Swagger Codegen
Начнем с swagger-api/swagger-codegen, представленным swagger.io. Swagger Codegen упрощает процесс сборки за счет создания серверных заглушек и клиентских SDK для любого API, определенного в спецификации OpenAPI (ранее известной как Swagger), вам остается лишь сделать реализацию вашего API.
Ниже приведен пример нашего swagger файла и его наглядный вид в Swagger Editor'е, где мы за одно можем и сгенерировать наши реализации.
{
"swagger" : "2.0",
"info" : {
"version" : "1.0.0",
"title" : "Swagger Petstore"
},
"host" : "localhost",
"basePath" : "/v1",
"tags" : [ {
"name" : "pet"
} ],
"schemes" : [ "https", "http" ],
"paths" : {
"/pet" : {
"post" : {
"tags" : [ "pet" ],
"summary" : "Add a new pet to the store",
"operationId" : "addPet",
"consumes" : [ "application/json" ],
"produces" : [ "application/json" ],
"parameters" : [ {
"in" : "body",
"name" : "body",
"description" : "Pet object that needs to be added to the store",
"required" : true,
"schema" : {
"$ref" : "#/definitions/Pet"
}
} ],
"responses" : {
"405" : {
"description" : "Invalid input"
}
}
}
}
},
"definitions" : {
"Category" : {
"type" : "object",
"properties" : {
"id" : {
"type" : "integer",
"format" : "int64"
},
"name" : {
"type" : "string"
}
}
},
"Tag" : {
"type" : "object",
"properties" : {
"id" : {
"type" : "integer",
"format" : "int64"
},
"name" : {
"type" : "string"
}
}
},
"Pet" : {
"type" : "object",
"required" : [ "name", "photoUrls" ],
"properties" : {
"id" : {
"type" : "integer",
"format" : "int64"
},
"category" : {
"$ref" : "#/definitions/Category"
},
"name" : {
"type" : "string",
"example" : "doggie"
},
"photoUrls" : {
"type" : "array",
"items" : {
"type" : "string"
}
},
"tags" : {
"type" : "array",
"items" : {
"$ref" : "#/definitions/Tag"
}
},
"status" : {
"type" : "string",
"description" : "pet status in the store",
"enum" : [ "available", "pending", "sold" ]
}
}
}
}
}
У клиента и сервера идентичны объекты структур. Пример файла go-client-generated/model_pet.go
и go-server-server-generated/go/model_pet.go
.
/*
* Swagger Petstore
*
* No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen)
*
* API version: 1.0.0
* Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
*/
package swagger
type Pet struct {
Id int64 `json:"id,omitempty"`
Category *Category `json:"category,omitempty"`
Name string `json:"name"`
PhotoUrls []string `json:"photoUrls"`
Tags []Tag `json:"tags,omitempty"`
// pet status in the store
Status string `json:"status,omitempty"`
}
Клиент содержит так же в себе методы для отправки запросов, а сервер endpiont'ы и заглушки для их обработки.
Самый простой инструмент, в плане своего функционала. Сильная сторона, это то что можно генерировать код почти под все современные языки программирования.
go-swagger
Следующий инструмент? — ?go-swagger/go-swagger. Он предоставляет сообществу go полный набор полнофункциональных API-компонентов для работы с Swagger API: сервер, клиент и модель данных.
- Создает сервер из спецификации Swagger
- Генерирует клиента из спецификации Swagger
- Поддерживает большинство функций, предлагаемых jsonschema и swagger, включая полиморфизм
- Создает спецификацию swagger из аннотированного кода go
- Дополнительные инструменты для работы со swagger спецификацией
- Отличные функции настройки, с расширениями поставщиков и настраиваемыми шаблонами
Сервер
Команда для генерации сервера:
$ swagger generate server -f ./swagger.json
Все файлы, кроме одного, будут перегенерированны после повторного запуска команды. Будте внимательны при обновления кода под новые версии документации.
Этот файл — go-swagger-service/restapi/configure_swagger_petstore.go
. В нем мы можем конфигурировать наш сервер: определять handler'ы, подключать middleware, настраивать логирование, переопределять ответы, и все что нам может еще быть полезным.
// This file is safe to edit. Once it exists it will not be overwritten
package restapi
import (
"crypto/tls"
"net/http"
errors "github.com/go-openapi/errors"
runtime "github.com/go-openapi/runtime"
middleware "github.com/go-openapi/runtime/middleware"
"go-generator/go-swagger-service/restapi/operations"
"go-generator/go-swagger-service/restapi/operations/pet"
)
//go:generate swagger generate server --target ../../go-swagger --name SwaggerPetstore --spec ../../swagger.json
func configureFlags(api *operations.SwaggerPetstoreAPI) {
// api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{ ... }
}
func configureAPI(api *operations.SwaggerPetstoreAPI) http.Handler {
// configure the api here
api.ServeError = errors.ServeError
// Set your custom logger if needed. Default one is log.Printf
// Expected interface func(string, ...interface{})
//
// Example:
// api.Logger = log.Printf
api.JSONConsumer = runtime.JSONConsumer()
api.JSONProducer = runtime.JSONProducer()
if api.PetAddPetHandler == nil {
api.PetAddPetHandler = pet.AddPetHandlerFunc(func(params pet.AddPetParams) middleware.Responder {
return middleware.NotImplemented("operation pet.AddPet has not yet been implemented")
})
}
api.ServerShutdown = func() {}
return setupGlobalMiddleware(api.Serve(setupMiddlewares))
}
// The TLS configuration before HTTPS server starts.
func configureTLS(tlsConfig *tls.Config) {
// Make all necessary changes to the TLS configuration here.
}
// As soon as server is initialized but not run yet, this function will be called.
// If you need to modify a config, store server instance to stop it individually later, this is the place.
// This function can be called multiple times, depending on the number of serving schemes.
// scheme value will be set accordingly: "http", "https" or "unix"
func configureServer(s *http.Server, scheme, addr string) {
}
// The middleware configuration is for the handler executors. These do not apply to the swagger.json document.
// The middleware executes after routing but before authentication, binding and validation
func setupMiddlewares(handler http.Handler) http.Handler {
return handler
}
// The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document.
// So this is a good place to plug in a panic handling middleware, logging and metrics
func setupGlobalMiddleware(handler http.Handler) http.Handler {
return handler
}
При обработке запросов происходит автоматическая валидация всех объектов и их полей. И отдается ответ со статусом 422
и описанием ошибки.
Поля валидируются на соответствие параметрам заданным в swagger документации. Такие как enum
, required
, type
, maximum
и другие.
// Code generated by go-swagger; DO NOT EDIT.
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"strconv"
strfmt "github.com/go-openapi/strfmt"
"github.com/go-openapi/errors"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// Pet pet
// swagger:model Pet
type Pet struct {
// category
Category *Category `json:"category,omitempty"`
// id
ID int64 `json:"id,omitempty"`
// name
// Required: true
Name *string `json:"name"`
// photo urls
// Required: true
PhotoUrls []string `json:"photoUrls"`
// pet status in the store
// Enum: [available pending sold]
Status string `json:"status,omitempty"`
// tags
Tags []*Tag `json:"tags"`
}
// Validate validates this pet
func (m *Pet) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateCategory(formats); err != nil {
res = append(res, err)
}
if err := m.validateName(formats); err != nil {
res = append(res, err)
}
if err := m.validatePhotoUrls(formats); err != nil {
res = append(res, err)
}
if err := m.validateStatus(formats); err != nil {
res = append(res, err)
}
if err := m.validateTags(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *Pet) validateCategory(formats strfmt.Registry) error {
if swag.IsZero(m.Category) { // not required
return nil
}
if m.Category != nil {
if err := m.Category.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("category")
}
return err
}
}
return nil
}
func (m *Pet) validateName(formats strfmt.Registry) error {
if err := validate.Required("name", "body", m.Name); err != nil {
return err
}
return nil
}
func (m *Pet) validatePhotoUrls(formats strfmt.Registry) error {
if err := validate.Required("photoUrls", "body", m.PhotoUrls); err != nil {
return err
}
return nil
}
var petTypeStatusPropEnum []interface{}
func init() {
var res []string
if err := json.Unmarshal([]byte(`["available","pending","sold"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
petTypeStatusPropEnum = append(petTypeStatusPropEnum, v)
}
}
const (
// PetStatusAvailable captures enum value "available"
PetStatusAvailable string = "available"
// PetStatusPending captures enum value "pending"
PetStatusPending string = "pending"
// PetStatusSold captures enum value "sold"
PetStatusSold string = "sold"
)
// prop value enum
func (m *Pet) validateStatusEnum(path, location string, value string) error {
if err := validate.Enum(path, location, value, petTypeStatusPropEnum); err != nil {
return err
}
return nil
}
func (m *Pet) validateStatus(formats strfmt.Registry) error {
if swag.IsZero(m.Status) { // not required
return nil
}
// value enum
if err := m.validateStatusEnum("status", "body", m.Status); err != nil {
return err
}
return nil
}
func (m *Pet) validateTags(formats strfmt.Registry) error {
if swag.IsZero(m.Tags) { // not required
return nil
}
for i := 0; i < len(m.Tags); i++ {
if swag.IsZero(m.Tags[i]) { // not required
continue
}
if m.Tags[i] != nil {
if err := m.Tags[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("tags" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *Pet) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *Pet) UnmarshalBinary(b []byte) error {
var res Pet
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}
Для старта сервера используется команда
$ run ./go-swagger-service/cmd/swagger-petstore-server/main.go --port 8090
Клиент
Команда для генерации клиента:
$ swagger generate client -f ./swagger.json
Эта фича очень полезна, для написания api-sdk. Клиент содержит в себе не только модели объектов, но и методы для их преобразования в запрос, его отправку и получения ответа.
// Code generated by go-swagger; DO NOT EDIT.
package pet
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/runtime"
strfmt "github.com/go-openapi/strfmt"
)
// New creates a new pet API client.
func New(transport runtime.ClientTransport, formats strfmt.Registry) *Client {
return &Client{transport: transport, formats: formats}
}
/*
Client for pet API
*/
type Client struct {
transport runtime.ClientTransport
formats strfmt.Registry
}
/*
AddPet adds a new pet to the store
*/
func (a *Client) AddPet(params *AddPetParams) error {
// TODO: Validate the params before sending
if params == nil {
params = NewAddPetParams()
}
_, err := a.transport.Submit(&runtime.ClientOperation{
ID: "addPet",
Method: "POST",
PathPattern: "/pet",
ProducesMediaTypes: []string{"application/json"},
ConsumesMediaTypes: []string{"application/json"},
Schemes: []string{"http", "https"},
Params: params,
Reader: &AddPetReader{formats: a.formats},
Context: params.Context,
Client: params.HTTPClient,
})
if err != nil {
return err
}
return nil
}
// SetTransport changes the transport on the client
func (a *Client) SetTransport(transport runtime.ClientTransport) {
a.transport = transport
}
Есть вариант генерации документации из кода приложения. Об этом подробно можно почитать Generate a spec from source code.
Я описал не все возможность этого инструмента. Оно является самым функциональным, гибким и полным для работы с swagger и кодогенерацией.
grpc-gateway
Теперь посмотрим инструменты для генерации swagger документации. grpc-ecosystem/grpc-gateway — ?это плагин protoc
. Он читает определение сервиса gRPC и генерирует обратный прокси-сервер, который переводит RESTful JSON API в gRPC. Этот сервер создается в соответствии с пользовательскими параметрами в вашем определении gRPC.
Этот проект направлен на предоставление интерфейса HTTP + JSON вашей службе gRPC. Это помогает вам предоставлять свои API в стиле gRPC и RESTful одновременно.
И как небольшой бонус так же есть возможность создания swagger.json с использованием protoc-gen-swagger.
Сейчас мы соберем наш сервер и пройдемся по каждому из шагов. Для этого установим наши сборщики.
$ go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger github.com/golang/protobuf/protoc-gen-go
Для начала нам потребуется написать вот такой .proto
файл
syntax = "proto3";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
package pet;
message AddPetRequest {
int64 id = 1;
message Category {
int64 id = 1;
string name = 2;
}
Category category = 2;
string name = 3;
repeated string photo_urls = 4;
message Tag {
int64 id = 1;
string name = 2;
}
repeated Tag tags = 5;
//pet status in the store
enum Status {
available = 0;
pending = 1;
sold = 2;
}
Status status = 6;
}
service PetService {
rpc AddPet (AddPetRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/pet"
body: "*"
};
}
}
Выполним команду для генерации gRPC stub:
$ protoc -I/usr/local/include -I. -I$GOPATH/src -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --go_out=plugins=grpc:. pet.proto
У нас появился файлик pet.pb.go
. И сразу же соберем reverse-proxy с помощью protoc-gen-grpc-gateway
:
$ protoc -I/usr/local/include -I. -I$GOPATH/src -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --grpc-gateway_out=logtostderr=true:. pet.proto
Файл pet.pb.gw.go
готов. Это и есть наш gateway. Теперь соберем pet.swagger
.json утилитой protoc-gen-swagger
:
$ 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:. pet.proto
Осталось реализовать интерфейс PetServiceServer.
// PetServiceServer is the server API for PetService service.
type PetServiceServer interface {
AddPet(context.Context, *AddPetRequest) (*empty.Empty, error)
}
А код для старта сервера может выглядеть примерно вот так:
package pet
import (
"context"
"flag"
"net/http"
"github.com/golang/glog"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
gw "grpc-gateway/pet"
"grpc-gateway/service"
)
func run() error {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
mux := runtime.NewServeMux()
err := gw.RegisterPetServiceHandlerServer(ctx, mux, &service.PetService{})
if err != nil {
return err
}
return http.ListenAndServe(":8081", mux)
}
func main() {
flag.Parse()
defer glog.Flush()
if err := run(); err != nil {
glog.Fatal(err)
}
}
Когда мы собрали минимально живучий продукт, можно перейти к тонкостям его реализации. Их вы можете посмотреть в моей отдельной статье, с более глубоким и детальным разбором подхода c инструментом grpc-gateway.
Swag
Swag преобразует аннотации Go в документацию Swagger 2.0. Написано множество плагинов для популярных веб-фреймворков Go. Это позволяет быстро интегрировать swaggo/swag в существующий проект Go (используя Swagger UI).
Поддерживаемые фреймворки:
Swagger аннотации делятся на две части, общей информации о документации и документацию endpoint'ов. Ниже приведены их примеры.
// @title Swagger Example API
// @version 1.0
// @description This is a sample server celler server.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /api/v1
// @query.collection.format multi
// @securityDefinitions.basic BasicAuth
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
// @securitydefinitions.oauth2.application OAuth2Application
// @tokenUrl https://example.com/oauth/token
// @scope.write Grants write access
// @scope.admin Grants read and write access to administrative information
// @securitydefinitions.oauth2.implicit OAuth2Implicit
// @authorizationurl https://example.com/oauth/authorize
// @scope.write Grants write access
// @scope.admin Grants read and write access to administrative information
// @securitydefinitions.oauth2.password OAuth2Password
// @tokenUrl https://example.com/oauth/token
// @scope.read Grants read access
// @scope.write Grants write access
// @scope.admin Grants read and write access to administrative information
// @securitydefinitions.oauth2.accessCode OAuth2AccessCode
// @tokenUrl https://example.com/oauth/token
// @authorizationurl https://example.com/oauth/authorize
// @scope.admin Grants read and write access to administrative information
// @x-extension-openapi {"example": "value on a json format"}
func main() {
// ...
}
// ShowAccount godoc
// @Summary Show a account
// @Description get string by ID
// @ID get-string-by-int
// @Accept json
// @Produce json
// @Param id path int true "Account ID"
// @Success 200 {object} model.Account
// @Header 200 {string} Token "qwerty"
// @Failure 400 {object} httputil.HTTPError
// @Failure 404 {object} httputil.HTTPError
// @Failure 500 {object} httputil.HTTPError
// @Router /accounts/{id} [get]
func (c *ExampleController) ShowAccount(ctx *contex.Context) {
// ...
}
Также исчерпывающая документация с примерами есть в репозитории на github.
Отличный способ написания документации уже по существующим проектам, проверенным временем, расширяющимся проектам.
Вывод
Сейчас мы только что расширили наше понимание возможных взаимодействий с swagger документацией. Рассмотрели способы проектирования кода и составления документации к нему.
И я думаю, что некоторые уже выбрали подходящий способ для реализации API своего нового проекта или примерили разобранные примеры к текущим.