Go Swagger: автогенерація клієнту та документації

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привіт! За минулий 2021 рік я встиг попрацювати з gRPC, gRPC-Web, OpenAPI (специфікація, початково відома як Swagger) та розібратись з gRPC-Gateway. Було б добре написати окрему статтю з порівнянням цих інструментів, але поки лише можу проконсультувати, бо пояснити особливості усно — це година-дві, а написати статтю з порівнянням — десять-двадцять годин.

А ще за 2021 рік в мене накопились теми, які я б хотів оформити в статті, а тому, якщо у вас є час та бажання пописати технічні статті на тематику Go, то пишіть мені, допоможу вам, це буде гарним вкладом в розвиток спільноти GolangUA. Спеціально для популяризації Go в Україні, Редакція DOU запустила суб’єктивну програму лояльності для авторів технічних статей з Go ПишуНаDOU де будуть дарувати іграшку «Гофер», але це тільки мої суб’єктивні очікування.

Про свої досягнення та фантазії стосовно програм лояльності розповів, тож варто починати технічну статтю.

Стаття буде класичної структури, а саме інструкція: від написання анотацій для API-методів на сервері до генерації документації та генерації клієнту, а в кінці статті буде посилання на репозиторії з кодом та усіма скриптами для запуску генерацій в Makefile.

OpenAPI (Swagger)

На першому проєкті, де я дізнався про Swagger, він використовувався тільки для генерації документації з анотацій до методів API. Це був 2015 рік, я працював з PHP.
Тоді подібна генерація для мене виглядала чимось надзвичайним і протягом довгого часу я сприймав Swagger тільки як генератор документації.

Коли в 2021 році, вже на проєкті з Go, я побачив Swagger знову, то вже мав досвід з gRPC, а тому вирішив пошукати, чи є можливість згенерувати клієнт для браузера, за аналогією з gRPC-Web, як виявилось, можливість є.

Swagger, як і gRPC, можна використовувати для кодогенерації міжсервісної клієнт-серверної взаємодії, хоча на мою думку, використання gRPC — краще рішення для мікросервісів, ніж OpenAPI (Swagger).

А ось для створення MVP (прототипів), де є взаємодія між браузером та сервером, краще OpenAPI. З OpenAPI почати працювати простіше, ніж з gRPC-Gateway. А згенерований OpenAPI клієнт поки має значно менший розмір, ніж клієнт gRPC-Web, який залежить від важкої бібліотеки protobufjs.

Далі в статті будуть зустрічатись обидві назви, сучасна OpenAPI та історична Swagger, за бажанням можете відкрити у фоновій вкладці статтю про OpenAPI (з Вікіпедії) та почитати пізніше.

Опис задачі

Зазвичай, в книгах наводять приклади банківських систем, але ми для прикладу візьмемо відомий для аудиторії DOU, пошук роботи і на його прикладі зробимо API:

  • Створення компаній та вакансій.
  • Перегляд.
  • Редагування.
  • Перегляд списку.

Тестовий запуск генерації документації

У Swagger відсутня залежність від вебкаркасів, а тому протестую генерацію документації без запуску HTTP-сервера.

Для початку потрібно створити проєкт та описати анотації:

mkdir -p ~/go/src/gitlab.com/go-yp/go-swagger-typescript-client-example
cd ~/go/src/gitlab.com/go-yp/go-swagger-typescript-client-example
go mod init
go: creating new go.mod: module gitlab.com/go-yp/go-swagger-typescript-client-example
package main

// Company meta from https://jobs.dou.ua/register/
type Company struct {
	ID            int    `json:"id"`
	Slug          string `json:"slug"`
	Name          string `json:"name"`
	Description   string `json:"description"`
	SiteURL       string `json:"site_url"`
	EmployeeCount int    `json:"employee_count"`
}

type ErrorResponse struct {
	Message string `json:"message"`
}

// Companies godoc
// @Summary      Show companies
// @Tags         Companies
// @Accept       json
// @Produce      json
// @Param        page query string false "Page"
// @Param        size query string false "Size"
// @Param        search query string false "Search by name and description"
// @Success      200  {object}  []Company
// @Failure      400  {object}  ErrorResponse
// @Failure      401  {object}  ErrorResponse
// @Failure      404  {object}  ErrorResponse
// @Failure      500  {object}  ErrorResponse
// @Router       /api/v1/hire/companies [get]
func Companies() {
	// NOP
}

type ResponseNew struct {
	ID int `json:"id"`
}

type CompaniesNewRequest struct {
	Slug          string `json:"slug"`
	Name          string `json:"name"`
	Description   string `json:"description"`
	SiteURL       string `json:"site_url"`
	EmployeeCount int    `json:"employee_count"`
}

// CompaniesNew godoc
// @Summary      Add company
// @Tags         Companies
// @Accept       json
// @Produce      json
// @Param        body body CompaniesNewRequest true "CompaniesNewRequest"
// @Success      200  {object}  ResponseNew
// @Failure      401  {object}  ErrorResponse
// @Failure      404  {object}  ErrorResponse
// @Failure      500  {object}  ErrorResponse
// @Router       /api/v1/hire/companies [post]
func CompaniesNew() {
	// NOP
}

// CompaniesGetByID godoc
// @Summary      Get one company by id
// @Tags         Companies
// @Accept       json
// @Produce      json
// @Param        id path integer true "id"
// @Success      200  {object}  Company
// @Failure      401  {object}  ErrorResponse
// @Failure      404  {object}  ErrorResponse
// @Failure      500  {object}  ErrorResponse
// @Router       /api/v1/hire/companies/{id} [get]
func CompaniesGetByID() {
	// NOP
}

type EmptyResponse = struct{}

type CompaniesUpdateRequest struct {
	Name          string `json:"name"`
	Description   string `json:"description"`
	SiteURL       string `json:"site_url"`
	EmployeeCount int    `json:"employee_count"`
}

// CompaniesUpdate godoc
// @Summary      Update company
// @Tags         Companies
// @Accept       json
// @Produce      json
// @Param        id path integer true "id"
// @Param        body body CompaniesUpdateRequest true "CompaniesUpdateRequest"
// @Success      200  {object}  EmptyResponse
// @Failure      401  {object}  ErrorResponse
// @Failure      404  {object}  ErrorResponse
// @Failure      500  {object}  ErrorResponse
// @Router       /api/v1/hire/companies/{id} [post]
func CompaniesUpdate() {
	// NOP
}

func main() {
	// NOP
}
tree .
├── examples
│   └── 001_annotations_only
│       └── main.go
├── go.mod
├── LICENSE
└── README.md

2 directories, 4 files

Якщо уважно прочитати анотації, то майже у всіх зрозуміле призначення. Єдина анотація, до якої мають бути питання, це @Param.

@Param має формат, розділений пробілами. Приклади які бачили раніше, для кращого розуміння можна представити в табличному виді:

HTTP methods@Paramparam nameparam typedata typeis mandatory?commentattribute(optional)
GET@Parampagequerystringfalse«Page»
GET@Paramsizequerystringfalse«Size»
POST@ParambodybodyCompaniesNewRequesttrue«CompaniesNewRequest»
GET, POST@Paramidpathintegertrue«id»
POST@ParambodybodyCompaniesUpdateRequesttrue«CompaniesUpdateRequest»

Можливі варіанти для Param Type (взяті з документації інструмента, який буду використовувати для генерації документації):

  • query
  • path
  • header
  • body
  • formData

Можливі варіанти для Data Type (взяті з документації інструмента, який буду використовувати для генерації документації):

  • string (string)
  • integer (int, uint, uint32, uint64)
  • number (float32)
  • boolean (bool)
  • user defined struct (CompaniesNewRequest, CompaniesUpdateRequest)

Для генерації документації буду використовувати github.com/swaggo/swag, бо має гарну документацію з прикладами використання для популярних вебкаркасів та додаткову теку example.

Для встановлення та використання swag створив Makefile:

install-swag:
	go get -u github.com/swaggo/swag/cmd/swag

generate-swagger-docs-001:
	swag init -g ./examples/001_annotations_only/main.go --output ./docs/examples/001_annotations_only/apidocs
make install-swag
make generate-swagger-docs-001
2022/01/08 03:15:30 Generate swagger docs....
2022/01/08 03:15:30 Generate general API Info, search dir:./
2022/01/08 03:15:30 Generating main.Company
2022/01/08 03:15:30 Generating main.ErrorResponse
2022/01/08 03:15:30 Generating main.CompaniesNewRequest
2022/01/08 03:15:30 Generating main.ResponseNew
2022/01/08 03:15:30 Generating main.CompaniesUpdateRequest
2022/01/08 03:15:30 Generating main.EmptyResponse
2022/01/08 03:15:30 create docs.go at docs/examples/001_annotations_only/apidocs/docs.go
2022/01/08 03:15:30 create swagger.json at docs/examples/001_annotations_only/apidocs/swagger.json
2022/01/08 03:15:30 create swagger.yaml at docs/examples/001_annotations_only/apidocs/swagger.yaml
tree .
├── docs
│   └── examples
│       └── 001_annotations_only
│           └── apidocs
│               ├── docs.go
│               ├── swagger.json
│               └── swagger.yaml
├── examples
│   └── 001_annotations_only
│       └── main.go
├── go.mod
├── LICENSE
├── Makefile
└── README.md

6 directories, 8 files

Для перегляду документації в браузері достатньо мати swagger.json (або swagger.yaml):

docker run -p 80:8080 \
-e SWAGGER_JSON=/www/swagger.json \
-v ${PWD}/docs/examples/001_annotations_only/apidocs:/www \
swaggerapi/swagger-ui
browse http://localhost
Swagger example
Маючи swagger.json (або swagger.yaml) можна згенерувати TypeScript клієнт:
generate-swagger-ts-client-001:
	docker run --rm \
      -v $(shell pwd)/docs/examples/001_annotations_only:/local openapitools/openapi-generator-cli generate \
      --skip-validate-spec \
      -g typescript-fetch \
      -i /local/apidocs/swagger.json \
      -o /local/client/gen
make generate-swagger-ts-client-001
tree ./docs/examples/001_annotations_only
./docs/examples/001_annotations_only
├── apidocs
│   ├── docs.go
│   ├── swagger.json
│   └── swagger.yaml
└── client
    └── gen
        ├── apis
        │   ├── CompaniesApi.ts
        │   └── index.ts
        ├── index.ts
        ├── models
        │   ├── index.ts
        │   ├── MainCompaniesNewRequest.ts
        │   ├── MainCompaniesUpdateRequest.ts
        │   ├── MainCompany.ts
        │   ├── MainErrorResponse.ts
        │   └── MainResponseNew.ts
        └── runtime.ts

5 directories, 13 files

Якщо ви працювали зі Swagger раніше, то цієї інформації вам має бути достатньо для кодогенерації TypeScript-клієнту.

Список доступних генераторів є в репозиторії організації OpenAPITools openapi-generator/tree/master/docs/generators.

Виведу тільки список генераторів, які стосуються JavaScript, TypeScript та Go:

  • javascript
  • javascript-apollo
  • javascript-closure-angular
  • javascript-flowtyped
  • typescript
  • typescript-node
  • typescript-angular
  • typescript-aurelia
  • typescript-axios
  • typescript-fetch
  • typescript-inversify
  • typescript-jquery
  • typescript-nestjs
  • typescript-redux-query
  • typescript-rxjs
  • go
  • go-echo-server
  • go-gin-server
  • go-server

Отже, працюючи зі Swagger, у вас є варіанти:


  • Перший, який описується в цій статті, писати анотації, на основі яких генерувати файли специфікації swagger.json та swagger.yaml, а далі, на основі файлів специфікації, генерувати документацію та клієнт.

  • Другий: ви можете писати файли специфікації swagger.json та swagger.yaml в улюбленій IDE або через спеціальний редактор editor.swagger.io, й на основі файлів специфікації генерувати серверну частину, документацію та клієнт.
Якщо ви хочете переглянути згенеровані файли, то ось:

Реалізація задачі на прикладі вебкаркасу Gin

Щоб краще розібратись, то пропоную реалізувати заглушки для задачі, яку описав раніше.

Для цього буду використовувати вебкаркас Gin та вже знайомий swag.

В інструмента swag є підтримка популярних каркасів:

  • gin (офіційна підтримка від swaggo);
  • echo (офіційна підтримка від swaggo);
  • buffalo (офіційна підтримка від swaggo);
  • net/http (офіційна підтримка від swaggo);
  • flamingo;
  • fiber;
  • atreugo.

Вибрав Gin, бо ім’я співзвучне з відомим сервісом для пошуку роботи achievki.io, а також, бо сподобалась обробка параметрів, маршрутизація та документація.

План такий: в main.go опишу маршрутизацію, додам API методи та анотації, згенерую документацію та клієнт, ініціалізую npm-проєкт, де підключу згенерований OpenAPI клієнт.

cat main.go
package main

import (
	"github.com/gin-gonic/gin"
	swaggerFiles "github.com/swaggo/files"
	ginSwagger "github.com/swaggo/gin-swagger"
	// @TODO uncomment after run "swag init ..."
	// _ "./docs/examples/002_extends/apidocs" // docs is generated by Swag CLI, you have to import it.
)

func TodoHandlerFunc(ctx *gin.Context) {
	// @TODO
}

func TodoAuthMiddleware(ctx *gin.Context) {
	// @TODO
}

func main() {
	r := gin.Default()

	// employer
	r.
		Use(TodoAuthMiddleware).
		GET("/api/v1/hire/companies", TodoHandlerFunc).              // get list
		POST("/api/v1/hire/companies", TodoHandlerFunc).             // create one
		GET("/api/v1/hire/companies/:company_id", TodoHandlerFunc).  // get one
		POST("/api/v1/hire/companies/:company_id", TodoHandlerFunc). // update one

		Use(TodoAuthMiddleware).
		GET("/api/v1/hire/companies/:company_id/vacancies", TodoHandlerFunc).             // get list
		POST("/api/v1/hire/companies/:company_id/vacancies", TodoHandlerFunc).            // create one
		GET("/api/v1/hire/companies/:company_id/vacancies/:vacancy_id", TodoHandlerFunc). // get one
		POST("/api/v1/hire/companies/:company_id/vacancies/:vacancy_id", TodoHandlerFunc) // update one

	// employee
	r.
		GET("/api/v1/companies", TodoHandlerFunc). // get list
		GET("/api/v1/vacancies", TodoHandlerFunc). // get list

		StaticFile("/vacancies", "./public/vacancies.html"). // view page
		StaticFile("/companies", "./public/companies.html")  // view page

	if gin.IsDebugging() {
		// use ginSwagger middleware to serve the API docs
		r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
	}

	r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
go run main.go
[GIN-debug] Listening and serving HTTP on :8080

Я буду рухатись такими ж кроками, якими зазвичай розробляю в комфортному темпі (ці кроки маленькі). Опишу контролери та використаю їх в файлі main.go:

tree ./controllers/
./controllers/
├── employee.go
└── employer.go

0 directories, 2 files
cat ./controllers/employer.go ./controllers/employer.go/
package controllers

import (
	"database/sql"
	"github.com/gin-gonic/gin"
)

type EmployerController struct {
	connection *sql.DB
}

func NewEmployerController(connection *sql.DB) *EmployerController {
	return &EmployerController{connection: connection}
}

func (c *EmployerController) Companies(ctx *gin.Context)       {}
func (c *EmployerController) CompaniesNew(ctx *gin.Context)    {}
func (c *EmployerController) CompaniesGetOne(ctx *gin.Context) {}
func (c *EmployerController) CompaniesUpdate(ctx *gin.Context) {}

func (c *EmployerController) Vacancies(ctx *gin.Context)       {}
func (c *EmployerController) VacanciesNew(ctx *gin.Context)    {}
func (c *EmployerController) VacanciesGetOne(ctx *gin.Context) {}
func (c *EmployerController) VacanciesUpdate(ctx *gin.Context) {}
package controllers

import (
	"database/sql"
	"github.com/gin-gonic/gin"
)

type EmployeeController struct {
	connection *sql.DB
}

func NewEmployeeController(connection *sql.DB) *EmployeeController {
	return &EmployeeController{connection: connection}
}

func (c *EmployeeController) Companies(ctx *gin.Context) {}
func (c *EmployeeController) Vacancies(ctx *gin.Context) {}
cat main.go
package main

import (
	"database/sql"
	"github.com/gin-gonic/gin"
	swaggerFiles "github.com/swaggo/files"
	ginSwagger "github.com/swaggo/gin-swagger"
	"gitlab.com/go-yp/go-swagger-typescript-client-example/controllers"
	"gitlab.com/go-yp/go-swagger-typescript-client-example/middlewares"
	// @TODO uncomment after run "swag init ..."
	// "./docs/examples/002_extends/apidocs" // docs is generated by Swag CLI, you have to import it.
)

func main() {
	var connection *sql.DB = nil // @TODO
	defer func() {
		if connection != nil {
			connection.Close()
		}
	}()

	var (
		erController = controllers.NewEmployerController(connection)
		eeController = controllers.NewEmployeeController(connection)
	)

	r := gin.Default()

	// employer
	r.
		Use(middlewares.Auth).
		GET("/api/v1/hire/companies", erController.Companies).                    // get list
		POST("/api/v1/hire/companies", erController.CompaniesNew).                // create one
		GET("/api/v1/hire/companies/:company_id", erController.CompaniesGetOne).  // get one
		POST("/api/v1/hire/companies/:company_id", erController.CompaniesUpdate). // update one

		Use(middlewares.Auth).
		GET("/api/v1/hire/companies/:company_id/vacancies", erController.Vacancies).                   // get list
		POST("/api/v1/hire/companies/:company_id/vacancies", erController.VacanciesNew).               // create one
		GET("/api/v1/hire/companies/:company_id/vacancies/:vacancy_id", erController.VacanciesGetOne). // get one
		POST("/api/v1/hire/companies/:company_id/vacancies/:vacancy_id", erController.VacanciesUpdate) // update one

	// employee
	r.
		GET("/api/v1/companies", eeController.Companies). // get list
		GET("/api/v1/vacancies", eeController.Vacancies). // get list

		StaticFile("/vacancies", "./public/vacancies.html"). // view page
		StaticFile("/companies", "./public/companies.html")  // view page

	if gin.IsDebugging() {
		// use ginSwagger middleware to serve the API docs
		r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
	}

	r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
go run main.go
[GIN-debug] Listening and serving HTTP on :8080

Додам анотації до усіх API методів, можете переглянути за посиланнями /controllers/employer.go та /controllers/employee.go, але в статті наведу тільки парочку, бо об’ємні й повторюють анотації з попередніх прикладів.

package controllers

import (
	"database/sql"
	"github.com/gin-gonic/gin"
	"gitlab.com/go-yp/go-swagger-typescript-client-example/middlewares"
	"gitlab.com/go-yp/go-swagger-typescript-client-example/models"
	"net/http"
)

// ...

type EmployerCompaniesQuery struct {
	Page string `form:"page"`
	Size string `form:"size"`
}

// Companies godoc
// @Summary      Show companies
// @Tags         Employer, Companies
// @Accept       json
// @Produce      json
// @Param        page query string false "Page"
// @Param        size query string false "Size"
// @Success      200  {object}  []models.Company
// @Failure      400  {object}  models.ErrorResponse
// @Failure      401  {object}  models.ErrorResponse
// @Failure      500  {object}  models.ErrorResponse
// @Router       /api/v1/hire/companies [get]
func (c *EmployerController) Companies(ctx *gin.Context) {
	var user, ok = middlewares.User(ctx)

	_ = user
	_ = ok

	if false && !ok {
		ctx.JSON(http.StatusUnauthorized, &models.ErrorResponse{
			Message: "Unauthorized",
		})

		return
	}

	var query EmployerCompaniesQuery
	if err := ctx.ShouldBindQuery(&query); err != nil {
		ctx.JSON(http.StatusBadRequest, &models.ErrorResponse{
			Message: err.Error(),
		})

		return
	}

	_ = query.Page
	_ = query.Size

	// ... logic

	ctx.JSON(http.StatusInternalServerError, &models.ErrorResponse{
		Message: "Unimplemented",
	})
}

// ...

type VacancyURI struct {
	CompanyID int `uri:"company_id" binding:"required"`
	VacancyID int `uri:"vacancy_id" binding:"required"`
}

// VacanciesUpdate godoc
// @Summary      Update vacancy
// @Tags         Employer, Vacancies
// @Accept       json
// @Produce      json
// @Param        company_id path integer true "Company ID"
// @Param        vacancy_id path integer true "Vacancy ID"
// @Param        body body models.VacanciesUpdateRequest true "VacanciesUpdateRequest"
// @Success      200  {object}  models.Vacancy
// @Failure      400  {object}  models.ErrorResponse
// @Failure      401  {object}  models.ErrorResponse
// @Failure      404  {object}  models.ErrorResponse
// @Failure      500  {object}  models.ErrorResponse
// @Router       /api/v1/hire/companies/{company_id}/vacancies/{vacancy_id} [post]
func (c *EmployerController) VacanciesUpdate(ctx *gin.Context) {
	var user, ok = middlewares.User(ctx)

	_ = user
	_ = ok

	if false && !ok {
		ctx.JSON(http.StatusUnauthorized, &models.ErrorResponse{
			Message: "Unauthorized",
		})

		return
	}

	var uri VacancyURI
	if err := ctx.ShouldBindUri(&uri); err != nil {
		ctx.JSON(http.StatusBadRequest, &models.ErrorResponse{
			Message: err.Error(),
		})

		return
	}

	_ = uri.CompanyID
	_ = uri.VacancyID

	var vacancyRequest models.VacanciesUpdateRequest
	if err := ctx.ShouldBindJSON(&vacancyRequest); err != nil {
		ctx.JSON(http.StatusBadRequest, &models.ErrorResponse{
			Message: err.Error(),
		})

		return
	}

	// ... logic

	ctx.JSON(http.StatusInternalServerError, &models.ErrorResponse{
		Message: "Unimplemented",
	})
}

Тепер, коли вже є анотації, то можемо згенерувати swagger.json, swagger.yaml та docs.go.

generate-swagger-docs-002:
	swag init -g main.go --output ./docs/examples/002_extends/apidocs --exclude ./examples/001_annotations_only
make generate-swagger-docs-002
tree ./docs/examples/002_extends/apidocs
./docs/examples/002_extends/apidocs
├── docs.go
├── swagger.json
└── swagger.yaml

0 directories, 3 files

Щоб документація стала доступною при запуску Go серверу, потрібно підключити файл docs.go в main.go:

import (
	// ...
	_ "gitlab.com/go-yp/go-swagger-typescript-client-example/docs/examples/002_extends/apidocs" // docs is generated by Swag CLI, you have to import it.
	// ...
)
package main

import (
	"database/sql"
	"github.com/gin-gonic/gin"
	swaggerFiles "github.com/swaggo/files"
	ginSwagger "github.com/swaggo/gin-swagger"
	"gitlab.com/go-yp/go-swagger-typescript-client-example/controllers"
	_ "gitlab.com/go-yp/go-swagger-typescript-client-example/docs/examples/002_extends/apidocs" // docs is generated by Swag CLI, you have to import it.
	"gitlab.com/go-yp/go-swagger-typescript-client-example/middlewares"
)

func main() {
	// ... same

	r := gin.Default()

	// employer
	// ... same

	// employee
	// ... same

	if gin.IsDebugging() {
		// use ginSwagger middleware to serve the API docs
		r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
	}

	r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
go run main.go
[GIN-debug] Listening and serving HTTP on :8080
browse http://localhost:8080/swagger/index.html

Swagger example

Єдине, що залишилось, це знову згенерувати TypeScript клієнт й написати приклад його використання:

generate-swagger-ts-client-002:
	# https://openapi-generator.tech/docs/installation/
	# openapi-generator-cli generate -i ./apidocs/swagger.yaml --generator-name typescript-fetch -o gen/api
	docker run --rm \
      -v $(shell pwd):/local openapitools/openapi-generator-cli generate \
      --skip-validate-spec \
      -g typescript-fetch \
      -i /local/docs/examples/002_extends/apidocs/swagger.json \
      -o /local/client/gen
make generate-swagger-ts-client-002
tree ./client/ -h
./client/
└── [4.0K]  gen
    ├── [4.0K]  apis
    │   ├── [8.2K]  CompaniesApi.ts
    │   ├── [3.3K]  EmployeeApi.ts
    │   ├── [ 15K]  EmployerApi.ts
    │   ├── [ 168]  index.ts
    │   └── [9.7K]  VacanciesApi.ts
    ├── [ 119]  index.ts
    ├── [4.0K]  models
    │   ├── [ 459]  index.ts
    │   ├── [2.3K]  ModelsCompaniesNewRequest.ts
    │   ├── [2.1K]  ModelsCompaniesUpdateRequest.ts
    │   ├── [2.3K]  ModelsCompany.ts
    │   ├── [1.8K]  ModelsEmployeeVacancyCompany.ts
    │   ├── [2.3K]  ModelsEmployeeVacancy.ts
    │   ├── [1.3K]  ModelsErrorResponse.ts
    │   ├── [1.3K]  ModelsResponseNew.ts
    │   ├── [1.6K]  ModelsVacanciesNewRequest.ts
    │   ├── [1.6K]  ModelsVacanciesUpdateRequest.ts
    │   └── [1.6K]  ModelsVacancy.ts
    └── [ 10K]  runtime.ts

3 directories, 18 files

Для мене було в новинку, що клієнти згрупувались по тегам:

  • CompaniesApi.ts
  • EmployeeApi.ts
  • EmployerApi.ts
  • VacanciesApi.ts

Тепер напишу два тестових приклади, де підключу ці клієнти, один для роботи з компаніями, а другий для роботи з вакансіями:

cat ./client/companies-app.ts
import {CompaniesApi, Configuration, ModelsCompany} from "./gen";

const companiesApiClient = new CompaniesApi(new Configuration({
    basePath: "http://localhost:8080",
}));

companiesApiClient.apiV1CompaniesGet()
    .then(function (companies: ModelsCompany[]) {
        for (const company of companies) {
            console.log(
                company.id,
                company.slug,
                company.name,
                company.description,
                company.siteUrl,
                company.employeeCount,
            );
        }
    })
    .catch(console.error)
cat ./client/vacancies-app.ts
import {VacanciesApi, Configuration, ModelsEmployeeVacancy} from "./gen";

const vacanciesApiClient = new VacanciesApi(new Configuration({
    basePath: "http://localhost:8080",
}));

vacanciesApiClient.apiV1VacanciesGet()
    .then(function (vacancies: ModelsEmployeeVacancy[]) {
        for (const vacancy of vacancies) {
            console.log(
                vacancy.id,
                vacancy.title,
                vacancy.description,

                vacancy.company.id,
                vacancy.company.slug,
                vacancy.company.name,
            );
        }
    })
    .catch(console.error)

Тепер у нас є повноцінний клієнт з усіма моделями.

Особливості swag або читайте документацію

У swag — чудова документація, основні можливості описані в CLI, але якщо будете використовувати в розробці, то прочитайте весь файл README.

По аналогії з go fmt, є swag fmt для форматування анотацій.

Також варто звернути увагу, коли в анотаціях використовуються типи зі стандартної бібліотеки Go, на зразок json.Number чи json.RawMessage, тоді потрібний флаг swag init —parseInternal, а якщо тип з залежностей на зразок gin.H{}, тоді swag init —parseDependency. Якщо генерація відбувається занадто довго, то ймовірно, вам треба налаштувати флаг swag init —parseDepth або ж додати частину тек в swag init —exclude.

У статті наводиться генерація клієнту через docker openapitools/openapi-generator-cli generate, але є й інші, приклади через npm i @openapitools/openapi-generator-cli.

Звісно, у OpenAPI є мінуси. Один з низ: після внесення змін до коду, анотації можуть забути оновити, й тоді документація й клієнт будуть відставати.

Go вакансії з OpenAPI для початківців

У компанія Softcery є вакансія для Go розробника, який має досвід розробки серверної частини на Go, хоча б у pet-проєктах.
Мені сподобалась їх вакансія Go Developer (Remote) бо вказана винагорода та є опис проєктів:
  • Ryddm — сервіс, який допомагає молодим музикантам зібрати свою аудиторію, стати частиною спільноти та заробляти на своїй творчості.
  • QuickCheckout — це нова система чекауту для власників онлайн-магазинів, яка полегшує процес оформлення замовлення мільйонам покупців з усього світу.

Також у вакансії описаний процес найму та відпустка.

Компанія Softcery фінансово підтримала мою мотивацію написати статтю про OpenAPI, яку ще спланував у 2021 році, бо Softcery також у проєктах генерують TypeScript клієнт.

Ви можете відгукнутись на вакансію на сторінці вакансії, через форму на сайті careers.softcery.com, познайомитись через calendly.com/softcery/introduction або написати напряму в LinkedIn Elijah Atamas.

Епілог

Маю надію, що вам вдалось осилити статтю, і вона буде вам корисною. Репозиторії:

Якщо ви осилили «Тестовий запуск генерації документації», то цього достатньо.

👍ПодобаєтьсяСподобалось10
До обраногоВ обраному8
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Дякую за статтю, цікаво!
Теж пишу на Go (хоч і недовго), і дозволю додати від себе кілька моментів:
— свого часу мені також доводилось генерувати сваггер док, але я робив це іншим способом — через параметр в protoc.
спочатку додавав новий option в service.proto, а потім генерив командою
 protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative \  // some code --openapiv2_out=doc/swagger --openapiv2_opt=allow_merge=true,merge_file_name=foobar 
цей спосіб не вимагає додавання анотацій на функції.

також знаю про лібу, яка вбудовує статичні файли в бінарник Го — github.com/rakyll/statik
бачив десь, як з використанням цієї ліби генерили бінарник з веб-інтерфейсом сваггера так, що не потрібно було ніякого TS чи чого іншого

Вже є go:embed.

Стосовно grpc-gateway, вважаю то ви його описували з openapiv2_opt, то також є бажання про grpc-gateway написати в майбутньому.

також знаю про лібу, яка вбудовує статичні файли в бінарник Го

Я пользуюсь способом получше — сшиваю все файлы в один большой файл, который может занимать любой объём, и дальше можно открывать вложенные файлы вот через эту либу:
github.com/schwarzlichtbezirk/wpk
Все стандартные интерфейсы там поддерживаются, архив можно открыть как memory-mapped, если его объём сопоставим с размерами оперативной памяти. Если архив слишком большой — читать из файла, если мелкий — можно просто загрузить в виде слайса.

Спасибо за статью.
Если нужно иметь единый источник правды для ваших апи (с генерацией клиента, сервера для http/grpc) можно посмотреть в сторону goa.design

Очень толковая статья, спасибо !

Привіт! За минулий 2021 рік я встиг попрацювати з gRPC, gRPC-Web, OpenAPI (специфікація, початково відома як Swagger) та розібратись з gRPC-Gateway.

Здорово! Я сейчас как раз тоже пишу код под github.com/grpc-ecosystem/grpc-gateway, очень удобная библиотека! Ну и конечно там есть генерация swagger.json, через плагин protoc-gen-openapiv2, я думал что ты именно об этом напишешь.

Вообще в grpc-gateway есть что покритиковать. Роутинг там довольно примитивный. Если раньше юзали gorilla-mux роутер, то многие возможности которые там есть, через гейтвей реализовать не выйдет. Нельзя например хендлить по контенту http-headers. Нельзя повесить middleware на хендлеры. Да и вообще, не дают управление соединением. Чтоб делать gRPC-запросы, нужно создавать своё собственное соединение, и от него клиента, хотя клиент создаётся внутри генерируемого кода. То есть, чтоб реализовать это, нужно скопировать сгенерированный код и модифицировать под своё усмотрение. Ну а вообще в целом либа очень удобная, можно описывать gRPC & REST API в одном proto-файле.

Почав зі Swagger, щоб в наступних статтях посилатись.

Якщо буду писати про gRPC-Gateway то звернусь, щоб переглянули перед публікацією.

Боюся що пан автор неправильно розуміє ідєю OpenApi. В статі описано як згенерувати документацію Swagger з анотацій, це давно вже відомо для інших мов. Просто інформацію з анотацій, не найзручишому форматі, до речі, переводимо в більш красиву форму.
Наявні два джерела інформації, неминуче документація розходится з кодом і виникають супутні проблеми, що вже описано в коментарях. А ось власне OpenApi це створеня опису АПИ в спеціальному форматі з метою генерації з ЄДИНОГО ДЖЕРЕЛА, крім доків, коду бібліотек, ручних і авто тестів тощо. Крім того можна OpenApi використовувати для всіляких подальших автоматизацій, для генерації визначень інтерфейсів (d.ts), DTO , контракт-тестів...https://rapidapi.com/blog/api-glossary/openapi/

Описував це у статті:

Отже, працюючи зі Swagger, у вас є варіанти:
  • Перший, який описується в цій статті, писати анотації, на основі яких генерувати файли специфікації swagger.json та swagger.yaml, а далі, на основі файлів специфікації, генерувати документацію та клієнт.
  • Другий: ви можете писати файли специфікації swagger.json та swagger.yaml в улюбленій IDE або через спеціальний редактор editor.swagger.io, й на основі файлів специфікації генерувати серверну частину, документацію та клієнт.
Звісно, у OpenAPI є мінуси. Один з них: після внесення змін до коду, анотації можуть забути оновити, й тоді документація й клієнт будуть відставати.

Так ви це загадали, але сенс цього всього openAPI — мати єдине джерело.
Якщо потік трохи зминити — а саме генерувати автотести (це можливо і із аннтоцій, але якось треба зберегати версійність) але краще YML або JSON( в стандарті openAPI версії передбачені) то проблема рассинхронізації кода і доків буде вирішена.
Ієрархія знань приблизно така:

Першоджерело(YML або JSON) -> generated autotests + generated docs ->(generated?) code

Таким чином коли, наприклад, змієються щось описі в АПІ, падають тести на сервері і клієнтах. Всі нещасні, всі оновлюють код відподно до документації і тестів, нова версія АПІ в усіх працює, всі щасливі. Звичайно, мова іде про якусь dev-гілку.
Те що воно в машино-читаємому форматі (а не конфлюєнс там якийсь) дає можливіть обробляти його скриптами, яких вже купа написана. На кшталт, вивести в браузері різними кольорами різницу двух джсонів, про що Олександр пише — задача для джуна на співбесіду ))

По генерації тестів ще менше інформації, ніж по клієнту, ймовірно багатьом лінь з цим возитись.

Показує всю маразматичність RESTу як протоколу «для всього». Коли кожна версія навіть із найдрібнішими змінами вимагає перепровірити буквально все, і жодних гарантій, що обгортки не прогавлять зміну. Не такий страшний REST, як факт, що для нього потрібно генерувати обгортки, які, як не крути, будуть мати прошарок, що складно піддається тестуванню.

Іншими словами, знадобиться вагон тестів виключно на те, як дані проходять через REST тонель. Саме тому, що він є генерованим автоматично, тобто «чорним ящиком». І як здогадуєтесь, протестувати автоматично абсолютно все неможливо, особливо те, що додається, чого раніше не було — а про це ви дізнаєтесь вже на продакшені.

Чим пропонуєш користуватись?

Головою. Все, що може дати автоматизація — це сміття по суті. Але це скоріше недолік самого RESTу як ідеї натягування сови на глобус.

Проблема сміття у тому, що ресурси мозку дуже обмежені, і якщо його забагато — то код стає нечитабельним, і це згубило просто таки мільйони гарних проектів і проектиків. Просто факт, що згенерований матеріал вимагає від мозку невластивої йому логіки, робить його генератором помилок людиною при спробі щось міняти, в кращому випадку призводить до прийняття рішення нічого не торкатися, доки робе. А найчастіше — призводить до смерті проекту.

Проблему можна частково вирішити, створивши гарну документацію. Але частково. Бо сама проблема полягає не лише у відсутності інфи, скоріше у її «нормальній формі», яка гарантує, що одна й та сама інфа не повторюватиметься в кількох місцях.

Проблема Swagger — саме у тому, що він все робить «з нуля». Тобто, якщо додалися якісь зміни (можливо помилково), буде тупо згенерована нова документація. А як ти пропонуєш зробити з людиною, яка вже прочитала стару версію? Пересадити мозок? Вибач, але кнопки збросу кешу в ньому не передбачено. Так само, як і в коді, який вже залежить від API.

Як результат, для повної сумісності доведеться робити версіалізацію API та не тільки генерувати все повністю та тримати на кожен чих по 100500 версій піднятими (уяви лише цю систему та її гальмуватість), але ще й шукати СПОСІБ, яким треба буде розуміти яка версія до чого, які вже можна ліквідувати, і так далі, і тому подібне.

Якщо коротко — REST api не є гнучким, а Swagger доводить цей недолік до маразму. Можна вбити не один тиждень, намагаючись розібратися в тому, що насправді робиться. Той самий випадок, коли за деревами не видно лісу.

Проблема не в ідеї організації інфи саме так. Проблема в людях, які такий продукт не можуть перетравити. Для людей така компоновка є отруйною для пам’яті, тобто читаючи такий «продукт» не просто не розбереся в ньому самому, а ще й витравиш левову частину того, що пам’ятав. В людей немає бекапу пам’яті. Якщо ти створюєш документацію, яка вимагає механізму garbage collector через вимогу читати інфу, яка в даний момент не потрібна, з метою відшукати те, чого там нема (хлібні крихти, вибору на основі порівняння, логічного сортування) — ти маєш знати, як саме в людей цей механізм працює. Це деградація на основі невизначеності, допустимості довільного вибору.

Коли треба сприйняти інфу, яка в даний момент НЕ потрібна, з тим щоб просто відшукати те, що потрібно, і ЛИШЕ ТОДІ забути зайву — механізм забування зачепить усе. Не тільки конкретну область, а ще й шлях до неї в пам′яті буде помічений як хибний, тобто кожна наступна спроба зрозуміти буде тяжча за попередню, і процес йде по експоненті. Тому проект вмирає не тільки на стороні потенційного користувача, а навіть автору з роками все складніше його зрозуміти.

Така ціна мікросервісної архітектури: сама архітектура має бути побудована і документована одночасно і в коді, і за межами коду, і все це з колосальною надлишковістю інфи, і все це потрібно буде контролювати. А якщо так — то навіщо робити вбивчий варіант? Я скажу, навіщо. Коли тобі відверто байдуже, що проект помре. Бо формально ти все зробив правильно. А реально — виніс смертний вирок проекту.

Можна більше деталей? На що ви пропонуєте натягнути сову замість глобуса?

Дякую за статтю. Swagger дуже корисний інструмент на мою думку як для тестування, так і для інтеграції для любого проєкту. Правда я його використав тільки для Erlang/OTP проєктів. Але я вже неодноразово бачив як інтеграція з клієнтом і сервером, а також, тестування відбувається в кілька разів швидше за наявності Swagger у пропроєкті. Правда для версій 2.х є одна проблема — github.com/...​pi/swagger-ui/issues/3072 — коли при запиті у відповідь не відображається header та body. Але в 3.х таких проблем вже немає.

Підписатись на коментарі