[GO] [OSS] api_gen released

Introduction

--The generator that automatically generates from the Go structure described in the specified format has reached v1.0.0, so the output times (v1.6.1 as of 12/31) --There are two types: server_generator that generates a controller and client_generator that generates TypeScript for API communication. --Basically just write the structure -(beta) Since godoc corresponding to swaggo is also inserted in the generated controller to some extent, Swagger code can be automatically generated if adjusted. -Repository (go-generalize/api_gen)


For such people

-I want to automatically generate an API controller/client with ** Go Code Justice ** ――It is troublesome to match the back-end and front-end I/F between engineers. --It's hard to write routing on the back end and API request on the front end for each implementation

Suitable for such people


Try it for the time being

Assumption: I want you to be in the here directory ~~ (miscellaneous) ~~

Drop each binary

$ make bootstrap

Front-end code generation

$ cd frontend && make && cd -

Backend side code generation

$ cd backend && make && cd -

(By the way, cd - returns to the previous directory / git checkout - returns to the previous branch)

That's it for generation Success if you can confirm that various codes are generated under backend/and frontend / Details will be described later


Server generator

Contents included in server_generator
Structure name rule
Corresponding HTTP method
When doing path routing

--The directory starts with _


Run

$ server_generator ./hoge/

mock option (see below)

$ server_generator -mock ./hoge/

Server generator usage example

Source code used this time
This configuration
$ tree backend/interfaces
backend/interfaces
├── api
│   ├── user
│   │   ├── 0_user_id.go
│   │   ├── _userID
│   │   │   └── age_increment.go
│   │   └── search.go
│   └── user.go
└── health_check.go

3 directories, 5 files

GET /health_check

interfaces/health_check.go


type GetHealthCheckRequest struct{}

type GetHealthCheckResponse struct {
	Status string `json:"status"`
}

PUT /api/user

interfaces/api/user.go


type PutUserRequest struct {
	Name   string       `json:"name" validate:"required,min=3,max=10,excludesall=!()#@{}"`
	Age    int          `json:"age" validate:"required,gt=0,lte=150"`
	Gender model.Gender `json:"gender" validate:"required,oneof=1 2 3"`
}

type PutUserResponse struct {
	Status int         `json:"status"`
	User   *model.User `json:"payload,omitempty"`
}

GET /api/user/search

interfaces/api/user/search.go


type GetSearchRequest struct {
	Name   string       `json:"name" query:"name"`
	Age    int          `json:"age" query:"age"`
	Gender model.Gender `json:"gender" query:"gender"`
}

type GetSearchResponse struct {
	Status   int                   `json:"status"`
	User     []*model.User         `json:"payload,omitempty"`
	Messages []werror.FailedReason `json:"messages"`
}

GET/PATCH/DELETE /api/user/:userID

interfaces/api/user/0_user_id.go


type GetRequest struct {
	ID string `json:"userID" param:"userID" validate:"required"`
}

type GetResponse struct {
	Status   int                   `json:"status"`
	User     *model.User           `json:"payload,omitempty"`
	Messages []werror.FailedReason `json:"messages"`
}

type PatchRequest struct {
	ID     string       `json:"userID" param:"userID" validate:"required"`
	Name   string       `json:"name,omitempty" validate:"min=5,max=10,excludesall=!()#@{}"`
	Age    int          `json:"age,omitempty" validate:"gt=0,lte=150"`
	Gender model.Gender `json:"gender,omitempty" validate:"oneof=1 2 3"`
}

type PatchResponse struct {
	Status   int                   `json:"status"`
	User     *model.User           `json:"payload,omitempty"`
	Messages []werror.FailedReason `json:"messages"`
}

type DeleteRequest struct {
	ID string `json:"userID" param:"userID" validate:"required"`
}

type DeleteResponse struct {
	Status   int                   `json:"status"`
	Messages []werror.FailedReason `json:"messages"`
}

POST /api/user/:userID/age_increment

interfaces/api/user/_userID/age_increment.go


type PostAgeIncrementRequest struct {
	ID string `json:"userID" param:"userID" validate:"required"`
}

type PostAgeIncrementResponse struct {
	Status   int                   `json:"status"`
	User     *model.User           `json:"payload,omitempty"`
	Messages []werror.FailedReason `json:"messages"`
}

Generate

$ make server_generate
`After generation tree`
$ tree backend/interfaces
backend/interfaces
├── api
│   ├── put_user_controller_gen.go
│   ├── routes_gen.go
│   ├── user
│   │   ├── 0_user_id.go
│   │   ├── _userID
│   │   │   ├── age_increment.go
│   │   │   ├── post_age_increment_controller_gen.go
│   │   │   └── routes_gen.go
│   │   ├── delete_controller_gen.go
│   │   ├── get_controller_gen.go
│   │   ├── get_search_controller_gen.go
│   │   ├── patch_controller_gen.go
│   │   ├── routes_gen.go
│   │   └── search.go
│   └── user.go
├── bootstrap_gen.go
├── get_health_check_controller_gen.go
├── health_check.go
├── props
│   └── controller_props.go
├── routes_gen.go
└── wrapper
    ├── internal
    │   └── fmt.go
    └── wrapper.go

6 directories, 20 files

Generation controller (part)

interfaces/api/user/_userID/post_age_increment_controller_gen.go


// Package _userID ...
// generated version: 1.6.1
package _userID

import (
	"github.com/54m/api_gen-example/backend/interfaces/props"
	"github.com/labstack/echo/v4"
)

// PostAgeIncrementController ...
type PostAgeIncrementController struct {
	*props.ControllerProps
}

// NewPostAgeIncrementController ...
func NewPostAgeIncrementController(cp *props.ControllerProps) *PostAgeIncrementController {
	p := &PostAgeIncrementController{
		ControllerProps: cp,
	}
	return p
}

// PostAgeIncrement ...
// @Summary WIP
// @Description WIP
// @Accept json
// @Produce json
// @Param userID path string true "user id"
// @Success 200 {object} PostAgeIncrementResponse
// @Failure 400 {object} wrapper.APIError
// @Failure 500 {object} wrapper.APIError
// @Router /api/user/{userID}/age_increment [POST]
func (p *PostAgeIncrementController) PostAgeIncrement(
	c echo.Context, req *PostAgeIncrementRequest,
) (res *PostAgeIncrementResponse, err error) {
	// API Error Usage: github.com/54m/api_gen-example/backend/interfaces/wrapper
	//
	// return nil, wrapper.NewAPIError(http.StatusBadRequest)
	//
	// return nil, wrapper.NewAPIError(http.StatusBadRequest).SetError(err)
	//
	// body := map[string]interface{}{
	// 	"code": http.StatusBadRequest,
	// 	"message": "invalid request parameter.",
	// }
	// return nil, wrapper.NewAPIError(http.StatusBadRequest, body).SetError(err)
	panic("require implements.") // FIXME require implements.
}

Eventually this is what happened


Client generator

Contents included in client_generator

--A library using Typescript + fetch is generated --Execute for the prepared folder for server_generator


After generation

$ tree frontend/client_generated 
frontend/client_generated
├── api_client.ts
└── classes
    ├── api
    │   ├── types.ts
    │   └── user
    │       ├── _userID
    │       │   └── types.ts
    │       └── types.ts
    └── types.ts

4 directories, 5 files

Client generator usage example

import { APIClient } from "./client_generated/api_client";

// the simplest
(async () => {
    const client = new APIClient();

    const resp = await client.getHealthCheck(/* param */ {});

    console.log(resp);
})();

// with options
(async () => {
    const client = new APIClient(
        /* token */
        "pass", // [optional] token for Authorization: Bearer
        /* commonHeaders */
        {
            "X-Foobar": "foobar", // [optional] custom headers
        },
        /* baseURL */
        "http://localhost:8888",  // [optional] custom endpoint
        /* commonOptions */
        {
            cache: "no-cache", // [optional] options for fetch API
        },
    );

    const resp = await client.api.putUser(
        /* param */ 
        {
            name: "54m",
            age: 100,
            gender: 1,
        },
        /* header */
        {
            "X-Foobar": "hoge", // [optional] custom headers
        },
        /* options */
        {
            mode: "cors" // [optional] options for fetch API
        },
    );

    console.log(resp);
})();

Frequently Asked Questions

Q. For the time being, the Req/Res structure has been created and it has become possible to communicate between the server and the client, but when there is no backend implementation, you have to wait for the development on the front side, right?

A. That's not the case A mock server is prepared for api_gen, and if used correctly, the back end and front end can be developed in parallel.

The sample prepared this time is treated as follows

`server`
$ make server_generate_with_mock

What did you do

$ server_generator -mock ./interfaces

Just add -mock to the argument like this

mock_jsons/
mock_routes_gen.go
mock_bootstrap_gen.go
backend/interfaces/cmd/mock/main.go

These files are additionally automatically generated

How to run mock server
$ make run_mock

The mock server is directly under the directory to be generated Generated as cmd/mock/main.go Build options to start the mock server Must have -tags mock

go run -tags mock backend/interfaces/cmd/mock/main.go -port 8888
`client`
import { APIClient, MockOption } from "./client_generated/api_client";

// mock mode
(async () => {
    const client = new APIClient();
    const mockOption: MockOption = {
        wait_ms: 10,
        target_file: 'default.json'
    }

    const resp = await client.api.user.getSearch(/* param */ {
        name: "54m",
        age: 100,
        gender: 1,
    }, /* header */ undefined, /* options */ {
        'mock_option': mockOption,
    });

    console.log(resp);
})();

Documents about mock server (server) / Documents about mock server (client)


Q. I want to send a cookie

A. Set {credentials:" include "} as an option in the fetch API (FYI)


Summary

I was happy just because the API I/F was automatically generated (?) It's still under development, so we are waiting for Issue / PR!

Recommended Posts

[OSS] api_gen released
Line talk analysis with janome (OSS released)