Пишем простой RESTful сервис с использованием Scotty
7 августа 2013
Scotty — это легковесный движок для создания веб-приложений на языке Haskell. Что-то вроде Cowboy из мира Erlang или Scalatra из мира Scala. Сегодня с помощью Scotty мы прикрутим веб-интерфейс к нашей телефонной книге.
Весь код веб-приложения находится в модуле Phonebook.Interface.HTTP. Как и модуль Phonebook.Interface.CLI, он экспортирует единственную функцию:
Посмотрим на ее код:
import qualified Phonebook.Storage as St
{- ... -}
main conn = Sc.scotty 8080 $ do
Sc.post "/api/v1.0/contacts" (create conn)
Sc.get "/api/v1.0/contacts" (read conn)
Sc.put "/api/v1.0/contacts/:cid" (update conn)
Sc.delete "/api/v1.0/contacts/:cid" (delete conn)
Не будем заострять внимание на типах использованных здесь функций. Если очень интересно, можете заглянуть в документацию по Scotty, там все есть. Что тут происходит, понятно и без них. Scotty запускается на порту 8080. Также объявляются маршруты/роуты и соответствующие им хэндлеры.
Давайте посмотрим на функцию create:
body <- Sc.jsonData :: Sc.ActionM (M.Map String String)
success <- withNamePhone body (\n p -> St.create n p conn)
Sc.status $ boolToHttpCode success
Функция jsonData берет тело запроса и пытается декодировать его, как строку с JSON’ом. Здесь после декодирования мы ожидаем получить тип Map String String
. Для успешного выполнения запроса пользователь должен передать JSON-объект, значения полей которого представляют собой строки. Если пользователь пошлет невалидный JSON или валидный, но такой, что его не удастся привести к ожидаемому типу, Scotty вернет ошибку 500 Internal Server Error.
Функции withNamePhone и boolToHttpCode определены так:
case extractNamePhone m of
Nothing -> return False
Just (name, phone) -> liftIO $ action name phone
extractNamePhone :: M.Map String String -> Maybe (String, String)
extractNamePhone m =
M.lookup "name" m >>=
\name -> M.lookup "phone" m >>=
\phone -> Just (name, phone)
boolToHttpCode success =
if success then noContent204
else badRequest400
Надеюсь, вы в достаточной мере владеете особой монадической магией, чтобы понять этот код, потому что обсуждение этой магии, пожалуй, представляет собой хорошую тему для целого отдельного поста. Если в двух словах, наше приложение возвращает код 204 No Content, если запись в телефонной книге была успешно создана, и 400 Bad Request, если что-то пошло не так, например, не было указано имя или телефон.
Рассмотрим следующий хэндлер, функцию read:
contacts <- liftIO $ St.read conn
let objects = map toObj contacts
Sc.json $ Array (V.fromList objects)
where
toObj :: (St.ContactId, St.Name, St.Phone) -> Value
toObj (cid, name, phone) =
object [ ("contactId", Number $ I cid)
, ("name", String $ T.pack name)
, ("phone", String $ T.pack phone)
]
Здесь мы читаем из базы все имеющиеся контакты, преобразуем их в список JSON-объектов, а затем полученный список преобразуем в JSON-массив, который отдается пользователю. Для работы с JSON в Haskell большой популярностью пользуется пакет aeson. JSON в нем представляется следующим образом:
type Object = HashMap Text Value
data Value
= Object !Object
| Array !Array
| String !Text
| Number !Number
| Bool !Bool
| Null
Использованная нами функция object имеет следующий тип:
Как вы уже догадались, она преобразует ассоциативный список в JSON-объект.
Вообще aeson — очень приятная в использовании библиотека:
ghci> :m + Data.Aeson
ghci> import Data.ByteString.Lazy.Char8 as BSLC
ghci> BSLC.unpack $ encode $ object [("aaa", Number 123)]
"{\"aaa\":123}"
ghci> decode $ encode $ object [("aaa", Number 123)] :: Maybe (Object)
Just fromList [("aaa",Number 123)]
ghci> decode (BSLC.pack "[1,2,3]") :: Maybe [Int]
Just [1,2,3]
Помимо простого кодирования/декодирования JSON она также позволяет удобным образом писать сериализаторы и десериализаторы для произвольных типов. Подробности вы найдете в документации по пакету.
Определение функции update:
cid <- Sc.param "cid" :: Sc.ActionM Integer
body <- Sc.jsonData :: Sc.ActionM (M.Map String String)
success <- withNamePhone body (\n p -> St.update cid n p conn)
Sc.status $ boolToHttpCode success
Она очень похожа на функцию create за тем исключением, что здесь с помощью функции param мы вычленяем параметр cid из запрашиваемого URL. Интересное свойство этой функции заключается в том, что если параметр не удастся привести к ожидаемому типу, в данном случае — Integer, никакой ошибки не произойдет. Scotty просто продолжит матчить запрошенный URL с роутами далее по списку. Если ни один из оставшихся роутов не подойдет, будет возвращен код 404 Not Found.
Оставшаяся функция delete совсем простая:
cid <- Sc.param "cid" :: Sc.ActionM Integer
success <- liftIO $ St.delete cid conn
Sc.status $ boolToHttpCode success
Для нас здесь уже нет совершенно ничего нового. Вот и весь код! Давайте теперь посмотрим, как он работает.
Получение всех контактов из телефонной книги:
HTTP/1.1 200 OK
Server: Warp/1.3.8.4
Transfer-Encoding: chunked
Content-Type: application/json
[{"phone":"789","name":"Alex","contactId":17}]
Создание новой записи в телефонной книге:
-d '{"name":"Bob","phone":"456"}' -D - \
http://localhost:8080/api/v1.0/contacts
HTTP/1.1 204 No Content
Server: Warp/1.3.8.4
Редактирование записи в телефонной книге:
-d '{"name":"Bob2","phone":"123"}' -D -
http://localhost:8080/api/v1.0/contacts/18
HTTP/1.1 204 No Content
Server: Warp/1.3.8.4
Удаление записи:
HTTP/1.1 204 No Content
Server: Warp/1.3.8.4
Несколько важных моментов.
Во-первых, обратите внимание, что при создании и редактировании записей мы вручную указываем Content-type. Если этого не сделать, curl пошлет заголовок Content-type: application/x-www-form-urlencoded
и Scotty будет считать, что тело запроса пустое. Кстати, для отладки веб-приложений на Haskell неплохо работают curl -v
, tcpdump, а также модуль Debug.Trace.
Во-вторых, здесь мы использовали одно-единственное соединение с PostgreSQL. Однако в реальных приложениях требуется создавать пул соединений и следить за тем, чтобы два процесса не воспользовались одним и тем же соединением одновременно. Также нужно корректно обрабатывать ситуации вроде падения сети между приложением и СУБД.
Дополнительные материалы:
- Документация по Scotty на Hackage;
- Описание модуля Data.Aeson;
- 24 Days of Hackage: scotty;
- REST API Tutorial — хороший туториал по REST;
Исходники к посту вы найдете в этом архиве.
Дополнение: Использование кондуитов (conduits) в Haskell
Метки: Haskell, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.