バックエンドの動作確認に使用できるワンライナーAPIサーバーの紹介

この記事は [Fujitsu Advent Calendar 2024](https://qiita.com/advent-calendar/2024/fujitsu) の 11日目 の記事です。

昨日は @earth429 さんの「[Cloud Digital Leader受験記](https://qiita.com/earth429/items/be951264d5401554184f)」でした。資格勉強は独学で手探りで進めがちなので、こうやって他の方の体験談が聞けるのはありがたいです！


# TL; DR

- AWKで手軽にWeb APIサーバーを起動可能

```bash
$ docker run -it -p 8080:8080 ghcr.io/syuparn/webawk:0.4.2 'GET("/names") { b["names"][1]="Taro"; res(200, b) }'
```

```bash
$ curl localhost:8080/names
{"names":["Taro"]}
```

https://github.com/Syuparn/webawk

# はじめに

Webアプリケーション開発の際、動作確認に必要な依存先のAPIサーバーがまだない、または使えないといった経験はないでしょうか？

例：
- 2つのアプリケーションを並行開発しており、呼び出したい別アプリケーションのAPIが未実装
- ローカル環境でNginx等の設定を検証したいが、ルーティング先のサーバーへ疎通できない

検証のためにモックサーバーを開発するのは少し面倒です。手軽に、欲を言えば1行でAPIサーバーが起動したいです。

ワンライナーといえばAWK（※個人の感想です）。というわけで、本記事では以前作った **AWK製のワンライナーAPIサーバー** について紹介したいと思います。

# ワンライナーAPIサーバー

リポジトリはこちらです。Dockerイメージを利用すれば環境構築不要で使えます。

https://github.com/Syuparn/webawk

:::note warn
セキュリティの考慮はされていないためインターネットへの公開は非推奨です。あらかじめご了承ください。
:::

AWKのワンライナーを指定し以下のコンテナを実行するとAPIサーバーが起動します。

```bash
# APIサーバーを起動
$ docker run -it -p 8080:8080 ghcr.io/syuparn/webawk:0.4.2 'GET("/names") { b["names"][1]="Taro"; res(200, b) }'
```

リクエストをしてみましょう。

```bash
$ curl localhost:8080/names
{"names":["Taro"]}
```

想定通りレスポンスが返ってきました。

続いて、APIの定義の書き方を紹介します。

## 書き方

パターンとアクションの組み合わせでAPIを定義します(一般的なAWKの書き方と同様です)。

```awk:API定義
# パターン: リクエストが条件にマッチするか確認、マッチしたらアクション実行
GET("/names") {
  # アクション: レスポンスを組み立てる
  b["names"][1]="Taro";
  res(200, b);
}
```

以下見やすさのためソースコードに改行を入れますが、ワンライナーでも実行可能です。

### レスポンスの組み立て
返すレスポンスを `res` 関数へ渡します。第二引数の連想配列はそのままJSONにシリアライズされます。

```awk
res(ステータスコード, 連想配列)
```

```awk:アクション部の再掲
# 連想配列bを作成
# NOTE: gawkの仕様上連想配列は要素代入時に暗黙的に初期化される
# (添え字が1オリジンなので注意！)
b["names"][1]="Taro";

# ステータスコード200("OK")、ボディbでレスポンスを返す
# bはJSON `{"names":["Taro"]}` にシリアライズされる
res(200, b);
```

### リクエストボディ
`body` でリクエストボディを取得可能です。引数には欲しい要素をjqのフィルタで指定します[^jq]。

```bash
# body(".name") でリクエストボディの nameフィールドを取得
POST("/users") { b1["name"]=body(".name"); res(201, b1) }
```

```bash
$ curl -XPOST -d '{"name": "Hanako"}' -H 'Content-Type: application/json' localhost:8080/users
{"name":"Hanako"}
```

パターンマッチにも使用可能です(以下紹介する他の関数も同様)。

```awk:バリデーションチェックの例
# nameフィールドがある場合: 正常系
POST("/names") && (n=body(".name")) { b1["name"]=n; res(201, b1) }
# それ以外の場合: バリデーションエラー
POST("/names")                      { e["error"]="name required"; res(400, e) }
```

```bash
$ curl -XPOST -d '{"name": "Hanako"}' -H 'Content-Type: application/json' localhost:8080/names
{"name":"Hanako"}

# nameフィールドが無い場合
$ curl -XPOST -d '{}' -H 'Content-Type: application/json' localhost:8080/names
{"error":"name required"}
```

### パスパラメータ
パターンに指定するパスに `:` を付けた部分がパスパラメータになります。ワイルドカードとしてマッチし、マッチした要素をアクション部で使用することも可能です。

```awk:API定義
GET("/users/:user") { b["name"]=path("user"); res(200, b) }
```

```bash
$ curl localhost:8080/users/Taro
{"name":"Taro"}
```

### ルーティング
パターンでHTTPメソッド、パスを指定します。パターンは複数記載可能で、上から順にマッチされます。
いずれにもマッチしない場合はデフォルトの404レスポンスを返します。

```awk:API定義
POST("/users")                   { b1["name"]=body(".name"); res(201, b1) }
GET("/users")                    { b2["users"][1]["name"]="Taro"; res(200, b2) }
DELETE("/users/:user")           { res(204) }
```

```bash
# POST("/users")
# NOTE: Content-Typeヘッダの指定が必要です
$ curl -XPOST -d '{"name": "Hanako"}' -H 'Content-Type: application/json' -i localhost:8080/users
HTTP/1.1 201 Created
Connection: keep-alive
Content-Length: 17
Content-Type: application/json
Date: Fri, 06 Dec 2024 03:55:14 GMT

{"name":"Hanako"}

# GET("/users")
$ curl -i localhost:8080/users
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 18
Content-Type: application/json
Date: Fri, 06 Dec 2024 03:56:24 GMT

{"users":[{"name":"Taro"}]}

# DELETE("/users/:user")
$ curl -XDELETE -i localhost:8080/users/Hanako
HTTP/1.1 204 No Content
Connection: keep-alive
Date: Fri, 06 Dec 2024 03:59:13 GMT

# 未定義のパス
$ curl -i localhost:8080/articles
HTTP/1.1 404 Not Found
Connection: keep-alive
Content-Length: 63
Content-Type: application/json
Date: Fri, 06 Dec 2024 04:00:07 GMT

{"error":"Oops! Any of patterns did not match to the request."}
```

### クエリパラメータ
`getquery` で指定した名前のクエリパラメータを取得できます。HTTPでは同名のクエリパラメータを複数指定可能なので、配列形式で返しています。

```awk:API定義
# getqueryでクエリパラメータを取得
# HACK: awkでは戻り値で配列を返すことができないため、第二引数の空の配列(q1)に破壊的変更を加え値を設定している
GET("/queries") { getquery("q", q1); res(200, q1) }
```

```bash
$ curl 'localhost:8080/queries?q=foo&q2=bar&q=baz'
["foo","baz"]
```

パターンに使用する際は `query` を使用してください。

```awk:API定義
# クエリパラメータtag=programming指定の場合
GET("/articles") && query("tag", "programming") { b["articles"][1]["name"]="learning awk"; res(200, b) }
# クエリパラメータtagがある場合(値は不問)
GET("/articles") && query("tag")                { getquery("tag", q); b2["articles"][1]["name"]="best practices of " q[1]; res(200, b2) }
# 無い場合
GET("/articles")                                { b3["articles"][1]["name"]="10 places to visit"; res(200, b3) }
```

```bash
# クエリパラメータ "tag" に "programming" を指定
$ curl 'localhost:8080/articles?tag=programming'
{"articles":[{"name":"learning awk"}]}

# クエリパラメータ "tag" 指定
$ curl 'localhost:8080/articles?tag=management'
{"articles":[{"name":"best practices of management"}]}

# クエリパラメータ "tag" なし
$ curl 'localhost:8080/articles'
{"articles":[{"name":"10 places to visit"}]}
```

### リクエスト、レスポンスヘッダ
リクエストヘッダは `header` で取得できます。

```awk:API定義
# Content-Lengthヘッダを取得
POST("/count") { b["length"]=int(header("Content-Length")); res(200, b) }
```

```bash
# NOTE: curlで暗黙的にリクエストヘッダ "Content-Length: 18" が指定される
curl -d '{"hello": "world"}' -H 'Content-Type: application/json' localhost:8080/count
{"length":18}
```

レスポンスヘッダを返す場合は `res` の第3引数に指定します。

```awk:API定義
GET("/hello") { b["hello"] = "world"; h["X-Request-ID"]="1234"; res(200, b, h) }
```

```bash
# X-Request-ID が指定されている
$ curl -i localhost:8080/hello
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 17
Content-Type: application/json
Date: Fri, 06 Dec 2024 05:35:56 GMT
X-Request-ID: 1234

{"hello":"world"}
```

# 仕組み
GNU AWKの双方向パイプでは、入出力に `/inet/tcp/${port}/0/0` を指定することでTCP通信を行うことができます。

https://www.gnu.org/software/gawk/manual/gawkinet/gawkinet.html#File-_002finet_002ftcp

これを利用し、以下の流れでHTTP通信を行っています。HTTP/1.1を使用しているため、処理の大半は一般的なAWKプログラムと同じ単なる文字列処理です。

- パイプからHTTPリクエストを受け取る
- HTTPリクエストをパースしグローバル変数へ格納
- パターン用関数(`GET` 等)でグローバル変数を参照しマッチするか判別
- マッチしたらアクション実行、HTTPレスポンスを作成
- HTTPレスポンスをパイプへ渡す

技術的な詳細については以前投稿した以下の記事をご覧ください。

https://qiita.com/Syuparn/items/09205af50bcead3c95b0

# おわりに

以上、AWK製のワンライナーHTTP APIサーバーの紹介でした。ちょっとモックのAPIサーバーが欲しいときに少しでもお役に立てれば幸いです。

（※繰り返しになりますが、セキュリティの考慮はされていないためインターネットへの公開は非推奨です。ご了承ください。）

明日は @SogoK さんの記事です。お楽しみに！

[^jq]: 内部的にはJSONのパース部分をjqへ委譲しています。