Scotty — это легковесный движок для создания веб-приложений на языке Haskell. Что-то вроде Cowboy из мира Erlang или Scalatra из мира Scala. Сегодня с помощью Scotty мы прикрутим веб-интерфейс к нашей телефонной книге .

Весь код веб-приложения находится в модуле Phonebook.Interface.HTTP. Как и модуль Phonebook.Interface.CLI, он экспортирует единственную функцию:

main :: IConnection a => a -> IO ( )

Посмотрим на ее код:

import qualified Web . Scotty as Sc
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:

create conn = do
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 определены так:

withNamePhone m action =
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:

read conn = do
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 Array = Vector Value
type Object = HashMap Text Value
data Value
= Object ! Object
| Array ! Array
| String ! Text
| Number ! Number
| Bool ! Bool
| Null

Использованная нами функция object имеет следующий тип:

object :: [ ( Text , Value ) ] -> Value

Как вы уже догадались, она преобразует ассоциативный список в JSON-объект.

Вообще aeson — очень приятная в использовании библиотека:

ghci> :set -XOverloadedStrings
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 «»[12

EnglishRussianUkrainian