mirror of
https://github.com/monadicsystems/okapi.git
synced 2024-11-27 13:59:13 +03:00
217 lines
5.8 KiB
Haskell
217 lines
5.8 KiB
Haskell
{-# LANGUAGE DeriveAnyClass #-}
|
|
{-# LANGUAGE DeriveGeneric #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE PatternSynonyms #-}
|
|
{-# LANGUAGE RecordWildCards #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
|
|
module Main where
|
|
|
|
import Control.Applicative ((<|>))
|
|
import Control.Applicative.Combinators
|
|
import Control.Monad.IO.Class
|
|
import Control.Monad.Trans.Class
|
|
import Data.Aeson (ToJSON, toJSON)
|
|
import Data.ByteString.Lazy (fromStrict)
|
|
import Data.Function ((&))
|
|
import Data.Maybe (listToMaybe)
|
|
import Data.Text
|
|
import Data.Text.Encoding (encodeUtf8)
|
|
import Database.SQLite.Simple
|
|
import Database.SQLite.Simple.FromField
|
|
import Database.SQLite.Simple.ToField
|
|
import GHC.Generics (Generic, Par1)
|
|
import Okapi
|
|
import Web.FormUrlEncoded (FromForm)
|
|
import Web.HttpApiData (ToHttpApiData)
|
|
import Web.Internal.HttpApiData
|
|
|
|
{-
|
|
We're going to build a Todo API with the following endpoints:
|
|
|
|
GET /
|
|
Health check. Returns 200 response with "OK" as the body.
|
|
|
|
GET /todos
|
|
Returns all todos as JSON. Has optional query param "status" to filter todos with that status.
|
|
|
|
GET /todos/:uid
|
|
Returns the todo with the matching :uid as JSON.
|
|
|
|
POST /todos
|
|
Accepts a todo/todos encoded as JSON to store on the server.
|
|
|
|
PATCH /todos/:uid
|
|
Accepts todo information as JSON. For updating a todo with the :uid.
|
|
|
|
DELETE /todos/:uid
|
|
Deletes the todo with the matching :uid from the server.
|
|
-}
|
|
|
|
-- TYPES --
|
|
|
|
data Todo = Todo
|
|
{ todoID :: Int,
|
|
todoName :: Text,
|
|
todoStatus :: TodoStatus
|
|
}
|
|
deriving (Eq, Ord, Generic, ToJSON, Show)
|
|
|
|
instance FromRow Todo where
|
|
fromRow = Todo <$> field <*> field <*> field
|
|
|
|
data TodoForm = TodoForm
|
|
{ todoFormName :: Text,
|
|
todoFormStatus :: TodoStatus
|
|
}
|
|
deriving (Eq, Ord, Generic, FromForm, Show)
|
|
|
|
instance ToRow TodoForm where
|
|
toRow (TodoForm name status) = toRow (name, status)
|
|
|
|
data TodoStatus
|
|
= Incomplete
|
|
| Archived
|
|
| Complete
|
|
deriving (Eq, Ord, Show)
|
|
|
|
instance ToJSON TodoStatus where
|
|
toJSON Incomplete = "incomplete"
|
|
toJSON Archived = "archived"
|
|
toJSON Complete = "complete"
|
|
|
|
instance FromHttpApiData TodoStatus where
|
|
parseQueryParam "incomplete" = Right Incomplete
|
|
parseQueryParam "archived" = Right Archived
|
|
parseQueryParam "complete" = Right Complete
|
|
parseQueryParam _ = Left "Incorrect format for TodoStatus value"
|
|
|
|
instance ToField TodoStatus where
|
|
toField status =
|
|
case status of
|
|
Incomplete -> SQLText "incomplete"
|
|
Archived -> SQLText "archived"
|
|
Complete -> SQLText "complete"
|
|
|
|
instance FromField TodoStatus where
|
|
fromField field = do
|
|
case fieldData field of
|
|
SQLText "incomplete" -> pure Incomplete
|
|
SQLText "archived" -> pure Archived
|
|
SQLText "complete" -> pure Complete
|
|
_ -> returnError ConversionFailed field "Couldn't methodGET TodoStatus value from field"
|
|
|
|
type Okapi = OkapiT IO
|
|
|
|
-- MAIN --
|
|
|
|
main :: IO ()
|
|
main = do
|
|
conn <- open "todo.db"
|
|
execute_ conn "CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY, name TEXT, status TEXT)"
|
|
run id (todoAPI conn)
|
|
close conn
|
|
|
|
-- SERVER FUNCTIONS
|
|
|
|
respond :: Response -> Okapi Response
|
|
respond response = do
|
|
methodEnd
|
|
pathEnd
|
|
return response
|
|
|
|
pattern HealthCheck :: Path
|
|
pattern HealthCheck = [""]
|
|
|
|
pattern GetAllTodos :: Path
|
|
pattern GetAllTodos = ["todos"]
|
|
|
|
pattern GetTodo :: Int -> Path
|
|
pattern GetTodo todoID = ["todos", PathParam todoID]
|
|
|
|
pattern PutTodo :: Int -> Path
|
|
pattern PutTodo todoID = ["todos", PathParam todoID]
|
|
|
|
pattern CreateTodo :: Path
|
|
pattern CreateTodo = ["todos"]
|
|
|
|
pattern ForgetTodo :: Int -> Path
|
|
pattern ForgetTodo todoID = ["todos", PathParam todoID]
|
|
|
|
todoAPI :: Connection -> Okapi Response
|
|
todoAPI conn =
|
|
getRoutes conn
|
|
<|> postRoutes conn
|
|
<|> putRoutes conn
|
|
<|> deleteRoutes conn
|
|
|
|
getRoutes :: Connection -> Okapi Response
|
|
getRoutes conn = do
|
|
methodGET
|
|
route path $ \case
|
|
HealthCheck -> respond ok
|
|
GetTodo todoID -> do
|
|
maybeTodo <- lift $ selectTodo conn todoID
|
|
case maybeTodo of
|
|
Nothing -> throw internalServerError
|
|
Just todo -> ok & setJSON todo & respond
|
|
GetAllTodos -> do
|
|
maybeStatus <- optional $ queryParam @TodoStatus "status"
|
|
todos <- lift $ selectAllTodos conn maybeStatus
|
|
ok & setJSON todos & respond
|
|
_ -> next
|
|
|
|
postRoutes :: Connection -> Okapi Response
|
|
postRoutes conn = do
|
|
methodPOST
|
|
route path $ \case
|
|
CreateTodo -> do
|
|
todoForm <- bodyForm
|
|
lift $ insertTodoForm conn todoForm
|
|
respond ok
|
|
_ -> next
|
|
|
|
putRoutes :: Connection -> Okapi Response
|
|
putRoutes conn = do
|
|
methodPUT
|
|
route path $ \case
|
|
PutTodo todoID -> do
|
|
todoForm <- bodyForm @TodoForm
|
|
lift $ updateTodo conn todoID todoForm
|
|
respond ok
|
|
_ -> next
|
|
|
|
deleteRoutes :: Connection -> Okapi Response
|
|
deleteRoutes conn = do
|
|
methodDELETE
|
|
route path $ \case
|
|
ForgetTodo todoID -> do
|
|
lift $ deleteTodo conn todoID
|
|
respond ok
|
|
_ -> next
|
|
|
|
-- DATABASE FUNCTIONS
|
|
|
|
insertTodoForm :: Connection -> TodoForm -> IO ()
|
|
insertTodoForm conn = execute conn "INSERT INTO todos (name, status) VALUES (?, ?)"
|
|
|
|
selectTodo :: Connection -> Int -> IO (Maybe Todo)
|
|
selectTodo conn todoID = listToMaybe <$> Database.SQLite.Simple.query conn "SELECT * FROM todos WHERE id = ?" (Only todoID)
|
|
|
|
selectAllTodos :: Connection -> Maybe TodoStatus -> IO [Todo]
|
|
selectAllTodos conn maybeStatus = case maybeStatus of
|
|
Nothing -> query_ conn "SELECT * FROM todos"
|
|
Just status -> Database.SQLite.Simple.query conn "SELECT * FROM todos WHERE status = ?" (Only status)
|
|
|
|
updateTodo :: Connection -> Int -> TodoForm -> IO ()
|
|
updateTodo conn todoID TodoForm {..} =
|
|
executeNamed
|
|
conn
|
|
"UPDATE todos SET name = :name, status = :status WHERE id = :id"
|
|
[":id" := todoID, ":name" := todoFormName, ":status" := todoFormStatus]
|
|
|
|
deleteTodo :: Connection -> Int -> IO ()
|
|
deleteTodo conn todoID = execute conn "DELETE FROM todos WHERE id = ?" (Only todoID)
|