gin + oapi-codegen + Docker で構築するAPIサーバ

## 本記事の目的
- oapi-codegenを使ってgin APIサーバ用のgo言語コードを生成する方法を学ぶ
- [oapi-codegenの公式サンプルコード](https://github.com/oapi-codegen/oapi-codegen/tree/main/examples/petstore-expanded)を必要最低限に単純化し，構成の本質的部分をハイライトする
- gin + oapi-codegenの実行環境をDockerで構築する

## 技術要素
- gin：GO言語ベースのWebフレームワーク．ルーティングやエラーハンドリング等のWebサーバとして必要な機能を備え，軽量なWebアプリを構築可能．APIサーバとして活用したり，静的ファイルのホスティングも可能
- oapi-codegen：OpenAPIのAPI仕様書から各種フレームワーク用のコードを生成できるジェネレータ．gin以外にもEchoなど主要なフレームワークに対応

## 構築物のディレクトリ構成のイメージ ※自動生成されるファイル等一部省略
完成物は[こちらのリポジトリ](https://github.com/h-masano/gin-api-sample)参照

``` sh
.
├─ api
|  └─...               #APIの実態のコードを配置
├─ config.yaml         #oapi-codegen用の設定ファイル
├─ docker-compose.yaml #コンテナベースで開発したい場合に必要
├─ Dockerfile          #コンテナベースで開発したい場合に必要
├─ generate.go
├─ Makefile
└─ openapi.yaml        #OpenAPIのAPI仕様定義 
```

## 構築手順

### OpenAPIのコード仕様をyamlで定義する

まずは`openapi.yaml`に必要最低限のGETメソッドのみ定義

```yaml:openapi.yaml
openapi: 3.0.1
info:
  title: gin api sample
  version: 1.0.0
paths:
  '/sample':
    get:
      parameters:
        - $ref: '#/components/parameters/sampleParam1'
      responses:
        '200':
          description: Sampleの取得成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SampleArray'
        '400':
          description: 不正なリクエストです。
        '401':
          description: 認証エラーです。

components:
  parameters:
    sampleParam1:
      in: query
      name: param1
      schema:
        type: string
      required: false
  schemas:
    SampleArray:
      type: array
      items:
        $ref: '#/components/schemas/SampleResponse'
    SampleResponse:
      type: object
      properties:
        value:
          type: string

```

### oapi-codegenを使ってコードを生成する

#### oapi-codegenのインストール
基本的な使い方は，[公式情報](https://github.com/oapi-codegen/oapi-codegen)を参照のこと．goのコマンドが実行可能な環境で，下記のようにインストール可能．

```sh
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
```

#### oapi-codegenのコード生成用設定ファイルを用意
ファイル名は`config.yaml`とする．今回はgin用のコードを生成するため，`gin-server: true`と設定しておく．
```yaml:config.yaml
package: main
generate:
  models: true
  embedded-spec: true
  gin-server: true
  strict-server: true
output-options:
  skip-prune: true
output: ./api/api.gen.go
```

#### oapi-codegenのコード生成コマンドを実行
```sh
oapi-codegen -config config.yaml openapi.yaml
```
- `openapi/generate.go:4: running "oapi-codegen": exec: "oapi-codegen": executable file not found in $PATH`といったエラーになる場合，下記のようにGO PATHとGO ROOTを環境変数に定するとよい
    ```sh
    export GOPATH=$HOME/go
    export PATH=$PATH:$GOPATH/bin
    ```
- コード生成が成功すれば，`api/api.gen.go`が自動生成される

### APIの中身を実装する

#### まずは，`go mod init`をプロジェクトのrootディレクトリで実行しておく
```sh
go mod init <your module name>
```
#### エントリーポイントとなる`api/main.go`を定義する

```golang:api/main.go
package main

import "github.com/gin-gonic/gin"

type SampleServer struct {
    ServerId int
}

func main() {
    r := gin.Default()
    server := &SampleServer{
        ServerId: 1, // Sample Value
    }
    RegisterHandlers(r, server)
    r.Run() // listen and serve on 0.0.0.0:8080
}

```
- oapi-codegenの開発環境下でエントリーポイントを実装する際，`RegisterHandlers`を呼ぶことで，生成されたコード（`api.gen.go`）を使うことができる
- `api.gen.go`には`ServerInterface`が定義されており，実装すべきメソッドが決められている．`RegisterHandlers`の第２引数が`ServerInterface`となっており，自身で定義したServerInterface用のtypeを引き渡す
- ServerInterface用のtype上で定義されているValue等は，API実装の各メソッドから共通して参照可能
    - 今回のサンプルコードでいう`ServerId`を書きで実装するGETメソッドの内部処理から参照可能
    - また，同サーバに別のメソッドを追加した際にも，同様に共通参照が可能

#### APIの内部実装を行う
```golang:api/sample_get.go
package main

import (
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
    "github.com/samber/lo"
)

func (server *SampleServer) GetSample(c *gin.Context, params GetSampleParams) {
    responseArray := []SampleResponse{
        {Value: params.Param1},
        {Value: lo.ToPtr(strconv.Itoa(server.ServerId))},
    }
    var responseBody GetSample200JSONResponse = GetSample200JSONResponse(responseArray)
    c.JSON(http.StatusOK, responseBody)
}
```
- 上記は，受け取ったパラメータと前述の`ServerInterface`から取り出した`ServerId`を含む配列を返す簡単なAPIの中身のサンプルの実装例
- `GetSample`は`ServerInterface`において実装すべきメソッドとして生成されたコード`api.gen.go`に定義されている
- 上記はもとより，パラメータやレスポンスとして使う型（`GetSampleParams`や`SampleResponse`,`GetSample200JSONResponse`）も`api.gen.go`で定義されている
- `api.gen.go`で自動生成された型を使った実装を守れば，OpenAPIで定義した通りのパラメータとレスポンスを持つAPIを実装可能

### サーバを立ち上げる
```sh
# on project root
go mod tidy
go run ./api/
```
- GET [http://localhost:8080/sample?param1=hage](http://localhost:8080/sample?param1=hage) でアクセス 

### Dockerで動く環境に変更する
- 以下は必要に応じて任意

#### Makefileとgenerate.goを作成し，各種必要なコマンドを定義する

```golang:generate.go
package main

//go:generate oapi-codegen -config config.yaml openapi.yaml

```

```makefile:Makefile
.PHONY: tidy
tidy:
	go mod tidy

.PHONY: generate
generate:
	go generate ./

```

#### Dockerfileおよびdocker-compose.yamlを作成する

- oapi-codegenのインストールおよび，各種go moduleのインストールも自動化

```Dockerfile:Dockerfile
FROM golang:1.22-alpine
RUN apk update && apk add --no-cache make
RUN mkdir /go/src/app/
WORKDIR /go/src/app/
COPY ./ /go/src/app/

RUN go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
RUN make tidy

ENTRYPOINT [ "go", "run", "./api/" ]
```

```yaml:docker-compose.yaml
version: "3"

services:
  api:
    build: ./
    volumes:
      - ./:/go/src/app/
    ports:
      - "8080:8080"
    tty: true
```

#### コンテナ上でginを実行する

```sh
docker-compose build
docker-compose up -d
```

- OpenAPIの定義を変更し，コードを生成し直したい場合，コンテナの内部でコマンド実行可能
    ```sh
    docker exec -it gin-api-sample-api-1 sh
    # in container
    make generate
    ```

## 成果物

https://github.com/h-masano/gin-api-sample

## 参考サイト

https://github.com/oapi-codegen/oapi-codegen/tree/main/examples/petstore-expanded

https://qiita.com/KWS_0901/items/11035a1f55bc779ad10f