LoginSignup
18
7

More than 5 years have passed since last update.

ClojureとHaskellで簡単なコマンドラインツール(RESTクライアント)を作ってみた

Last updated at Posted at 2018-01-15

昨年末のAdvent Calendarで2回に分けて紹介したsituated-program-challengeのClojure/Duct版実装とHaskell/Yesod版実装ですが、そこではもともとの課題のうちRESTサーバしか実現できていなかったため、今回新たにRESTクライアント実装についてご紹介します。

ちなみに、situated-program-challengeのその他の実装例は、本記事執筆時点で以下のものがあるようです。

仕様の詳細検討

situated-program-challengeにおけるRESTクライアントの仕様を検討した結果、以下のように引数の仕様に若干の変更を加えることにした。

  • POSTデータは key1=value1 key2=value2 ... という形式で列挙するのではなく、標準入力でJSON文字列を渡すように変更(上記のScala版と同様)
  • HTTPメソッド文字列は使いやすくcase-insensitiveで指定できるように変更(上記のCommon Lisp版と同様)

また、エラー処理については以下のように扱うことに決めた。

  • 引数が2個以外の個数指定された場合にはエラー
  • 引数で指定されたHTTPメソッドがGET, POST以外の場合にはエラー
  • POSTの場合にJSONデータが正常にパースできなければエラー
  • HTTPステータスコードが200以外の場合にはステータスコードも標準出力

Clojure版

Clojure版では、HTTPクライアントとして定番のひとつと思われるclj-httpとJSON操作にCheshireを利用する。

1. プロジェクトの生成

今回は特に土台とすべきフレームワーク/ライブラリもないため、コマンドラインアプリを作るのに手軽な app テンプレートでプロジェクト生成する。

$ lein new app rest-client
$ cd rest-client

2. 実装

(ns rest-client.core
  (:gen-class)
  (:require [cheshire.core :as cheshire]
            [clj-http.client :as client]
            [clojure.string :as str])
  (:import (java.io BufferedReader)))

(defn read-json-from-stdin []
  (cheshire/parse-stream (BufferedReader. *in*)))

(defmulti http (fn [_ method] (str/upper-case method)))
(defmethod http "GET" [url _]
  (client/get url {:accept :json
                   :throw-exceptions false}))
(defmethod http "POST" [url _]
  (client/post url {:form-params (read-json-from-stdin)
                    :content-type :json
                    :accept :json
                    :throw-exceptions false}))
(defmethod http :default [_ method]
  (throw (UnsupportedOperationException. (str "Unsupported HTTP method: " method))))

(defn -main [& args]
  (when (not= (count args) 2)
    (throw (IllegalArgumentException. "Exactly 2 arguments must be specified")))
  (let [{:keys [status body]} (apply http args)]
    (when (not= status 200)
      (print (str status \tab)))
    (println body)))

HTTPメソッドによって処理を振り分ける部分については、単純に casecond などで条件分岐させることもできたが、外からの拡張に開いたマルチメソッド(cf. Runtime Polymorphism)を利用してみた。

エラー処理はここでは単純に例外をスローすることにした。

3. 動作確認

RESTサーバ(cf. README)をあらかじめ起動した状態で

$ lein uberjar
$ java -jar target/uberjar/rest-client-0.1.0-SNAPSHOT-standalone.jar [args]

または

$ lein run [args]

で実行できる。

もしくは、deps.edn を以下のように用意してあるので、Leiningenではなくcljコマンドから実行することもできる。

{:paths ["src"]
 :deps {cheshire {:mvn/version "5.8.1"}
        clj-http {:mvn/version "3.9.1"}
        org.clojure/clojure {:mvn/version "1.10.0"}}
 :aliases {:test {:extra-paths ["test"]}}}
$ clj -m rest-client.core [args]

例えば、

  • メンバー一覧情報の取得
$ lein uberjar
$ java -jar target/uberjar/rest-client-0.1.0-SNAPSHOT-standalone.jar http://localhost:3000/members GET
[{"first-name":"You","last-name":"Watanabe","email":"y.watanabe@uranohoshi.ac.jp","member-id":1},{"first-name":"Yoshiko","last-name":"Tsushima","email":"y.tsushima@uranohoshi.ac.jp","member-id":2}]
  • メンバーの登録
$ java -jar target/uberjar/rest-client-0.1.0-SNAPSHOT-standalone.jar http://localhost:3000/members POST
{"first-name": "Dia",
 "last-name": "Kurosawa",
 "email": "d.kurosawa@uranohoshi.ac.jp"}
{"first-name":"Dia","last-name":"Kurosawa","email":"d.kurosawa@uranohoshi.ac.jp","member-id":3}

Haskell版

HaskellでHTTPクライアントライブラリに何を利用するか、wreqreqなども検討したが、最終的には

Making HTTP requests - http-client library

を参考にhttp-conduitを利用することにした。

また、RESTサーバ実装と同様にJSON操作にはaesonを利用する。

1. プロジェクトの生成

Haskell版についても、今回は特にベースとなるフレームワーク/ライブラリはなしで単純にプロジェクト生成した。

$ stack new rest-client
$ cd rest-client

2. 実装

{-# LANGUAGE OverloadedStrings #-}
module Main where

import           Control.Monad              (when)
import           Data.Char                  (toUpper)
import           System.Environment         (getArgs)

import           Control.Lens               ((&))
import           Data.Aeson                 (Value, eitherDecode)
import           Data.Aeson.Text            (encodeToLazyText)
import qualified Data.ByteString.Lazy.Char8 as BLC
import qualified Data.Text.Lazy.IO          as L
import           Network.HTTP.Simple

main :: IO ()
main = do
    args <- getArgs
    when (length args /= 2) $ error "Exactly 2 arguments must be specified"
    let [url, method] = args
    res <- http url method
    let status = getResponseStatusCode res
    when (status /= 200) . putStr $ show status ++ "\t"
    L.putStrLn . encodeToLazyText $ getResponseBody res

http :: String -> String -> IO (Response Value)
http url method = do
    req <- parseRequest url
    case map toUpper method of
        "GET" -> httpJSON $ req & setRequestMethod "GET"
                                & setRequestHeader "Accept" ["application/json"]
        "POST" -> do
            json <- readJSONfromStdin
            httpJSON $ req & setRequestMethod "POST"
                           & setRequestBodyJSON json
                           & setRequestHeader "Accept" ["application/json"]
        _ -> error $ "Unsupported HTTP method: " ++ method

readJSONfromStdin :: IO Value
readJSONfromStdin = do
    input <- BLC.getContents
    return . either error id $ eitherDecode input

リクエストデータを構築する部分では、読みやすさのためにlens& を利用してみている(ここではControl.Lensのものを利用したが、Data.Functionで標準提供されているものでも良さそう)。

Clojure版とほぼ同等の振る舞いになるように実装してみた(エラー処理もClojure版と同様に直ちにエラーを発生させることにした)。

Haskellについてはまだまだ実用的なコードを書き慣れていないため、特に改善の余地がある気がする🤔

3. 動作確認

RESTサーバ(cf. README)をあらかじめ起動した状態で

$ stack build
$ stack exec rest-client-exe [args]

または

$ stack runghc app/Main.hs [args]

で実行できる。

例えば、

  • メンバー一覧情報の取得
$ stack build
$ stack exec rest-client-exe http://localhost:3000/members GET
[{"email":"y.watanabe@uranohoshi.ac.jp","member-id":1,"last-name":"Watanabe","first-name":"You"},{"email":"y.tsushima@uranohoshi.ac.jp","member-id":2,"last-name":"Tsushima","first-name":"Yoshiko"}]
  • メンバーの登録
$ stack exec rest-client-exe http://localhost:3000/members POST
{"first-name": "Dia",
 "last-name": "Kurosawa",
 "email": "d.kurosawa@uranohoshi.ac.jp"}
{"email":"d.kurosawa@uranohoshi.ac.jp","member-id":3,"last-name":"Kurosawa","first-name":"Dia"}

まとめ

  • ClojureでもHaskellでもHTTPアクセスしてJSONを扱うコマンドラインアプリが簡潔に実装できた
  • Clojure楽しい>ω</
  • Haskell楽しい>ω</

Further Reading

18
7
2

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
18
7