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

Автор або джерело: Ярослав Характерник

Першоджерело

API Go, tech,

Повна версія

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| @Param| param name| param type| data type| is mandatory?| comment| attribute(optional)
---|---|---|---|---|---|---|---
GET| @Param| page| query| string| false| «Page»|
GET| @Param| size| query| string| false| «Size»|
POST| @Param| body| body| CompaniesNewRequest| true| «CompaniesNewRequest»|
GET, POST| @Param| id| path| integer| true| «id»|
POST| @Param| body| body| CompaniesUpdateRequest| true| «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.json swagger.yaml

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, й на основі файлів специфікації генерувати серверну частину, документацію та клієнт.

  • /docs/examples/001_annotations_only

    • /apidocs

    • docs.go

    • swagger.json
    • swagger.yaml
    • /client/gen

    • /apis

      • index.ts
      • CompaniesApi.ts

        • /models
      • index.ts

      • MainCompaniesNewRequest.ts
      • MainCompaniesUpdateRequest.ts
      • MainCompany.ts
      • MainErrorResponse.ts
      • MainResponseNew.ts
        • index.ts
    • runtime.ts

Реалізація задачі на прикладі вебкаркасу 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

Єдине, що залишилось, це знову згенерувати 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 Developer (Remote)

  • Ryddm — сервіс, який допомагає молодим музикантам зібрати свою аудиторію, стати частиною спільноти та заробляти на своїй творчості.
  • QuickCheckout — це нова система чекауту для власників онлайн-магазинів, яка полегшує процес оформлення замовлення мільйонам покупців з усього світу.

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

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

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

Епілог

  • gitlab.com/go-yp/go-swagger-typescript-client-example
  • gitlab.com/go-yp/go-openapi-crud-example

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

Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU