0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FUJITSUAdvent Calendar 2024

Day 11

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

Last updated at Posted at 2024-12-10

この記事は Fujitsu Advent Calendar 2024 の 11日目 の記事です。

昨日は @earth429 さんの「Cloud Digital Leader受験記」でした。資格勉強は独学で手探りで進めがちなので、こうやって他の方の体験談が聞けるのはありがたいです!

TL; DR

  • AWKで手軽にWeb APIサーバーを起動可能
$ docker run -it -p 8080:8080 ghcr.io/syuparn/webawk:0.4.2 'GET("/names") { b["names"][1]="Taro"; res(200, b) }'
$ curl localhost:8080/names
{"names":["Taro"]}

はじめに

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

例:

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

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

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

ワンライナーAPIサーバー

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

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

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

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

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

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

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

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

書き方

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

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

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

レスポンスの組み立て

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

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

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

リクエストボディ

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

# body(".name") でリクエストボディの nameフィールドを取得
POST("/users") { b1["name"]=body(".name"); res(201, b1) }
$ curl -XPOST -d '{"name": "Hanako"}' -H 'Content-Type: application/json' localhost:8080/users
{"name":"Hanako"}

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

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

パスパラメータ

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

API定義
GET("/users/:user") { b["name"]=path("user"); res(200, b) }
$ curl localhost:8080/users/Taro
{"name":"Taro"}

ルーティング

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

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) }
# 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では同名のクエリパラメータを複数指定可能なので、配列形式で返しています。

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

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

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) }
# クエリパラメータ "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 で取得できます。

API定義
# Content-Lengthヘッダを取得
POST("/count") { b["length"]=int(header("Content-Length")); res(200, b) }
# NOTE: curlで暗黙的にリクエストヘッダ "Content-Length: 18" が指定される
curl -d '{"hello": "world"}' -H 'Content-Type: application/json' localhost:8080/count
{"length":18}

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

API定義
GET("/hello") { b["hello"] = "world"; h["X-Request-ID"]="1234"; res(200, b, h) }
# 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通信を行うことができます。

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

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

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

おわりに

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

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

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

  1. 内部的にはJSONのパース部分をjqへ委譲しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?