Способы реализации API-сервера на Golang с автогенерацией кода и документации

от автора

Я бы хотел в этой статье рассказать вам о том как можно быстро и просто сделать веб сервер на языке Golang с документацией к нему. И о том какие есть подходы и инструменты для их реализации

Сегодня мы разберем эти готовые инструменты:

Swagger Codegen

Начнем с swagger-api/swagger-codegen, представленным swagger.io. Swagger Codegen упрощает процесс сборки за счет создания серверных заглушек и клиентских SDK для любого API, определенного в спецификации OpenAPI (ранее известной как Swagger), вам остается лишь сделать реализацию вашего API.

Ниже приведен пример нашего swagger файла и его наглядный вид в Swagger Editor’е, где мы за одно можем и сгенерировать наши реализации.

swagger.json

{   "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" ]         }       }     }   } }

Наглядный вид в Swagger UI

Наглядный вид в Swagger UI

Экспорт из Swagger Editor

Экспорт из Swagger Editor

У клиента и сервера идентичны объекты структур. Пример файла go-client-generated/model_pet.go и go-server-server-generated/go/model_pet.go.

Модель структуры Pet в клиенте и сервере

/*  * 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 и другие.

Объект Pet и его валидация

// 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 одновременно.

Визуализация работы grpc-gateway в качестве reverse-proxy

Визуализация работы grpc-gateway в качестве reverse-proxy

И как небольшой бонус так же есть возможность создания 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 файл

Пример protobuf файла с аннотациями

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) }

А код для старта сервера может выглядеть примерно вот так:

main.go

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)     } }

Когда мы собрали минимально живучий продукт, можно перейти к тонкостям его реализации. Их вы можете посмотреть в моей отдельной статье, с более глубоким и детальным разбором этого подхода к построению веб-сервера. 

Swag

Swag преобразует аннотации Go в документацию Swagger 2.0. Написано множество плагинов для популярных веб-фреймворков Go. Это позволяет быстро интегрировать swaggo/swag в существующий проект Go (используя Swagger UI).
Поддерживаемые фреймворки:

  • gin
  • echo
  • buffalo
  • net/http

Swagger аннотации делятся на две части, общей информации о документации и документацию endpoint’ов. Ниже приведены их примеры.

Общие аннотации API

// @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() {     // ...  }

Аннотации API-методов

// 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 документацией. Рассмотрели способы проектирования и кода и составления документации к нему.

И я даже думаю, что некоторые уже выбрал для подходящий способ для реализации своего нового проекта или примерили разобранные примеры уже на завершенные проекты.

ссылка на оригинал статьи https://habr.com/ru/post/496098/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *