Classic ASPによるRESTful APIのルーティング実装例

この記事は[ZOZOテクノロジーズ #1 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech) 20日目の記事です。

## はじめに
同じエンドポイントのAPIでも、HTTPメソッドによって実行内容が変化するルーティング処理を、Classic ASP（VBScript）で実装してみます。
 
大事なのはこの三点です。

1. `Request.ServerVariables("REQUEST_METHOD")`でHTTPメソッドを取得する。
2. `Request.ServerVariables("URL")`で取得したURLからリソース名を取得する。
3. `GetRef`を使って文字列で実行する処理を決める。

## 実装

これから実装する、エンドポイント・HTTPメソッド・実行される関数は、それぞれ表の通りです。

<table>
<tr>
  <th>エンドポイント</th>
  <th>HTTPメソッド</th>
  <th>実行される関数</th>
</tr>
<tr>
  <td rowspan=3><a>https://example.com/api/v1/hoges</a></td>
</tr>
<tr>
  <td>POST</td>
  <td>CreateHoges</td>
</tr>
<tr>
  <td>GET</td>
  <td>ReadHoges</td>
</tr>
<tr>
  <td rowspan=3><a>https://example.com/api/v1/fugas</a></td>
</tr>
<tr>
  <td>POST</td>
  <td>CreateFugas</td>
</tr>
<tr>
  <td>GET</td>
  <td>ReadFugas</td>
</tr>
</table>
※今回はDELETEやPUTなどの更新系については触れません。

### 1. HTTPメソッドからCRUDを特定する

HTTPメソッドからCRUDのどの操作をするか決めます。

`Request.ServerVariables("REQUEST_METHOD")` でHTTPメソッドを取得することができます。
ここではCRUDのいずれかの文字列を返す関数を作成します。

```vb:/api/v1/router.asp

Function GetCrudByHttpMethod()
    Select Case Request.ServerVariables("REQUEST_METHOD")
    Case "POST" GetCrudByHttpMethod = "Create"
    Case "GET" GetCrudByHttpMethod = "Read"
    Case "PUT" GetCrudByHttpMethod = "Update"
    Case "DELETE" GetCrudByHttpMethod = "Delete"
    End Select
End Function
```

### 2. URLからリソース名を特定する

`Request.ServerVariables("URL")` でURLを取得することができます。
URLから不要な文字列を除去して、リソース名を取得する関数を作ります。

```vb:/api/v1/router.asp

Const API_ENDPOINT_BASE_PATH_V1 = "/api/v1"
Const API_DEFAULT_ASP = "Default.asp"

Function GetResourceNameByUrl()
    Dim resourceName : resourceName = Request.ServerVariables("URL")
    resourceName = Replace(resourceName, API_ENDPOINT_BASE_PATH_V1, "")
    resourceName = Replace(resourceName, API_DEFAULT_ASP, "")
    resourceName = Replace(resourceName, "/", "")
    GetResourceNameByUrl = resourceName
End Function
```

例えば https://example.com/api/v1/hoges の場合、戻り値は`hoges`となります。

### 3. CRUDとリソース名を組み合わせて関数名を組み立てる

先ほど作った二つの関数を組み合わせるだけです。

```vb:/api/v1/router.asp

Function GetFuncName()
    GetFuncName = (GetCrudByHttpMethod() & GetResourceNameByUrl())
End Function
```

例えば https://example.com/api/v1/hoges がGETで呼び出された場合、戻り値は`ReadHoges`となります。

### 4. 関数の場所を見つける

VBScriptの組み込み関数`GetRef`を用いて、メモリ内での関数の場所（アドレス）を取得する処理を作成します。
`GetRef`はC言語などで言うところの「関数ポインタ」です。

先ほど組み立てた関数名をGetRefの引数で渡します。
**存在しない関数名を指定するとエラーになる**ので、On Errorステートメントを使って回避しています。
また初期値に`Nothing`をセットすることで、存在しない関数であることが、呼び出し元でもわかるようになります。

```vb:/api/v1/router.asp

Function GetFuncPointer(ByVal funcName)
    Set GetFuncPointer = Nothing

   'GetRefに存在しない関数名を指定するとエラーになる。
    On Error Resume Next
    Set GetFuncPointer = GetRef(funcName)
    On Error GoTo 0
End Function
```

### 5. 関数を呼び出す

`GetRef`で取得した関数ポインタは、`Call`で呼び出せます。
つまり`GetRef`を使うことで、**文字列によって呼び出す関数を決めることができます。**

存在しない関数の場合は、`GetFuncPointer`で指定した初期値の`Nothing`で判定して制御します。

```vb:/api/v1/router.asp

Sub Router()
    Dim funcName : funcName = GetFuncName()
    Dim FuncPointer : Set FuncPointer = GetFuncPointer(funcName)

    If FuncPointer Is Nothing Then
        Response.write funcName & "は存在しません"
    Else
        Call FuncPointer()
    End If
End Sub
```

## 実行

### 1. 準備

あらかじめ下記のファイルを用意しておきます。
存在しない関数の挙動を確認するために`CreateHoges`はコメントアウトしておきます。

```vb:/api/v1/hoges/default.asp

<!-- #include virtual="\api\v1\router.asp" -->
<%
Call Router()

'存在しない関数を確認するためにコメントアウト
'Function CreateHoges()
'    Response.write "CreateHogesが呼ばれました"
'End Function

Function ReadHoges()
    Response.write "ReadHogesが呼ばれました"
End Function   
```

### 2. GETで実行

https://example.com/api/v1/hoges をGETで実行します。

```
ReadHogesが呼ばれました
```

HTTPメソッドがGETなので、hogesをreadする処理が呼び出されました。

### 3. POSTで実行

https://example.com/api/v1/hoges をPOSTで実行します。

```
Createhogesは存在しません
```

`CreateHoges`はコメントアウトしたので呼び出されません。
関数の有無も正しく判定できていることが確認できました。

### 4. 別のエンドポイントで実行

続いて、異なるエンドポイントでも検証してみます。
以下のファイルを用意します。

```vb:/api/v1/fugas/default.asp

<!-- #include virtual="\api\v1\router.asp" -->
<%
Call Router()

Function ReadFugas()
    Response.write "ReadFugasが呼ばれました"
End Function
%>
```

https://example.com/api/v1/fugas をGETで実行します。

```
ReadFugasが呼ばれました
```
エンドポイントが変わっても、正常に動作していることが確認できました。

## まとめ

大事なのはこの三点です。

1. `Request.ServerVariables("REQUEST_METHOD")`でHTTPメソッドを取得する。
2. `Request.ServerVariables("URL")`で取得したURLからリソース名を取得する。
3. `GetRef`を使って文字列で実行する処理を決める。

ここまでで登場したコードは以下にまとめました。

```vb:/api/v1/router.asp

<%
Const API_ENDPOINT_BASE_PATH_V1 = "/api/v1"
Const API_DEFAULT_ASP = "Default.asp"

Sub Router()
    Dim funcName : funcName = GetFuncName()
    Dim FuncPointer : Set FuncPointer = GetFuncPointer(funcName)

    If FuncPointer Is Nothing Then
        Response.write funcName & "は存在しません"
    Else
        Call FuncPointer()
    End If
End Sub

Function GetFuncName()
    GetFuncName = (GetCrudByHttpMethod() & GetResourceNameByUrl())
End Function

Function GetCrudByHttpMethod()
    Select Case Request.ServerVariables("REQUEST_METHOD")
    Case "POST" GetCrudByHttpMethod = "Create"
    Case "GET" GetCrudByHttpMethod = "Read"
    Case "PUT" GetCrudByHttpMethod = "Update"
    Case "DELETE" GetCrudByHttpMethod = "Delete"
    End Select
End Function

Function GetResourceNameByUrl()
    Dim resourceName : resourceName = Request.ServerVariables("URL")
    resourceName = Replace(resourceName, API_ENDPOINT_BASE_PATH_V1, "")
    resourceName = Replace(resourceName, API_DEFAULT_ASP, "")
    resourceName = Replace(resourceName, "/", "")
    GetResourceNameByUrl = resourceName
End Function

Function GetFuncPointer(ByVal funcName)
    Set GetFuncPointer = Nothing

    On Error Resume Next
    Set GetFuncPointer = GetRef(funcName)
    On Error GoTo 0
End Function
%>
```

```vb:/api/v1/hoges/default.asp

<!-- #include virtual="\api\v1\router.asp" -->
<%
Call Router()

'存在しない関数を確認するためにコメントアウト
'Function CreateHoges()
'    Response.write "CreateHogesが呼ばれました"
'End Function

Function ReadHoges()
    Response.write "ReadHogesが呼ばれました"
End Function   
```

```vb:/api/v1/fugas/default.asp

<!-- #include virtual="\api\v1\router.asp" -->
<%
Call Router()

Function CreateFugas()
    Response.write "CreateFugasが呼ばれました"
End Function

Function ReadFugas()
    Response.write "ReadFugasが呼ばれました"
End Function
%>
```
## 参考
[ServerVariables | Microsoft Docs](https://docs.microsoft.com/ja-jp/previous-versions/iis/iis-5.0/cc338803%28v%3dmsdn.10%29)
[GetRef 関数 | Microsoft Docs](https://docs.microsoft.com/ja-jp/previous-versions/windows/scripting/cc410245(v=msdn.10)?redirectedfrom=MSDN)

---
明日は[@takanamito](https://qiita.com/takanamito)さんの記事です！

また、今年は全部で5つのAdvent Calendarが公開されています。

[ZOZOテクノロジーズ #1 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech)
[ZOZOテクノロジーズ #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech2)
[ZOZOテクノロジーズ #3 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech3)
[ZOZOテクノロジーズ #4 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech4)
[ZOZOテクノロジーズ #5 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech5)

宜しければご覧ください。