Haskellから簡単にWeb APIを叩く方法

Haskellで[http-conduit](http://hackage.haskell.org/package/http-conduit)を使って簡単にWeb APIを叩く方法についてまとめました。
"[通信系テストのためのサイトのススメ](http://yamitzky.hatenablog.com/entry/2016/05/13/204107)"という便利なWebサイトをまとめてくれている記事があるのでこれに沿って話を進めていきます。

## <i class="fa fa-cube"></i> example.com
まずは単純にURLを叩いてコンテンツを取得してみましょう :exclamation: 
<https://example.com>からHTMLのデータを取得してみます。[`Network.HTTP.Simple`](http://hackage.haskell.org/package/http-conduit/docs/Network-HTTP-Simple.html)というモジュールにある[`httpLBS`](http://hackage.haskell.org/package/http-conduit/docs/Network-HTTP-Simple.html#v:httpLBS) という関数を使います。型は

```hs
httpLBS :: Request -> IO (Response ByteString)
```

このようになっており、`Request`を引数にとり副作用を伴って`Response ByteString`を返すというものです。[`Request`](http://hackage.haskell.org/package/http-conduit/docs/Network-HTTP-Simple.html#t:Request)は[`IsString`](https://hackage.haskell.org/package/base/docs/Data-String.html#t:IsString)のインスタンスになっているので[`OverloadedStrings`](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#overloaded-string-literals)というGHC拡張を指定すれば単純にURLの文字列リテラルを書くだけで値を作ることが出来ます。
[`Response`](http://hackage.haskell.org/package/http-conduit/docs/Network-HTTP-Simple.html#t:Response)にはHTTPのレスポンスに関する全ての情報が含まれていて、例えばbodyの内容が欲しいときは[`getResponseBody`](http://hackage.haskell.org/package/http-conduit/docs/Network-HTTP-Simple.html#v:getResponseBody)を使って取り出します。

```hs
getResponseBody :: Response a -> a
```

これらを組み合わせると

```hs
{-# LANGUAGE OverloadedStrings #-}

import Network.HTTP.Simple

main :: IO ()
main = do
    res <- httpLBS "https://example.com"
    print (getResponseBody res)

-- $ runhaskell Main.hs
-- "<!doctype html>\n<html>\n<head>...
```

ちゃんとHTMLを取得できていると思います :smile: 

## <i class="fa fa-cube"></i> httpbin
[httpbin](https://httpbin.org/)はHTTPに関するいろんな情報をJSONで返してくれるサイトです。ここでは[aeson](http://hackage.haskell.org/package/aeson)というライブラリを使ってJSONを扱う方法を見ていきます。

### /ip
送信元のIPアドレスを返してくれるAPIです。レスポンスはこんな感じ

```bash
$ curl https://httpbin.org/ip
{
  "origin": "192.0.43.10"
}
```

まずはこれを独自のデータ構造にマッピングしようと思います

```hs
data IPRes = IPRes { origin :: String } deriving Show
```

`aeson`にある[`decode`](http://hackage.haskell.org/package/aeson/docs/Data-Aeson.html#v:decode) という関数を使えば安全にJSONの文字列をパースすることが出来ます。`decode`の返り値の型は`FromJSON`のインスタンスになっている必要があるので`IPRes`の`FromJSON`のインスタンスを定義してJSONからデコードできるようにしてみましょう。

```hs
{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson

instance FromJSON IPRes where
    parseJSON (Object v) = IPRes <$> v .: "origin"
    parseJSON _          = mempty
```

ここまでのコードを組み合わせると以下のようになります。

```hs
{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson
import Network.HTTP.Simple

data IPRes = IPRes { origin :: String } deriving Show

instance FromJSON IPRes where
    parseJSON (Object v) = IPRes <$> v .: "origin"
    parseJSON _          = mempty

main :: IO ()
main = do
    res <- httpLbs "https://httpbin.org/ip"
    let ip = decode (getResponseBody res) :: Maybe IPRes
    case ip of
      Nothing -> putStrLn "parsing failed"
      Just ip -> print ip

-- $ runhaskell Main.hs
-- IPRes {origin = "192.0.43.10"}
```

細かいですがIPを表す型が`String`では不便だと思います。[`iproute`](http://hackage.haskell.org/package/iproute)というライブラリで[`IP`](http://hackage.haskell.org/package/iproute/docs/Data-IP.html#t:IP)という型が定義されているのでこれを使うことにしましょう。JSONからデコードするので`IP`も`FromJSON`のインスタンスである必要がありますが実は[`aeson-iproute`](https://hackage.haskell.org/package/aeson-iproute)というライブラリで定義されるのでそれを使うことにしましょう。以下のように変更してみてください。

```diff
 {-# LANGUAGE OverloadedStrings #-}

 import Data.Aeson
+import Data.Aeson.IP
+import Data.IP
 import Network.HTTP.Simple

-data IPRes = IPRes { origin :: String } deriving Show
+data IPRes = IPRes { origin :: IP } deriving Show

 instance FromJSON IPRes where
     parseJSON (Object v) = IPRes <$> v .: "origin"
```

型を変更するだけで`IP`に対応することが出来ました。

さて、独自のデータ構造を定義するたびに`FromJSON`のインスタンスを作るのは少々手間です。Aesonでは`Generic`のインスタンスになっている型なら自動的に`FromJSON`のインスタンスを導出できるようになっています。以下のように変更してみてください

```diff
+{-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE OverloadedStrings #-}

 import Data.Aeson
 import Data.Aeson.IP
 import Data.IP
+import GHC.Generics
 import Network.HTTP.Simple

-data IPRes = IPRes { origin :: IP } deriving Show
+data IPRes = IPRes { origin :: IP } deriving (Generic, Show)

-instance FromJSON IPRes where
-    parseJSON (Object v) = IPRes <$> v .: "origin"
-    parseJSON _          = mempty
+instance FromJSON IPRes

 main :: IO ()
 main = do
```

フィールドの多い型ならコードが減る量も多くなるでしょう。この`Generics`を使った方法の欠点は名前がかぶるなどの制約でJSONとHaskellのレコードの名前を一対一に出来ない場合に使えないところです(GHC8で改善される予定です)。そういった場合はモジュールを分けるなどの工夫が必要になります。

実は`Network.HTTP.Simple`には[`httpJSON`](http://hackage.haskell.org/package/http-conduit/docs/Network-HTTP-Simple.html#v:httpJSON)という関数があってJSONのパースを勝手にやってくれます。

```diff
 main :: IO ()
 main = do
-    res <- httpLbs "https://httpbin.org/ip"
-    let ip = decode (getResponseBody res) :: Maybe IPRes
-    case ip of
-      Nothing -> putStrLn "parsing failed"
-      Just ip -> print ip
+    res <- httpJSON "https://httpbin.org/ip"
+    let ip = (getResponseBody res :: IPRes)
+    print ip
```

結局コードは以下のようになりました。

```hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson
import Data.Aeson.IP
import Data.IP
import GHC.Generics
import Network.HTTP.Simple

data IPRes = IPRes { origin :: IP } deriving (Generic, Show)

instance FromJSON IPRes

main :: IO ()
main = do
    res <- httpJSON "https://httpbin.org/ip"
    let ip = getResponseBody res :: IPRes
    print ip

-- $ runhaskell Main.hs
-- IPRes {origin = "192.0.43.10"}
```

### /headers
リクエストヘッダーの中身を返してくれるAPIです。レスポンスはこんな感じ

```bash
$ curl https://httpbin.org/headers
{
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.43.0"
  }
}
```

例えば`Host`の値が欲しかったとします。たった１つのパラメータにアクセスするためだけに新しい型を定義するのは面倒くさいですよね？ [`lens`](http://hackage.haskell.org/package/lens) と [`lens-aeson`](http://hackage.haskell.org/package/lens-aeson) というライブラリを使えば独自の型にデコードすること無くJSONの値に自由にアクセスすることが出来ます。

```hs
{-# LANGUAGE OverloadedStrings #-}

import Control.Lens
import Data.Aeson.Lens
import Network.HTTP.Simple

main :: IO ()
main = do
    res <- httpLbs "https://httpbin.org/headers"
    print $ getResponseBody res ^? key "headers" . key "Host" . _String

-- $ runhaskell Main.hs
-- Just "httpbin.org"
```

単純に値を取り出しているだけなのでさっきのコードよりずっと短くなりました。 Lens はJSONへのアクセスに限らない汎用的な概念です。詳しく知りたい方は以下のリンクを参考にして下さい。

* [Lensで行こう！](http://myuon-myon.hatenablog.com/entry/20121228/1356708483)
* [LensでHaskellをもっと格好良く！](http://www.slideshare.net/itsoutoftunethismymusic/ekmett-17955009)
* [lens over tea](https://artyom.me/#lens-over-tea)

### /post
これまでGETでアクセスしてJSONを取得していましたがPOSTでアクセスしたい時はどうすればいいでしょう。 `/post` にPOSTでアクセスすると

```bash
curl -XPOST https://httpbin.org/post
{
  "args": {},
  "data": "",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.43.0"
  },
  "json": null,
  "origin": "192.0.43.10",
  "url": "https://httpbin.org/post"
}
```

このような情報が返ってきます。間違ってGETでアクセスすると

```bash
$ curl https://httpbin.org/post
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>
```

このようになります。`httpLbs`を使って`/post`から情報を取得してみましょう

```hs
{-# LANGUAGE OverloadedStrings #-}

import Control.Lens
import Data.Aeson.Lens
import Network.HTTP.Simple

main :: IO ()
main = do
    let req = setRequestMethod "POST" "https://httpbin.org/post"
    res <- httpLbs req
    print $ getResponseBody res ^? key "headers" . key "Host" . _String

-- $ runhaskell Main.hs
-- Just "httpbin.org"
```

大事なのはこの行です。

```hs
let req = setRequestMethod "POST" "https://httpbin.org/post"
```

[`setRequestMethod`](http://hackage.haskell.org/package/http-conduit/docs/Network-HTTP-Simple.html#v:setRequestMethod)は

```hs
setRequestMethod :: ByteString -> Request -> Request
```

このような型になっていて`Request`に設定されているメソッドを単純に書き換えることが出来ます。


### /status/:statusCode
もしAPIのレスポンスが正常系じゃなかった場合はどうなるのでしょうか。`/status/:statusCode`を使うと指定したステータスコードのレスポンスを受け取ることが出来ます。

```bash
$ curl -v https://httpbin.org/status/400
*   Trying 54.175.219.8...
* Connected to httpbin.org (54.175.219.8) port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
* Server certificate: *.httpbin.org
* Server certificate: COMODO RSA Domain Validation Secure Server CA
* Server certificate: COMODO RSA Certification Authority
* Server certificate: AddTrust External CA Root
> GET /status/400 HTTP/1.1
> Host: httpbin.org
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 400 BAD REQUEST
< Server: nginx
< Date: Sat, 04 Jun 2016 07:22:38 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 0
< Connection: keep-alive
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Credentials: true
<
* Connection #0 to host httpbin.org left intact
```

`httpLbs`は異常系のステータスコードが返ってくると[`StatusCodeException`](http://hackage.haskell.org/package/http-conduit/docs/Network-HTTP-Simple.html#v:StatusCodeException)という例外を投げます。この例外を[`catch`](http://hackage.haskell.org/package/base/docs/Control-Exception.html#v:catch)を使って捕捉してみましょう

```hs
{-# LANGUAGE OverloadedStrings #-}

import Control.Exception
import Network.HTTP.Simple

main :: IO ()
main = do
    res <- (Just <$> httpLbs "https://httpbin.org/status/500") `catch`
            \(StatusCodeException s _ _) -> print s >> pure Nothing
    print res

-- $ runhaskell Main.hs
-- Status {statusCode = 500, statusMessage = "INTERNAL SERVER ERROR"}
-- Nothing
```

### /delay/:n
Web APIはインターネットを経由するのでレスポンスが返ってくるまでに時間がかかります。複数のAPIを同時に叩くとどれが最初に返ってくるか分かりません。 ここでは[`async`](http://hackage.haskell.org/package/async)というライブラリを使って期待した順番で情報を取得できるようにしてみます。

`/delay/:n`を叩くと指定した秒数だけ待ってレスポンスが返ってきます。

```bash
$ time curl "https://httpbin.org/delay/3"
{
  "args": {},
  "data": "",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.43.0"
  },
  "origin": "14.3.18.120",
  "url": "https://httpbin.org/delay/3"
}
curl "https://httpbin.org/delay/3"  0.03s user 0.02s system 1% cpu 4.566 total
```

これを使って以下の様なコードを書いてみます

```hs
{-# LANGUAGE OverloadedStrings #-}

import Control.Concurrent.Async
import Network.HTTP.Simple

main :: IO ()
main = do
    a1 <- async $ httpLbs "https://httpbin.org/delay/5"
    a2 <- async $ httpLbs "https://httpbin.org/delay/4"
    a3 <- async $ httpLbs "https://httpbin.org/delay/3"

    res1 <- wait a1
    res2 <- wait a2
    res3 <- wait a3

    print $ getResponseBody res1
    print $ getResponseBody res2
    print $ getResponseBody res3

-- $ time ./Main
-- ... \"url\": \"https://httpbin.org/delay/5\"\n ...
-- ... \"url\": \"https://httpbin.org/delay/4\"\n ...
-- ... \"url\": \"https://httpbin.org/delay/3\"\n ...
-- ./Main  0.12s user 0.04s system 2% cpu 7.162 total
```

ちゃんと並列に実行されて期待した通りの順番で返ってきてることがわかります。

## <i class="fa fa-cube"></i> badssl.com
<https://badssl.com/> を利用するとTLSのエラーのテストが出来ます。これも例外として投げられるので落ち着いて`catch`で捕捉します。

```hs
{-# LANGUAGE OverloadedStrings #-}

import Control.Exception
import Network.HTTP.Simple

main :: IO ()
main = do
    res <- (Just <$> httpLbs "https://expired.badssl.com/") `catch`
             \(TlsExceptionHostPort s _ _) -> print s >> pure Nothing
    print res

-- $ runhaskell Main.hs
-- HandshakeFailed (Error_Protocol ("certificate has expired",True,CertificateExpired))
-- Nothing
```

----

この記事は[第32回Haskellもくもく会@朝日ネット](http://haskellmokumoku.connpass.com/event/31778/)のもくもく時間を利用して書かれました。