PowerShell製のWebフレームワーク、Polarisの使い方

本記事は PowerShell Advent Calendar 2018 の 14 日目です。

100% PowerShellで書かれたPolarisというWebフレームワークの使い方を紹介します。

※Polarisは誕生してまだ1年あまりの試験的なプロジェクトのため、
　ここで記載した情報はすぐに古くなる可能性があります。（念のため）

# Polarisとは

Polaris[^1]とは、MicrosoftのPowerShellチームがOSSとして開発しているPowerShell製のWebフレームワークです。
[^1]: Polaris（ポラリス、pəlˈærɪs : 北極星）という命名ですが、おそらく、PowerShell + Solaris から来ていると思われます。（推測）
100% PowerShell[^2]で実装されており、Windows PowerShellとPowerShell Coreの両方で動作可能となっています。
[^2]: 発足時はコア部分だけC#で実装されていましたが、2018年4月頃に「[WIP: Pure PowerShell Implementation](https://github.com/PowerShell/Polaris/pull/96)」でPowerShellのみを使用するようにリファクタリングされました。
現在、PoC（Proof of Concept）として開発されており、Microsoft製品としてはサポート対象外とされています。

| 項目 | 説明 |
|:-----------------|------------------|
| 開発元 | Microsoft |
| 登場時期 | 2017年10月3日（Initial commit） |
| 最新版 | 0.2.0 / 2018年12月6日[^3] |
[^3]: 配布先：[PowerShell Gallery](https://www.powershellgallery.com/packages/Polaris/0.2.0)
| リポジトリ | https://github.com/PowerShell/Polaris |
| プログラミング言語 | PowerShell |
| 対応OS | Cross-platform (Windows, Linux or Mac) |
| ライセンス | MIT License |
| 公式ドキュメント | https://powershell.github.io/Polaris/ |

## 特徴
サーバーの起動・停止はもとより、Web-APIリソースの追加、DocumentRootの指定などをすべてコマンドレットで行えることができます。
最近、「〇〇で爆速Webアプリ開発生活始めてみた！」のような記事が注目されています（偏見？）が
シンプルなGETのWeb-APIであれば、ワンラインで実装（もちろん、ホットデプロイ）できるので、
これ以上爆速があるのか？と思わせるくらい速い実装＆実行が可能です。
ただ、サポートされる機能が非常にシンプルなため、一般的なWebアプリにはまだ使用することは現実的ではないと思っています。
Mockサーバーなど、手軽に利用したいものに関しては有効かもしれません。

# 環境構築

PolarisはPowerShell 5 以上が必要です。
PowerShellGetで取得できます。

```powershell
Install-Module Polaris
```

# 使い方

以下、Polarisの基本形。

```powershell:SampleServer.ps1
#Requires -Version 5.0
#Requires -Modules Polaris

$ErrorActionPreference = 'stop'

New-PolarisGetRoute -Path "/helloworld" -Force -Scriptblock {
    $Response.Send('Hello World!')
}
```

```powershell:ターミナル（サーバー側）
PS> . .\SampleServer.ps1

PS> Start-Polaris
#=> Port              : 8080
#=> RouteMiddleWare   : {}
#=> ScriptblockRoutes : {[/helloworld, System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ScriptBlock]]}
#=> GetLogsString     : PolarisLogs
#=> ClassDefinitions  :
#=> ContextHandler    : System.AsyncCallback
```

```powershell:ターミナル（クライアント側）
PS> Invoke-RestMethod http://localhost:8080/helloworld
Hello World!
```

ターミナルをサーバーとクライアントとで2つに分けているのは、
`Start-Polaris`を実行したターミナルだとなぜか`Invoke-WebRequest`ができなかったためです。
※Issuesにあるかと思いましたが、何も報告されていなかった。（仕様？環境による？）

## CRUDのWeb-APIサンプル
以下、蔵書管理システムのCRUDをWeb-APIで実装していきます。
先ほどドット演算子で読み込んだ `SampleServer.ps1` にWeb-APIのリソースを追加します。

### 各APIの仕様

`~\Documents\Polaris`内に`FileStorage`ディレクトリを作成し、
1ファイル1書籍データでJSON形式に管理していきます。
ファイル名はユニークである必要があるため、ISBNコードを使用することにします。
以下、サンプルです。

```json:~\Documents\Polaris\FileStorage\9781788838986.json
{
  "isbn" : 9781788838986,
  "title" : "Learn PowerShell Core 6.0",
  "publisher" : "Packt Publishing"
}
```

APIはもちろんCRUDなので、以下の4つを作成します。

| Path| Method| Description |
|--------------|---------|------------------|-------------------------------|
| /books　　　　| POST　　　 | 新しい書籍を登録します。　　　　　　　　　　　　　　　|
| /books　　　　| GET　　 　 | 一致するISBNの書籍情報を取得します。 |
| /books　　　　| PUT　　　　 | 一致するISBNの書籍情報を更新します。 |
| /books　　　　| DELETE　　　 | 一致するISBNの書籍情報を削除します。 |

基本的に、RequestとResponseとも`application/json`で通信させていきます。

### 下準備（Middlewareの実装）

先に`~\Documents\Polaris\`ディレクトリを作成しておきます。

```powershell:ターミナル（サーバー側）
New-Item "~\Documents\Polaris\" -ItemType Director -Force
```

次に、各API特有のロジックに入る前に行う共通処理（Middleware）を実装します。
Middlewareですが、通常はOSとアプリケーションの間のプログラムを指す言葉ですが、
PlarisにおいてはPolarisとアプリケーションロジックの間（Middle）を示す用語として使用されています。

```powershell:SampleServer.ps1
# $Request.BodyにRequestで受け取ったJSON文字列をオブジェクトとして格納する。
Use-PolarisJsonBodyParserMiddleware

# Requestで受け取った書籍情報を保存するディレクトリを作成、および、パスを設定する。
New-PolarisRouteMiddleware -Name FileStorage -Force -ScriptBlock {
  $PolarisPath = "~\Documents\Polaris\FileStorage"
  if (-not (Test-Path $PolarisPath)) {
    New-Item $PolarisPath -ItemType Directory > $null
  }

  $Request | Add-Member -Name PolarisPath -Value $PolarisPath -MemberType NoteProperty
}
```

修正した`SampleServer.ps1`を起動中のPolarisサーバーに適用したい場合は、
再度`. .\SampleServer.ps1`コマンドを実行すれば大丈夫です。
※以降も同様。

### POST

それでは、新しい書籍を登録するAPIから実装をしていきます。

```powershell:SampleServer.ps1
# 書籍登録API
New-PolarisPostRoute -Path "/books" -Force -Scriptblock {
  if (-not $Request.Body.ISBN -or -not $Request.Body.Title) {
    $Response.SetStatusCode(400)
    $Response.Send("ISBN or Title may not be empty.")
    return
  }

  New-Item -ItemType File -Path $Request.PolarisPath -Name "$($Request.Body.ISBN).json" -Value $Request.BodyString > $null
}
```

では、作成したAPIの動作確認をしていきます。

```powershell:ターミナル（クライアント側）
# 修正を適用
PS> . .\SampleServer.ps1
# 1件目
PS> $book = @{isbn = 9781788838986; title = 'Learn PowerShell Core 6.0'; publisher = 'Packt Publishing'}
PS> Invoke-RestMethod http://localhost:8080/books -Method Post -Body ($book | ConvertTo-Json -Compress)
# 2件目
PS> $book = @{isbn = 9781633430297; title = 'Windows PowerShell in Action'; publisher = 'Manning Publications'}
PS> Invoke-RestMethod http://localhost:8080/books -Method Post -Body ($book | ConvertTo-Json -Compress)

# FileStorageに登録した書籍ごとにjsonファイルが作成されている。
PS> ls ~\Documents\Polaris\FileStorage
#=> 
#=>     ディレクトリ: C:\Users\uokeaj\Documents\FileStorage
#=> 
#=> Mode                LastWriteTime         Length Name
#=> ----                -------------         ------ ----
#=> -a----       2018/12/13     21:10             96 9781633430297.json
#=> -a----       2018/12/13     21:10             89 9781788838986.json
```

### GET

次に、一致するISBNの書籍情報を取得できるようにします。
リクエストパラメーターは`$Request.Query['パラメーター名']`で取得可能です。

```powershell:SampleServer.ps1
# 書籍取得API
New-PolarisGetRoute -Path "/books" -Force -Scriptblock {
  $query = if ($Request.Query['isbn']) {$Request.Query['isbn']} else {'*'}
  $gciParams = @{
    Path        = $Request.PolarisPath
    Filter      = "$($query).json"
    ErrorAction = 'SilentlyContinue'
  }

  $books = Get-ChildItem @gciParams | Foreach-Object {$_ | Get-Content | ConvertFrom-Json}
  $Response.Send(($books | ConvertTo-Json));
}
```

```powershell:ターミナル（クライアント側）
# 修正を適用
PS> . .\SampleServer.ps1
# 1件だけ取得
PS> Invoke-RestMethod http://localhost:8080/books?isbn=9781633430297

#=> publisher            title                                 isbn
#=> ---------            -----                                 ----
#=> Manning Publications Windows PowerShell in Action 9781633430297

# 全件取得
PS> Invoke-RestMethod http://localhost:8080/books

#=> publisher            title                                 isbn
#=> ---------            -----                                 ----
#=> Manning Publications Windows PowerShell in Action 9781633430297
#=> Packt Publishing     Learn PowerShell Core 6.0    9781788838986
```

### PUT

今度は、一致するISBNの書籍情報を更新します。
POSTの時と実装の仕方はあまり変わりません。

```powershell:SampleServer.ps1
# 書籍更新API
New-PolarisPutRoute -Path "/books" -Force -Scriptblock {
  if (-not $Request.Body.ISBN -or -not $Request.Body.Title) {
    $Response.SetStatusCode(400)
    $Response.Send("ISBN or Title may not be empty.")
    return
  }

  Set-Content -Path (Join-Path $Request.PolarisPath "$($Request.Body.ISBN).json") -Value $Request.BodyString
}
```

```powershell:ターミナル（クライアント側）
# 修正を適用
PS> . .\SampleServer.ps1
# 試しに、'Learn PowerShell Core 6.0'の出版社情報を省略した状態で更新してみます。
PS> $book = @{isbn = 9781788838986; title = 'Learn PowerShell Core 6.0'}
PS> Invoke-RestMethod http://localhost:8080/books -Method Put -Body ($book | ConvertTo-Json -Compress)

# publisherプロパティが消えていることが確認できました。
PS> Invoke-RestMethod http://localhost:8080/books?isbn=9781788838986

#=>          isbn title
#=>          ---- -----
#=> 9781788838986 Learn PowerShell Core 6.0
```

### DELETE

最後に、一致するISBNの書籍情報を削除できるようにします。
こちらも、POST・PUTとあまり変わりません。

```powershell:SampleServer.ps1
# 書籍削除API
New-PolarisDeleteRoute -Path "/books" -Force -Scriptblock {
  if (-not $Request.Body.ISBN) {
    $response.SetStatusCode(400)
    $response.Send("ISBN may not be empty.")
    return
  }

  Remove-Item -Path (Join-Path $Request.PolarisPath "$($Request.Body.ISBN).json")
}
```

```powershell:ターミナル（クライアント側）
# 修正を適用
PS> . .\SampleServer.ps1
PS> Invoke-RestMethod http://localhost:8080/books -Method Delete -Body '{"isbn":9781788838986}'
# 削除できていることが確認できました。
PS> Invoke-RestMethod http://localhost:8080/books?isbn=9781788838986
#=> No output
```

## Webページの表示方法

Polarisでは、まだテンプレートエンジンで動的なWebページ画面を生成する機能はないですが、
静的ページであれば、`New-PolarisStaticRoute`というコマンドレットで簡単に表示させることができます。

```powershell:ターミナル（サーバー側）
PS> New-Item "~\Documents\Polaris\Public" -ItemType Director -Force

PS> @"
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>Hello world!</title>
  </head>
  <body>
    PolarisでHTMLページを表示しました。
  </body>
</html>
"@ | Out-File "~\Documents\Polaris\Public\index.html" -Encoding utf8

PS> New-PolarisStaticRoute -RoutePath 'public' -FolderPath "~\Documents\Polaris\Public" -Force
PS> Start-Process http://localhost:8080/public/index.html
```

すると、下記のようにWebページを表示することができます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/114728/e7c8bda9-7f4b-9bfa-6a44-d9394757a031.png)

現在、PSHTMLというDSLでもっとHTMLソースを扱いやすくできるようにするための提案がされています。
[RFC #002: Bundle PSHTML with Polaris #165](https://github.com/PowerShell/Polaris/issues/165)
今後のアップデートに注目ですね。

# 参考文献
[Polaris docs](https://powershell.github.io/Polaris/)
[Preview Online Code Files Learn PowerShell Core 6.0](https://www.packtpub.com/networking-and-servers/learn-powershell-core-60)