Skip to content

Implement server for the REST API #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 31 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
72e5602
Add server conf to configuration handling
memowe Sep 12, 2023
00afc43
Improve arbitrary storage config instance
memowe Sep 12, 2023
53538da
Bugfix: fix example config.ini file (person-file)
memowe Sep 12, 2023
5230fb9
Update server source directory
memowe Sep 12, 2023
02a6aad
Add basic web service setup with "Hello world" route
memowe Sep 12, 2023
41f736f
Also test status with hello web service test
memowe Sep 12, 2023
8faac1b
Test all no-hello endpoints using QuickCheck
memowe Sep 13, 2023
8d3ab7f
Add serve to Makefile
memowe Sep 14, 2023
653bf5b
Merge branch 'main' into basic-webservice
memowe Nov 16, 2023
f0bd51c
Skip GHC 9.8 for now (dependency problems in this branch)
memowe Nov 16, 2023
9de8070
Restructure WebService code with JSON example
memowe Feb 16, 2024
7b1ba6d
Add config to web service actions
memowe Feb 19, 2024
94db821
Avoid identifier warnings (config/cfg) with LiBroState
memowe Feb 21, 2024
5705aa7
Move libro state modifiers to Control
memowe Feb 21, 2024
04579ba
Fix some intermediate warnings
memowe Feb 23, 2024
bee31ce
Gitignore LibreOffice swap files
memowe Feb 23, 2024
48f1c60
Redesign web service actions for the person IDs listing endpoint
memowe Feb 23, 2024
751fc69
Fix typos in default config
memowe Feb 23, 2024
58441d2
Add person details endpoint
memowe Feb 23, 2024
3bea836
Remove dummy endpoint
memowe Feb 23, 2024
10d5dcd
Restructure web service endpoint tests
memowe Feb 23, 2024
1555f16
Add top-level task ID listing endpoint
memowe Feb 23, 2024
11167e9
Improve listing endpoint test structure
memowe Feb 23, 2024
0a1aa2f
Handle bad person ID requests correctly
memowe Feb 23, 2024
35d67ce
Add tasks of a person endpoint
memowe Feb 23, 2024
9600bf3
Restructure and improve ID-only API
memowe Feb 26, 2024
b00490d
Add forest search to utilities
memowe Feb 27, 2024
9f6a8c1
Improve TaskForest handling
memowe Feb 28, 2024
2efbea6
Add task subtree endpoint
memowe Feb 28, 2024
49455c8
Merge branch 'main' into basic-webservice
memowe Aug 2, 2024
94a75e1
Merge branch 'main' into basic-webservice (forgot haddocks)
memowe Aug 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/haskell-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
ghc-version: ['9.4', '9.6', '9.8']
ghc-version: ['9.4', '9.6']
steps:

- name: Checkout repository content
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Vim swap files
*.sw[a-p]

# LibreOffice swap files
.~lock.*#

# Data storage files
data-storage/*.xlsx

# Generated documentstion
api-docs

Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all build warnings test test_only doc stats
.PHONY: all build warnings test test_only doc serve stats

all: build test doc stats

Expand All @@ -17,6 +17,9 @@ test_only:
doc:
cabal haddock --haddock-hyperlinked-source --haddock-html-location='https://hackage.haskell.org/package/$$pkg-$$version/docs'

serve:
cabal run libro-backend

stats:
find lib -name '*.hs' -not -path "./dist-newstyle/*" | sort | xargs wc -l
find test -name '*.hs' -not -path "./dist-newstyle/*" | sort | xargs wc -l
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ Haskell dependencies:
cabal install --only-dependencies all
```

## Run the RESTful JSON web service

```
make serve
```

## Run tests

Running all the tests with `make test` may take some time. Run individual tests with
Expand Down
4 changes: 0 additions & 4 deletions app/Main.hs

This file was deleted.

8 changes: 6 additions & 2 deletions config.ini
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
[storage]
directory = data-storage
tasks-file = tasks.csv
tracking-file = tracking.csv
person-file = persons.xlsx
tasks-file = tasks.xlsx
tracking-file = tracking.xlsx

[server]
port = 8080
15 changes: 13 additions & 2 deletions lib/LiBro/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,22 @@ data StorageConfig = Storage
instance Default StorageConfig where
def = Storage "data-storage" "persons.xlsx" "tasks.xlsx" "tracking.xlsx"

-- | Configuration of server details
data ServerConfig = Server
{ port :: Int
} deriving (Eq, Show)

instance Default ServerConfig where
def = Server 8080

-- | Global settings.
data Config = Config
{ storage :: StorageConfig
, server :: ServerConfig
} deriving (Eq, Show)

instance Default Config where
def = Config def
def = Config def def

-- | Parses a 'Config' value from a given 'Text'
-- or gives a parsing error message.
Expand All @@ -35,7 +44,9 @@ parseConfig = flip parseIniFile $ do
<*> fieldOf "person-file" string
<*> fieldOf "tasks-file" string
<*> fieldOf "tracking-file" string
return $ Config st
srv <- section "server" $
Server <$> fieldOf "port" number
return $ Config st srv

-- | Reads a 'Config' value from @config.ini@.
-- Prints parsing error messages to @STDERR@ when failing.
Expand Down
52 changes: 52 additions & 0 deletions lib/LiBro/Control.hs
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,55 @@ saveData blocking libroData = do
storeData =<< liftIO (readMVar libroData)
_ <- liftIO $ takeMVar blocking
return True

-- | Shared libro system state to access data any time.
data LiBroState = LiBroState
{ config :: Config
, mvBlocking :: MVar Blocking
, mvData :: MVar LiBroData
}

-- | Initialization of a 'LiBroState'.
initLiBroState :: LiBro LiBroState
initLiBroState = do
mvb <- liftIO newEmptyMVar
mvd <- liftIO newEmptyMVar
initData mvb mvd
cfg <- ask
return $ LiBroState cfg mvb mvd

-- | Type alias for actions holding a 'LiBroState' inside 'ReaderT'.
type Action = ReaderT LiBroState IO

-- | 'Config' accessor action.
lsConfig :: Action Config
lsConfig = asks config

-- | Checks whether the system is blocked
-- and by what type of 'Blocking' action.
lsBlockedBy :: Action (Maybe Blocking)
lsBlockedBy = do
mvb <- asks mvBlocking
lift $ tryTakeMVar mvb

-- | 'LiBroData' accessor action.
lsData :: Action LiBroData
lsData = do
mvd <- asks mvData
lift $ readMVar mvd

-- | 'initData' action.
lsInitData :: Action ()
lsInitData = do
cfg <- asks config
mvb <- asks mvBlocking
mvd <- asks mvData
lift $ runLiBro cfg $ initData mvb mvd

-- | 'saveData' action.
lsSaveData :: Action Bool
lsSaveData = do
cfg <- asks config
mvb <- asks mvBlocking
mvd <- asks mvData
lift $ runLiBro cfg $ saveData mvb mvd
2 changes: 1 addition & 1 deletion lib/LiBro/Data.hs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type Tasks = Forest Task

-- | Find all 'Task's assigned to a given 'Person'.
assignedTasks :: Person -> Tasks -> [Task]
assignedTasks p = filter ((p `elem`) . assignees) . concatMap flatten
assignedTasks p = concatMap (filter ((p `elem`) . assignees) . flatten)

-- | Complete LiBro state in one type
data LiBroData = LBS
Expand Down
10 changes: 9 additions & 1 deletion lib/LiBro/Util.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
-- in more than one place.
module LiBro.Util
(
-- * Tree building
-- * Tree utilities
ParentList
, readForest
, findSubtree
-- * Counting monad transformer
, CountingT
, next
Expand Down Expand Up @@ -59,6 +60,13 @@ readForest pairs =
Nothing -> []; Just [] -> []
Just xs -> fill cs <$> sort xs

-- | Find the first matching subtree of a forest
findSubtree :: (a -> Bool) -> Forest a -> Maybe (Tree a)
findSubtree p = asum . map findTree
where findTree t@(Node x cs)
| p x = Just t
| otherwise = findSubtree p cs

-- | Simple monad transformer that allows to read an increasing 'Int'.
type CountingT m = StateT Int m

Expand Down
83 changes: 83 additions & 0 deletions lib/LiBro/WebService.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
module LiBro.WebService where

import LiBro.Control
import LiBro.Data
import LiBro.Util
import qualified Data.Map as M
import Data.Tree
import Data.Aeson
import Data.Proxy
import Servant
import Control.Monad.Reader
import GHC.Generics

type LiBroHandler = ReaderT LiBroState Handler

runAction :: Action a -> LiBroHandler a
runAction action = ask >>= liftIO . runReaderT action

data PersonDetails = PersonDetails
{ person :: Person
, personTasks :: [Task]
} deriving Generic
instance ToJSON PersonDetails

-- JSON-friendly rewrite of Forest/Tree, their ToJSON instance is weird
type TaskForest = [TaskTree]
data TaskTree = TaskTree
{ task :: Task
, subTasks :: TaskForest
} deriving Generic
instance ToJSON TaskTree

convertTaskTree :: Tree Task -> TaskTree
convertTaskTree (Node t ts) = TaskTree t (convertTasksForest ts)

convertTasksForest :: Tasks -> TaskForest
convertTasksForest = map convertTaskTree

type LiBroAPI =
"person" :> Get '[JSON] [Person]
:<|> "person" :> Capture "pid" Int :> Get '[JSON] PersonDetails
:<|> "task" :> Get '[JSON] [Task]
:<|> "task" :> "tree" :> Get '[JSON] TaskForest
:<|> "task" :> Capture "tid" Int :> Get '[JSON] TaskTree

libroServer :: ServerT LiBroAPI LiBroHandler
libroServer = hPersonList
:<|> hPersonDetails
:<|> hTaskTopLevelList
:<|> hTaskFullForest
:<|> hTaskDetails
where
hPersonList :: LiBroHandler [Person]
hPersonList = M.elems . persons <$> runAction lsData

hPersonDetails :: Int -> LiBroHandler PersonDetails
hPersonDetails pId = do
d <- runAction lsData
case M.lookup pId (persons d) of
Just p -> let ts = assignedTasks p (tasks d)
in return $ PersonDetails p ts
Nothing -> throwError err404 {errBody = "Person not found"}

hTaskTopLevelList :: LiBroHandler [Task]
hTaskTopLevelList = map rootLabel . tasks <$> runAction lsData

hTaskFullForest :: LiBroHandler TaskForest
hTaskFullForest = convertTasksForest . tasks <$> runAction lsData

hTaskDetails :: Int -> LiBroHandler TaskTree
hTaskDetails tId = do
result <- findSubtree ((== tId) . tid) . tasks <$> runAction lsData
case result of
Just tree -> return $ convertTaskTree tree
Nothing -> throwError err404 {errBody = "Task not found"}

libroApi :: Proxy LiBroAPI
libroApi = Proxy

libro :: LiBroState -> Application
libro initState =
let server = hoistServer libroApi (`runReaderT` initState) libroServer
in serve libroApi server
13 changes: 12 additions & 1 deletion libro-backend.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,15 @@ library
default-extensions: OverloadedStrings
, GeneralizedNewtypeDeriving
, DeriveGeneric
, DataKinds
, TypeOperators
exposed-modules: LiBro.Base
, LiBro.Config
, LiBro.Data
, LiBro.Data.Storage
, LiBro.Data.SafeText
, LiBro.Control
, LiBro.WebService
, LiBro.Util
build-depends: aeson
, attoparsec
Expand All @@ -55,6 +58,8 @@ library
, mtl
, process
, QuickCheck
, servant
, servant-server
, temporary
, text
, unordered-containers
Expand All @@ -65,11 +70,13 @@ executable libro-backend
import: consumer
main-is: Main.hs
build-depends: libro-backend
hs-source-dirs: app
, warp
hs-source-dirs: server

test-suite libro-backend-test
import: consumer
default-extensions: OverloadedStrings
, QuasiQuotes
, DeriveGeneric
type: exitcode-stdio-1.0
hs-source-dirs: test
Expand All @@ -80,10 +87,13 @@ test-suite libro-backend-test
, LiBro.Data.StorageSpec
, LiBro.Data.SafeTextSpec
, LiBro.ControlSpec
, LiBro.WebServiceSpec
, LiBro.UtilSpec
main-is: run-all-tests.hs
build-depends: libro-backend
, hspec
, hspec-wai
, hspec-wai-json
, QuickCheck
, quickcheck-text
, generic-arbitrary
Expand All @@ -101,3 +111,4 @@ test-suite libro-backend-test
, text
, transformers
, vector
, wai
18 changes: 18 additions & 0 deletions server/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module Main where

import LiBro.Base
import LiBro.Config
import LiBro.Control
import LiBro.WebService
import Network.Wai.Handler.Warp

configuredMain :: Config -> IO ()
configuredMain cfg = do
let p = port $ server cfg
putStrLn $ "Serving LiBro backend on port " ++ show p ++ "."
initState <- runLiBro cfg initLiBroState
run p $ libro initState

main :: IO ()
main = readConfig >>= maybe complain configuredMain
where complain = putStrLn "Invalid config: aborting"
Loading
Loading