From 72e56024ef6bf34aecd7574ad033b2f5413afdc9 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Tue, 12 Sep 2023 15:32:36 +0200 Subject: [PATCH 01/28] Add server conf to configuration handling --- config.ini | 3 +++ lib/LiBro/Config.hs | 15 +++++++++++++-- test/LiBro/ConfigSpec.hs | 31 ++++++++++++++++++++++--------- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/config.ini b/config.ini index 2ed888e..fbf32ee 100644 --- a/config.ini +++ b/config.ini @@ -2,3 +2,6 @@ directory = data-storage tasks-file = tasks.csv tracking-file = tracking.csv + +[server] +port = 8080 diff --git a/lib/LiBro/Config.hs b/lib/LiBro/Config.hs index 1e6a3c5..a906cf7 100644 --- a/lib/LiBro/Config.hs +++ b/lib/LiBro/Config.hs @@ -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. @@ -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. diff --git a/test/LiBro/ConfigSpec.hs b/test/LiBro/ConfigSpec.hs index 0c4348d..9c376d6 100644 --- a/test/LiBro/ConfigSpec.hs +++ b/test/LiBro/ConfigSpec.hs @@ -18,22 +18,28 @@ import System.IO.Silently writeConfig :: Config -> Text writeConfig c = T.unlines [ "[storage]" - , "directory = " <> T.pack (directory s) - , "person-file = " <> T.pack (personFile s) - , "tasks-file = " <> T.pack (tasksFile s) - , "tracking-file = " <> T.pack (trackingFile s) + , "directory = " <> T.pack (directory st) + , "person-file = " <> T.pack (personFile st) + , "tasks-file = " <> T.pack (tasksFile st) + , "tracking-file = " <> T.pack (trackingFile st) + , "" + , "[server]" + , "port = " <> T.pack (show $ port srv) ] <> "\n" - where s = storage c + where st = storage c + srv = server c instance Arbitrary Config where arbitrary = do - st <- Storage <$> name <*> name <*> name <*> name - return $ Config st + st <- Storage <$> name <*> name <*> name <*> name + srv <- Server <$> port + return $ Config st srv where chars = [choose ('a','z'), choose ('A','Z'), return '/'] name = do a <- oneof chars z <- oneof chars as <- listOf $ oneof (return ' ' : chars) return (a : as ++ [z]) + port = elements [1024 .. 49151] -- Wikipedia "Registered port" spec :: Spec spec = describe "INI file configuration" $ do @@ -49,6 +55,9 @@ defaultConfig = describe "Default config values" $ do it "person file" $ personFile st `shouldBe` "persons.xlsx" it "tasks file" $ tasksFile st `shouldBe` "tasks.xlsx" it "tracking file" $ trackingFile st `shouldBe` "tracking.xlsx" + describe "Server configuration" $ do + let srv = server dc + it "port" $ port srv `shouldBe` 8080 where dc = def :: Config parsing :: Spec @@ -56,7 +65,9 @@ parsing = describe "Configuration parsing" $ do context "With simple values" $ it "parse correct simple values" $ do - let simple = Config $ Storage "foo" "bar" "baz" "quux" + let simple = Config + (Storage "foo" "bar" "baz" "quux") + (Server 1742) parseConfig (writeConfig simple) `shouldBe` Right simple context "With invalid ini input" $ @@ -71,7 +82,9 @@ reading :: Spec reading = describe "Reading configuration from file" $ do context "With existing test config file" $ do - let simple = Config $ Storage "bar" "baz" "quux" "quuux" + let simple = Config + (Storage "bar" "baz" "quux" "quuux") + (Server 4217) config <- runIO $ withSystemTempFile "config.ini" $ \fp h -> do hPutStr h (T.unpack $ writeConfig simple) >> hClose h readConfigFrom fp From 00afc4325692e56e158f1e2838927554cd308508 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Tue, 12 Sep 2023 15:37:29 +0200 Subject: [PATCH 02/28] Improve arbitrary storage config instance --- test/LiBro/ConfigSpec.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/LiBro/ConfigSpec.hs b/test/LiBro/ConfigSpec.hs index 9c376d6..939328e 100644 --- a/test/LiBro/ConfigSpec.hs +++ b/test/LiBro/ConfigSpec.hs @@ -34,10 +34,10 @@ instance Arbitrary Config where st <- Storage <$> name <*> name <*> name <*> name srv <- Server <$> port return $ Config st srv - where chars = [choose ('a','z'), choose ('A','Z'), return '/'] - name = do a <- oneof chars - z <- oneof chars - as <- listOf $ oneof (return ' ' : chars) + where chars = '/' : ['a'..'z'] ++ ['A'..'Z'] + name = do a <- elements chars + z <- elements chars + as <- listOf $ elements (' ' : chars) return (a : as ++ [z]) port = elements [1024 .. 49151] -- Wikipedia "Registered port" From 53538da3e08fa592cd12c0262525f95f83cd76ef Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Tue, 12 Sep 2023 15:45:16 +0200 Subject: [PATCH 03/28] Bugfix: fix example config.ini file (person-file) --- config.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/config.ini b/config.ini index fbf32ee..1ad8937 100644 --- a/config.ini +++ b/config.ini @@ -1,5 +1,6 @@ [storage] directory = data-storage +person-file = persons.csv tasks-file = tasks.csv tracking-file = tracking.csv From 5230fb9116e135af4f84cefc1f2b0c6fab444624 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Tue, 12 Sep 2023 16:15:12 +0200 Subject: [PATCH 04/28] Update server source directory --- libro-backend.cabal | 2 +- {app => server}/Main.hs | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {app => server}/Main.hs (100%) diff --git a/libro-backend.cabal b/libro-backend.cabal index 7c961cf..2953980 100644 --- a/libro-backend.cabal +++ b/libro-backend.cabal @@ -57,7 +57,7 @@ executable libro-backend main-is: Main.hs build-depends: base >=4.14.0.0 , libro-backend - hs-source-dirs: app + hs-source-dirs: server test-suite libro-backend-test default-language: Haskell2010 diff --git a/app/Main.hs b/server/Main.hs similarity index 100% rename from app/Main.hs rename to server/Main.hs From 02a6aad2eb6f899e374b477793c70f8514d2c9a0 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Tue, 12 Sep 2023 16:45:56 +0200 Subject: [PATCH 05/28] Add basic web service setup with "Hello world" route --- README.md | 6 ++++++ lib/LiBro/WebService/API.hs | 9 +++++++++ lib/LiBro/WebService/Server.hs | 10 ++++++++++ libro-backend.cabal | 9 +++++++++ server/Main.hs | 13 ++++++++++++- test/LiBro/WebServiceSpec.hs | 21 +++++++++++++++++++++ 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 lib/LiBro/WebService/API.hs create mode 100644 lib/LiBro/WebService/Server.hs create mode 100644 test/LiBro/WebServiceSpec.hs diff --git a/README.md b/README.md index ef43295..ec80369 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,12 @@ Haskell dependencies: cabal install --only-dependencies all ``` +## Run the RESTful JSON web service + +``` +cabal run libro-backend +``` + ## Run tests Running all the tests with `make test` may take some time. Run individual tests with diff --git a/lib/LiBro/WebService/API.hs b/lib/LiBro/WebService/API.hs new file mode 100644 index 0000000..fdc9231 --- /dev/null +++ b/lib/LiBro/WebService/API.hs @@ -0,0 +1,9 @@ +module LiBro.WebService.API where + +import Data.Proxy +import Servant.API + +type LiBroAPI = "hello" :> Get '[PlainText] String + +libroApi :: Proxy LiBroAPI +libroApi = Proxy diff --git a/lib/LiBro/WebService/Server.hs b/lib/LiBro/WebService/Server.hs new file mode 100644 index 0000000..cb73d7e --- /dev/null +++ b/lib/LiBro/WebService/Server.hs @@ -0,0 +1,10 @@ +module LiBro.WebService.Server where + +import LiBro.WebService.API +import Servant + +server :: Server LiBroAPI +server = return "Hello LiBro!" + +libro :: Application +libro = serve libroApi server diff --git a/libro-backend.cabal b/libro-backend.cabal index 2953980..f43b992 100644 --- a/libro-backend.cabal +++ b/libro-backend.cabal @@ -27,11 +27,15 @@ library default-language: Haskell2010 default-extensions: OverloadedStrings , DeriveGeneric + , DataKinds + , TypeOperators exposed-modules: LiBro.Config , LiBro.Data , LiBro.Data.Storage , LiBro.Data.SafeText , LiBro.Control + , LiBro.WebService.API + , LiBro.WebService.Server , LiBro.Util build-depends: base >=4.14.0.0 , aeson @@ -46,6 +50,8 @@ library , mtl , process , QuickCheck + , servant + , servant-server , temporary , text , unordered-containers @@ -57,6 +63,7 @@ executable libro-backend main-is: Main.hs build-depends: base >=4.14.0.0 , libro-backend + , warp hs-source-dirs: server test-suite libro-backend-test @@ -72,10 +79,12 @@ 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: base >=4.14.0.0 , hspec + , hspec-wai , QuickCheck , quickcheck-text , generic-arbitrary diff --git a/server/Main.hs b/server/Main.hs index ef52089..93bc118 100644 --- a/server/Main.hs +++ b/server/Main.hs @@ -1,4 +1,15 @@ module Main where +import LiBro.Config as Conf +import LiBro.WebService.Server +import Network.Wai.Handler.Warp + +configuredMain :: Config -> IO () +configuredMain config = do + let port = Conf.port $ Conf.server config + putStrLn $ "Serving LiBro backend on port " ++ show port ++ "." + run port libro + main :: IO () -main = putStrLn "42" +main = readConfig >>= maybe complain configuredMain + where complain = putStrLn "Invalid config: aborting" diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs new file mode 100644 index 0000000..0edb0f4 --- /dev/null +++ b/test/LiBro/WebServiceSpec.hs @@ -0,0 +1,21 @@ +module LiBro.WebServiceSpec where + +import Test.Hspec +import Test.Hspec.Wai + +import LiBro.WebService.Server + +spec :: Spec +spec = describe "RESTful JSON web service" $ do + helloLibro + +helloLibro :: Spec +helloLibro = describe "Dummy: hello libro!" $ with (return libro) $ do + + describe "Root" $ do + it "Respond with 404" $ do + get "/" `shouldRespondWith` 404 + + describe "Hello endpoint" $ do + it "Respond with greeting" $ do + get "/hello" `shouldRespondWith` "Hello LiBro!" From 41f736f2b3e364a138d82b875d9171722f97fd9e Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Tue, 12 Sep 2023 18:05:36 +0200 Subject: [PATCH 06/28] Also test status with hello web service test --- test/LiBro/WebServiceSpec.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs index 0edb0f4..e800cd8 100644 --- a/test/LiBro/WebServiceSpec.hs +++ b/test/LiBro/WebServiceSpec.hs @@ -17,5 +17,5 @@ helloLibro = describe "Dummy: hello libro!" $ with (return libro) $ do get "/" `shouldRespondWith` 404 describe "Hello endpoint" $ do - it "Respond with greeting" $ do - get "/hello" `shouldRespondWith` "Hello LiBro!" + it "Respond with 200 greeting" $ do + get "/hello" `shouldRespondWith` "Hello LiBro!" {matchStatus = 200} From 8faac1b6c1696fa22cecb092caf8bee3a90138d5 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Wed, 13 Sep 2023 15:30:33 +0200 Subject: [PATCH 07/28] Test all no-hello endpoints using QuickCheck --- test/LiBro/WebServiceSpec.hs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs index e800cd8..ac6f6e9 100644 --- a/test/LiBro/WebServiceSpec.hs +++ b/test/LiBro/WebServiceSpec.hs @@ -2,8 +2,10 @@ module LiBro.WebServiceSpec where import Test.Hspec import Test.Hspec.Wai +import Test.Hspec.Wai.QuickCheck import LiBro.WebService.Server +import Data.ByteString spec :: Spec spec = describe "RESTful JSON web service" $ do @@ -12,10 +14,12 @@ spec = describe "RESTful JSON web service" $ do helloLibro :: Spec helloLibro = describe "Dummy: hello libro!" $ with (return libro) $ do - describe "Root" $ do - it "Respond with 404" $ do - get "/" `shouldRespondWith` 404 - describe "Hello endpoint" $ do it "Respond with 200 greeting" $ do get "/hello" `shouldRespondWith` "Hello LiBro!" {matchStatus = 200} + + describe "Any other endpoint" $ do + it "Respond with 404" $ do + property $ \endpoint -> + show endpoint /= "hello" ==> + get (pack endpoint) `shouldRespondWith` 404 From 8d3ab7fa274ae62e817e01cc3422eca26275031e Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Thu, 14 Sep 2023 09:09:00 +0200 Subject: [PATCH 08/28] Add serve to Makefile --- Makefile | 5 ++++- README.md | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 4ceaf2f..cf1d584 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build test test_only doc stats +.PHONY: all build test test_only doc serve stats all: build test doc stats @@ -14,6 +14,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 diff --git a/README.md b/README.md index ec80369..255ee10 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ cabal install --only-dependencies all ## Run the RESTful JSON web service ``` -cabal run libro-backend +make serve ``` ## Run tests From f0bd51cc925a309b196be4fbb9b3734751c99f5d Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Thu, 16 Nov 2023 11:58:19 +0100 Subject: [PATCH 09/28] Skip GHC 9.8 for now (dependency problems in this branch) --- .github/workflows/haskell-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml index 2601972..a183f4f 100644 --- a/.github/workflows/haskell-ci.yml +++ b/.github/workflows/haskell-ci.yml @@ -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 From 9de8070bbbc8646b54b5a8e5e968cf2cf17f1834 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Fri, 16 Feb 2024 14:35:31 +0100 Subject: [PATCH 10/28] Restructure WebService code with JSON example --- lib/LiBro/WebService.hs | 29 +++++++++++++++++++++++++++++ lib/LiBro/WebService/API.hs | 9 --------- lib/LiBro/WebService/Server.hs | 10 ---------- lib/LiBro/WebService/State.hs | 25 +++++++++++++++++++++++++ libro-backend.cabal | 7 +++++-- server/Main.hs | 2 +- test/LiBro/WebServiceSpec.hs | 13 ++++++++++--- test/run-all-tests.hs | 2 ++ 8 files changed, 72 insertions(+), 25 deletions(-) create mode 100644 lib/LiBro/WebService.hs delete mode 100644 lib/LiBro/WebService/API.hs delete mode 100644 lib/LiBro/WebService/Server.hs create mode 100644 lib/LiBro/WebService/State.hs diff --git a/lib/LiBro/WebService.hs b/lib/LiBro/WebService.hs new file mode 100644 index 0000000..fe44060 --- /dev/null +++ b/lib/LiBro/WebService.hs @@ -0,0 +1,29 @@ +module LiBro.WebService where + +import Data.Aeson +import Data.Proxy +import Servant +import GHC.Generics + +newtype PersonIDs = PersonIDs {personIDs :: [Int]} deriving Generic +instance ToJSON PersonIDs + +type LiBroAPI = "hello" :> Get '[JSON] PersonIDs + :<|> "yay" :> Get '[PlainText] String + +libroServer :: Server LiBroAPI +libroServer = handleHello + :<|> handleYay + where + handleHello :: Handler PersonIDs + handleHello = return $ PersonIDs [17, 42] + + handleYay :: Handler String + handleYay = return "Yay!" + +libroApi :: Proxy LiBroAPI +libroApi = Proxy + +libro :: Application +libro = serve libroApi libroServer + diff --git a/lib/LiBro/WebService/API.hs b/lib/LiBro/WebService/API.hs deleted file mode 100644 index fdc9231..0000000 --- a/lib/LiBro/WebService/API.hs +++ /dev/null @@ -1,9 +0,0 @@ -module LiBro.WebService.API where - -import Data.Proxy -import Servant.API - -type LiBroAPI = "hello" :> Get '[PlainText] String - -libroApi :: Proxy LiBroAPI -libroApi = Proxy diff --git a/lib/LiBro/WebService/Server.hs b/lib/LiBro/WebService/Server.hs deleted file mode 100644 index cb73d7e..0000000 --- a/lib/LiBro/WebService/Server.hs +++ /dev/null @@ -1,10 +0,0 @@ -module LiBro.WebService.Server where - -import LiBro.WebService.API -import Servant - -server :: Server LiBroAPI -server = return "Hello LiBro!" - -libro :: Application -libro = serve libroApi server diff --git a/lib/LiBro/WebService/State.hs b/lib/LiBro/WebService/State.hs new file mode 100644 index 0000000..6beec0f --- /dev/null +++ b/lib/LiBro/WebService/State.hs @@ -0,0 +1,25 @@ +module LiBro.WebService.State where + +import LiBro.Config +import LiBro.Data +import LiBro.Control +import Control.Concurrent + +data LiBroState = LiBroState + { config :: Config + , mvBlocking :: MVar Blocking + , mvData :: MVar LiBroData + } + +lsConfig :: LiBroState -> IO Config +lsConfig = return . config + +lsData :: LiBroState -> IO LiBroData +lsData = readMVar . mvData + +lsInit :: Config -> IO LiBroState +lsInit config = do + mvb <- newEmptyMVar + mvd <- newEmptyMVar + initData config mvb mvd + return $ LiBroState config mvb mvd diff --git a/libro-backend.cabal b/libro-backend.cabal index fda5d1d..fefc78d 100644 --- a/libro-backend.cabal +++ b/libro-backend.cabal @@ -41,8 +41,8 @@ library , LiBro.Data.Storage , LiBro.Data.SafeText , LiBro.Control - , LiBro.WebService.API - , LiBro.WebService.Server + , LiBro.WebService + , LiBro.WebService.State , LiBro.Util build-depends: aeson , attoparsec @@ -75,6 +75,7 @@ executable libro-backend test-suite libro-backend-test import: consumer default-extensions: OverloadedStrings + , QuasiQuotes , DeriveGeneric type: exitcode-stdio-1.0 hs-source-dirs: test @@ -91,6 +92,7 @@ test-suite libro-backend-test build-depends: libro-backend , hspec , hspec-wai + , hspec-wai-json , QuickCheck , quickcheck-text , generic-arbitrary @@ -108,3 +110,4 @@ test-suite libro-backend-test , text , transformers , vector + , wai diff --git a/server/Main.hs b/server/Main.hs index 93bc118..2b6a3a9 100644 --- a/server/Main.hs +++ b/server/Main.hs @@ -1,7 +1,7 @@ module Main where import LiBro.Config as Conf -import LiBro.WebService.Server +import LiBro.WebService import Network.Wai.Handler.Warp configuredMain :: Config -> IO () diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs index ac6f6e9..33d2be5 100644 --- a/test/LiBro/WebServiceSpec.hs +++ b/test/LiBro/WebServiceSpec.hs @@ -2,9 +2,10 @@ module LiBro.WebServiceSpec where import Test.Hspec import Test.Hspec.Wai +import Test.Hspec.Wai.JSON import Test.Hspec.Wai.QuickCheck -import LiBro.WebService.Server +import LiBro.WebService import Data.ByteString spec :: Spec @@ -14,9 +15,15 @@ spec = describe "RESTful JSON web service" $ do helloLibro :: Spec helloLibro = describe "Dummy: hello libro!" $ with (return libro) $ do - describe "Hello endpoint" $ do + describe "Yay endpoint" $ do it "Respond with 200 greeting" $ do - get "/hello" `shouldRespondWith` "Hello LiBro!" {matchStatus = 200} + get "/yay" `shouldRespondWith` "Yay!" {matchStatus = 200} + + describe "Dummy person ID endpoint" $ do + it "Respond with IDs" $ do + get "/hello" `shouldRespondWith` + [json|{"personIDs":[17,42]}|] + {matchStatus = 200} describe "Any other endpoint" $ do it "Respond with 404" $ do diff --git a/test/run-all-tests.hs b/test/run-all-tests.hs index d5459a5..a048e30 100644 --- a/test/run-all-tests.hs +++ b/test/run-all-tests.hs @@ -10,6 +10,7 @@ import qualified LiBro.ConfigSpec as Config import qualified LiBro.ControlSpec as Control import qualified LiBro.TestUtilSpec as TestUtil import qualified LiBro.UtilSpec as Util +import qualified LiBro.WebServiceSpec as WebService withLibreOffice :: IO () -> IO () withLibreOffice runTests = do @@ -31,3 +32,4 @@ main = hspec $ aroundAll_ withLibreOffice $ do Control.spec TestUtil.spec Util.spec + WebService.spec From 7b1ba6dca9e6c259770129cbc5a5e380bb215611 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Mon, 19 Feb 2024 10:30:00 +0100 Subject: [PATCH 11/28] Add config to web service actions --- lib/LiBro/WebService.hs | 12 ++++++------ server/Main.hs | 6 +++--- test/LiBro/WebServiceSpec.hs | 6 +++++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/LiBro/WebService.hs b/lib/LiBro/WebService.hs index fe44060..cfececb 100644 --- a/lib/LiBro/WebService.hs +++ b/lib/LiBro/WebService.hs @@ -1,5 +1,6 @@ module LiBro.WebService where +import LiBro.Config import Data.Aeson import Data.Proxy import Servant @@ -11,9 +12,9 @@ instance ToJSON PersonIDs type LiBroAPI = "hello" :> Get '[JSON] PersonIDs :<|> "yay" :> Get '[PlainText] String -libroServer :: Server LiBroAPI -libroServer = handleHello - :<|> handleYay +libroServer :: Config -> Server LiBroAPI +libroServer cfg = handleHello + :<|> handleYay where handleHello :: Handler PersonIDs handleHello = return $ PersonIDs [17, 42] @@ -24,6 +25,5 @@ libroServer = handleHello libroApi :: Proxy LiBroAPI libroApi = Proxy -libro :: Application -libro = serve libroApi libroServer - +libro :: Config -> Application +libro = serve libroApi . libroServer diff --git a/server/Main.hs b/server/Main.hs index 2b6a3a9..2a4ed5b 100644 --- a/server/Main.hs +++ b/server/Main.hs @@ -5,10 +5,10 @@ import LiBro.WebService import Network.Wai.Handler.Warp configuredMain :: Config -> IO () -configuredMain config = do - let port = Conf.port $ Conf.server config +configuredMain cfg = do + let port = Conf.port $ Conf.server cfg putStrLn $ "Serving LiBro backend on port " ++ show port ++ "." - run port libro + run port (libro cfg) main :: IO () main = readConfig >>= maybe complain configuredMain diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs index 33d2be5..961fe09 100644 --- a/test/LiBro/WebServiceSpec.hs +++ b/test/LiBro/WebServiceSpec.hs @@ -5,7 +5,9 @@ import Test.Hspec.Wai import Test.Hspec.Wai.JSON import Test.Hspec.Wai.QuickCheck +import LiBro.Config import LiBro.WebService +import Data.Default import Data.ByteString spec :: Spec @@ -13,7 +15,7 @@ spec = describe "RESTful JSON web service" $ do helloLibro helloLibro :: Spec -helloLibro = describe "Dummy: hello libro!" $ with (return libro) $ do +helloLibro = describe "Dummy: hello libro!" $ with (return cfgLibro) $ do describe "Yay endpoint" $ do it "Respond with 200 greeting" $ do @@ -30,3 +32,5 @@ helloLibro = describe "Dummy: hello libro!" $ with (return libro) $ do property $ \endpoint -> show endpoint /= "hello" ==> get (pack endpoint) `shouldRespondWith` 404 + + where cfgLibro = libro $ Config def def From 94db8213b8779856eb20bb647bab098703adfdaa Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Wed, 21 Feb 2024 12:01:45 +0100 Subject: [PATCH 12/28] Avoid identifier warnings (config/cfg) with LiBroState --- lib/LiBro/Control.hs | 8 ++++---- test/LiBro/ControlSpec.hs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/LiBro/Control.hs b/lib/LiBro/Control.hs index 01dfc8b..e8badd6 100644 --- a/lib/LiBro/Control.hs +++ b/lib/LiBro/Control.hs @@ -16,21 +16,21 @@ data Blocking -- | Initially load data and put it into the shared state. -- Expects the given 'MVar' to be empty. initData :: Config -> MVar Blocking -> MVar LiBroData -> IO () -initData config blocking libroData = do +initData cfg blocking libroData = do putMVar blocking Reading - putMVar libroData =<< loadData config + putMVar libroData =<< loadData cfg _ <- takeMVar blocking return () -- | Try to store shared state data. Expects the given blocking MVar -- to be empty. Iff not, returns 'False'. saveData :: Config -> MVar Blocking -> MVar LiBroData -> IO Bool -saveData config blocking libroData = do +saveData cfg blocking libroData = do isBlocked <- not <$> isEmptyMVar blocking if isBlocked then return False else do putMVar blocking Writing - storeData config =<< readMVar libroData + storeData cfg =<< readMVar libroData _ <- takeMVar blocking return True diff --git a/test/LiBro/ControlSpec.hs b/test/LiBro/ControlSpec.hs index 6b5ee94..1ce34f7 100644 --- a/test/LiBro/ControlSpec.hs +++ b/test/LiBro/ControlSpec.hs @@ -20,14 +20,14 @@ dataInitialization :: Spec dataInitialization = describe "Blocking data loading" $ do context "With simple data files" $ do - let config = def { storage = def { directory = "test/storage-files/data" }} - expectedData <- runIO $ loadData config + let cfg = def { storage = def { directory = "test/storage-files/data" }} + expectedData <- runIO $ loadData cfg blocking <- runIO $ newEmptyMVar libroData <- runIO $ newEmptyMVar (beb, bed, aeb, aned, ld) <- runIO $ do beforeEmptyBlocking <- isEmptyMVar blocking beforeEmptyData <- isEmptyMVar libroData - initData config blocking libroData + initData cfg blocking libroData afterEmptyBlocking <- isEmptyMVar blocking afterNonEmptyData <- isEmptyMVar libroData loadedData <- readMVar libroData From 5705aa72f37b709a8185e4116a92ebc230d2c63b Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Wed, 21 Feb 2024 12:03:39 +0100 Subject: [PATCH 13/28] Move libro state modifiers to Control --- lib/LiBro/Control.hs | 49 +++++++++++++++++++++++++++++++++++ lib/LiBro/WebService/State.hs | 25 ------------------ libro-backend.cabal | 1 - 3 files changed, 49 insertions(+), 26 deletions(-) delete mode 100644 lib/LiBro/WebService/State.hs diff --git a/lib/LiBro/Control.hs b/lib/LiBro/Control.hs index e8badd6..f8e73e2 100644 --- a/lib/LiBro/Control.hs +++ b/lib/LiBro/Control.hs @@ -5,6 +5,7 @@ import LiBro.Config import LiBro.Data import LiBro.Data.Storage import Control.Concurrent +import Control.Monad.Reader -- | Represents a blocking action because the system is loading -- or saving data. @@ -34,3 +35,51 @@ saveData cfg blocking libroData = do storeData cfg =<< readMVar libroData _ <- 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 :: Config -> IO LiBroState +initLiBroState cfg = do + mvb <- newEmptyMVar + mvd <- newEmptyMVar + initData cfg mvb mvd + return $ LiBroState cfg mvb mvd + +-- | 'Config' accessor action. +lsConfig :: ReaderT LiBroState IO Config +lsConfig = asks config + +-- | Checks whether the system is blocked +-- and by what type of 'Blocking' action. +lsBlockedBy :: ReaderT LiBroState IO (Maybe Blocking) +lsBlockedBy = do + mvb <- asks mvBlocking + lift $ tryTakeMVar mvb + +-- | 'LiBroData' accessor action. +lsData :: ReaderT LiBroState IO LiBroData +lsData = do + mvd <- asks mvData + lift $ readMVar mvd + +-- | 'initData' action. +lsInitData :: ReaderT LiBroState IO () +lsInitData = do + cfg <- asks config + mvb <- asks mvBlocking + mvd <- asks mvData + lift $ initData cfg mvb mvd + +-- | 'saveData' action. +lsSaveData :: ReaderT LiBroState IO Bool +lsSaveData = do + cfg <- asks config + mvb <- asks mvBlocking + mvd <- asks mvData + lift $ saveData cfg mvb mvd diff --git a/lib/LiBro/WebService/State.hs b/lib/LiBro/WebService/State.hs deleted file mode 100644 index 6beec0f..0000000 --- a/lib/LiBro/WebService/State.hs +++ /dev/null @@ -1,25 +0,0 @@ -module LiBro.WebService.State where - -import LiBro.Config -import LiBro.Data -import LiBro.Control -import Control.Concurrent - -data LiBroState = LiBroState - { config :: Config - , mvBlocking :: MVar Blocking - , mvData :: MVar LiBroData - } - -lsConfig :: LiBroState -> IO Config -lsConfig = return . config - -lsData :: LiBroState -> IO LiBroData -lsData = readMVar . mvData - -lsInit :: Config -> IO LiBroState -lsInit config = do - mvb <- newEmptyMVar - mvd <- newEmptyMVar - initData config mvb mvd - return $ LiBroState config mvb mvd diff --git a/libro-backend.cabal b/libro-backend.cabal index fefc78d..bbda8c0 100644 --- a/libro-backend.cabal +++ b/libro-backend.cabal @@ -42,7 +42,6 @@ library , LiBro.Data.SafeText , LiBro.Control , LiBro.WebService - , LiBro.WebService.State , LiBro.Util build-depends: aeson , attoparsec From 04579baa862dfd1876b0ace9f07cf316344541c8 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Fri, 23 Feb 2024 10:53:45 +0100 Subject: [PATCH 14/28] Fix some intermediate warnings --- lib/LiBro/Data.hs | 2 +- server/Main.hs | 6 +++--- test/LiBro/ConfigSpec.hs | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/LiBro/Data.hs b/lib/LiBro/Data.hs index 5a3b89f..1878c9b 100644 --- a/lib/LiBro/Data.hs +++ b/lib/LiBro/Data.hs @@ -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 diff --git a/server/Main.hs b/server/Main.hs index 2a4ed5b..c6c1d3a 100644 --- a/server/Main.hs +++ b/server/Main.hs @@ -6,9 +6,9 @@ import Network.Wai.Handler.Warp configuredMain :: Config -> IO () configuredMain cfg = do - let port = Conf.port $ Conf.server cfg - putStrLn $ "Serving LiBro backend on port " ++ show port ++ "." - run port (libro cfg) + let p = Conf.port $ Conf.server cfg + putStrLn $ "Serving LiBro backend on port " ++ show p ++ "." + run p (libro cfg) main :: IO () main = readConfig >>= maybe complain configuredMain diff --git a/test/LiBro/ConfigSpec.hs b/test/LiBro/ConfigSpec.hs index 73033ba..b20c47a 100644 --- a/test/LiBro/ConfigSpec.hs +++ b/test/LiBro/ConfigSpec.hs @@ -33,15 +33,15 @@ writeConfig c = T.unlines instance Arbitrary Config where arbitrary = do - st <- Storage <$> name <*> name <*> name <*> name - srv <- Server <$> port + st <- Storage <$> aname <*> aname <*> aname <*> aname + srv <- Server <$> aport return $ Config st srv where chars = '/' : ['a'..'z'] ++ ['A'..'Z'] - name = do a <- elements chars + aname = do a <- elements chars z <- elements chars as <- listOf $ elements (' ' : chars) return (a : as ++ [z]) - port = elements [1024 .. 49151] -- Wikipedia "Registered port" + aport = elements [1024 .. 49151] -- Wikipedia "Registered port" spec :: Spec spec = describe "INI file configuration" $ do From bee31ce0f2287b85008823ae92255dce5833372a Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Fri, 23 Feb 2024 12:51:12 +0100 Subject: [PATCH 15/28] Gitignore LibreOffice swap files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 72b1946..18efc33 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Vim swap files *.sw[a-p] +# LibreOffice swap files +.~lock.*# + # Generated documentstion api-docs From 48f1c60556d1cf8e121fbfd64205cdc533d4fe39 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Fri, 23 Feb 2024 14:04:45 +0100 Subject: [PATCH 16/28] Redesign web service actions for the person IDs listing endpoint --- lib/LiBro/Control.hs | 13 ++++++++----- lib/LiBro/WebService.hs | 32 ++++++++++++++++++++++---------- server/Main.hs | 8 +++++--- test/LiBro/WebServiceSpec.hs | 28 +++++++++------------------- 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/lib/LiBro/Control.hs b/lib/LiBro/Control.hs index f8e73e2..30f5ffb 100644 --- a/lib/LiBro/Control.hs +++ b/lib/LiBro/Control.hs @@ -51,25 +51,28 @@ initLiBroState cfg = do initData cfg mvb mvd return $ LiBroState cfg mvb mvd +-- | Type alias for actions holding a 'LiBroState' inside 'ReaderT'. +type Action = ReaderT LiBroState IO + -- | 'Config' accessor action. -lsConfig :: ReaderT LiBroState IO Config +lsConfig :: Action Config lsConfig = asks config -- | Checks whether the system is blocked -- and by what type of 'Blocking' action. -lsBlockedBy :: ReaderT LiBroState IO (Maybe Blocking) +lsBlockedBy :: Action (Maybe Blocking) lsBlockedBy = do mvb <- asks mvBlocking lift $ tryTakeMVar mvb -- | 'LiBroData' accessor action. -lsData :: ReaderT LiBroState IO LiBroData +lsData :: Action LiBroData lsData = do mvd <- asks mvData lift $ readMVar mvd -- | 'initData' action. -lsInitData :: ReaderT LiBroState IO () +lsInitData :: Action () lsInitData = do cfg <- asks config mvb <- asks mvBlocking @@ -77,7 +80,7 @@ lsInitData = do lift $ initData cfg mvb mvd -- | 'saveData' action. -lsSaveData :: ReaderT LiBroState IO Bool +lsSaveData :: Action Bool lsSaveData = do cfg <- asks config mvb <- asks mvBlocking diff --git a/lib/LiBro/WebService.hs b/lib/LiBro/WebService.hs index cfececb..633efc3 100644 --- a/lib/LiBro/WebService.hs +++ b/lib/LiBro/WebService.hs @@ -1,29 +1,41 @@ module LiBro.WebService where -import LiBro.Config +import LiBro.Control +import LiBro.Data +import qualified Data.Map as M import Data.Aeson import Data.Proxy import Servant +import Control.Monad.Reader import GHC.Generics newtype PersonIDs = PersonIDs {personIDs :: [Int]} deriving Generic instance ToJSON PersonIDs -type LiBroAPI = "hello" :> Get '[JSON] PersonIDs +type LiBroHandler = ReaderT LiBroState Handler + +runAction :: Action a -> LiBroHandler a +runAction action = ask >>= liftIO . runReaderT action + +type LiBroAPI = "person":> Get '[JSON] PersonIDs :<|> "yay" :> Get '[PlainText] String -libroServer :: Config -> Server LiBroAPI -libroServer cfg = handleHello - :<|> handleYay +libroServer :: ServerT LiBroAPI LiBroHandler +libroServer = hPersonIDs + :<|> handleYay where - handleHello :: Handler PersonIDs - handleHello = return $ PersonIDs [17, 42] + hPersonIDs :: LiBroHandler PersonIDs + hPersonIDs = do + ps <- persons <$> runAction lsData + return $ PersonIDs (M.keys ps) - handleYay :: Handler String + handleYay :: LiBroHandler String handleYay = return "Yay!" libroApi :: Proxy LiBroAPI libroApi = Proxy -libro :: Config -> Application -libro = serve libroApi . libroServer +libro :: LiBroState -> Application +libro initState = + let server = hoistServer libroApi (`runReaderT` initState) libroServer + in serve libroApi server diff --git a/server/Main.hs b/server/Main.hs index c6c1d3a..5340a1e 100644 --- a/server/Main.hs +++ b/server/Main.hs @@ -1,14 +1,16 @@ module Main where -import LiBro.Config as Conf +import LiBro.Config +import LiBro.Control import LiBro.WebService import Network.Wai.Handler.Warp configuredMain :: Config -> IO () configuredMain cfg = do - let p = Conf.port $ Conf.server cfg + let p = port $ server cfg putStrLn $ "Serving LiBro backend on port " ++ show p ++ "." - run p (libro cfg) + initState <- initLiBroState cfg + run p $ libro initState main :: IO () main = readConfig >>= maybe complain configuredMain diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs index 961fe09..71654ec 100644 --- a/test/LiBro/WebServiceSpec.hs +++ b/test/LiBro/WebServiceSpec.hs @@ -3,34 +3,24 @@ module LiBro.WebServiceSpec where import Test.Hspec import Test.Hspec.Wai import Test.Hspec.Wai.JSON -import Test.Hspec.Wai.QuickCheck import LiBro.Config +import LiBro.Control import LiBro.WebService import Data.Default -import Data.ByteString spec :: Spec spec = describe "RESTful JSON web service" $ do - helloLibro + personSpecs -helloLibro :: Spec -helloLibro = describe "Dummy: hello libro!" $ with (return cfgLibro) $ do +personSpecs :: Spec +personSpecs = describe "Person related endpoints" $ with lws $ do - describe "Yay endpoint" $ do - it "Respond with 200 greeting" $ do - get "/yay" `shouldRespondWith` "Yay!" {matchStatus = 200} - - describe "Dummy person ID endpoint" $ do + describe "Person ID listing endpoint" $ do it "Respond with IDs" $ do - get "/hello" `shouldRespondWith` - [json|{"personIDs":[17,42]}|] + get "/person" `shouldRespondWith` + [json|{"personIDs": [1,2]}|] {matchStatus = 200} - describe "Any other endpoint" $ do - it "Respond with 404" $ do - property $ \endpoint -> - show endpoint /= "hello" ==> - get (pack endpoint) `shouldRespondWith` 404 - - where cfgLibro = libro $ Config def def + where lws = libro <$> initLiBroState cfg + cfg = Config (def {directory = "test/storage-files/data"}) def From 751fc69df5a62ba850696da1fe3d2175f2bc2d61 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Fri, 23 Feb 2024 14:24:17 +0100 Subject: [PATCH 17/28] Fix typos in default config --- .gitignore | 3 +++ config.ini | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 18efc33..4caa3f7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ # LibreOffice swap files .~lock.*# +# Data storage files +data-storage/*.xlsx + # Generated documentstion api-docs diff --git a/config.ini b/config.ini index 1ad8937..6bd6409 100644 --- a/config.ini +++ b/config.ini @@ -1,8 +1,8 @@ [storage] directory = data-storage -person-file = persons.csv -tasks-file = tasks.csv -tracking-file = tracking.csv +person-file = persons.xlsx +tasks-file = tasks.xlsx +tracking-file = tracking.xlsx [server] port = 8080 From 58441d230535d72c976fed1825313736f65f0f25 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Fri, 23 Feb 2024 14:29:58 +0100 Subject: [PATCH 18/28] Add person details endpoint --- lib/LiBro/WebService.hs | 10 +++++++++- test/LiBro/WebServiceSpec.hs | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/LiBro/WebService.hs b/lib/LiBro/WebService.hs index 633efc3..8c625ac 100644 --- a/lib/LiBro/WebService.hs +++ b/lib/LiBro/WebService.hs @@ -3,6 +3,7 @@ module LiBro.WebService where import LiBro.Control import LiBro.Data import qualified Data.Map as M +import Data.Map ((!)) import Data.Aeson import Data.Proxy import Servant @@ -17,11 +18,13 @@ type LiBroHandler = ReaderT LiBroState Handler runAction :: Action a -> LiBroHandler a runAction action = ask >>= liftIO . runReaderT action -type LiBroAPI = "person":> Get '[JSON] PersonIDs +type LiBroAPI = "person" :> Get '[JSON] PersonIDs + :<|> "person" :> Capture "pid" Int :> Get '[JSON] Person :<|> "yay" :> Get '[PlainText] String libroServer :: ServerT LiBroAPI LiBroHandler libroServer = hPersonIDs + :<|> hPersonDetails :<|> handleYay where hPersonIDs :: LiBroHandler PersonIDs @@ -29,6 +32,11 @@ libroServer = hPersonIDs ps <- persons <$> runAction lsData return $ PersonIDs (M.keys ps) + hPersonDetails :: Int -> LiBroHandler Person + hPersonDetails pId = do + ps <- persons <$> runAction lsData + return $ ps ! pId + handleYay :: LiBroHandler String handleYay = return "Yay!" diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs index 71654ec..dc9bb83 100644 --- a/test/LiBro/WebServiceSpec.hs +++ b/test/LiBro/WebServiceSpec.hs @@ -22,5 +22,11 @@ personSpecs = describe "Person related endpoints" $ with lws $ do [json|{"personIDs": [1,2]}|] {matchStatus = 200} + describe "Person details endpoint" $ do + it "Respond with correct details" $ do + get "/person/2" `shouldRespondWith` + [json|{"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"}|] + {matchStatus = 200} + where lws = libro <$> initLiBroState cfg cfg = Config (def {directory = "test/storage-files/data"}) def From 3bea8362e461a4e50ae577ae7267bacf2f19167f Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Fri, 23 Feb 2024 14:30:50 +0100 Subject: [PATCH 19/28] Remove dummy endpoint --- lib/LiBro/WebService.hs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/LiBro/WebService.hs b/lib/LiBro/WebService.hs index 8c625ac..85dccf0 100644 --- a/lib/LiBro/WebService.hs +++ b/lib/LiBro/WebService.hs @@ -20,12 +20,10 @@ runAction action = ask >>= liftIO . runReaderT action type LiBroAPI = "person" :> Get '[JSON] PersonIDs :<|> "person" :> Capture "pid" Int :> Get '[JSON] Person - :<|> "yay" :> Get '[PlainText] String libroServer :: ServerT LiBroAPI LiBroHandler libroServer = hPersonIDs :<|> hPersonDetails - :<|> handleYay where hPersonIDs :: LiBroHandler PersonIDs hPersonIDs = do @@ -37,9 +35,6 @@ libroServer = hPersonIDs ps <- persons <$> runAction lsData return $ ps ! pId - handleYay :: LiBroHandler String - handleYay = return "Yay!" - libroApi :: Proxy LiBroAPI libroApi = Proxy From 10d5dcd3b4f0970c50b83ea9c0fdfc4347b98db3 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Fri, 23 Feb 2024 14:42:23 +0100 Subject: [PATCH 20/28] Restructure web service endpoint tests --- test/LiBro/WebServiceSpec.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs index dc9bb83..de97522 100644 --- a/test/LiBro/WebServiceSpec.hs +++ b/test/LiBro/WebServiceSpec.hs @@ -11,10 +11,10 @@ import Data.Default spec :: Spec spec = describe "RESTful JSON web service" $ do - personSpecs + listings -personSpecs :: Spec -personSpecs = describe "Person related endpoints" $ with lws $ do +listings :: Spec +listings = describe "Simple data listing endpoints" $ with lws $ do describe "Person ID listing endpoint" $ do it "Respond with IDs" $ do From 1555f168112b9bd774f2b95a3c53db589f0cb1c6 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Fri, 23 Feb 2024 14:47:36 +0100 Subject: [PATCH 21/28] Add top-level task ID listing endpoint --- lib/LiBro/WebService.hs | 12 +++++++++++- test/LiBro/WebServiceSpec.hs | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/LiBro/WebService.hs b/lib/LiBro/WebService.hs index 85dccf0..f095e72 100644 --- a/lib/LiBro/WebService.hs +++ b/lib/LiBro/WebService.hs @@ -4,14 +4,17 @@ import LiBro.Control import LiBro.Data import qualified Data.Map as M import Data.Map ((!)) +import Data.Tree import Data.Aeson import Data.Proxy import Servant import Control.Monad.Reader import GHC.Generics -newtype PersonIDs = PersonIDs {personIDs :: [Int]} deriving Generic +newtype PersonIDs = PersonIDs {personIDs :: [Int]} deriving Generic +newtype TaskIDs = TaskIDs {taskIDs :: [Int]} deriving Generic instance ToJSON PersonIDs +instance ToJSON TaskIDs type LiBroHandler = ReaderT LiBroState Handler @@ -20,10 +23,12 @@ runAction action = ask >>= liftIO . runReaderT action type LiBroAPI = "person" :> Get '[JSON] PersonIDs :<|> "person" :> Capture "pid" Int :> Get '[JSON] Person + :<|> "task" :> Get '[JSON] TaskIDs libroServer :: ServerT LiBroAPI LiBroHandler libroServer = hPersonIDs :<|> hPersonDetails + :<|> hTaskIDs where hPersonIDs :: LiBroHandler PersonIDs hPersonIDs = do @@ -35,6 +40,11 @@ libroServer = hPersonIDs ps <- persons <$> runAction lsData return $ ps ! pId + hTaskIDs :: LiBroHandler TaskIDs + hTaskIDs = do + ts <- tasks <$> runAction lsData + return $ TaskIDs (tid . rootLabel <$> ts) + libroApi :: Proxy LiBroAPI libroApi = Proxy diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs index de97522..642f03d 100644 --- a/test/LiBro/WebServiceSpec.hs +++ b/test/LiBro/WebServiceSpec.hs @@ -28,5 +28,11 @@ listings = describe "Simple data listing endpoints" $ with lws $ do [json|{"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"}|] {matchStatus = 200} + describe "Task ID listing endpoint" $ do + it "Respond with correct IDs" $ do + get "/task" `shouldRespondWith` + [json|{"taskIDs": [17]}|] + {matchStatus = 200} + where lws = libro <$> initLiBroState cfg cfg = Config (def {directory = "test/storage-files/data"}) def From 11167e9d295cf46446d5666f27edcc22e503c265 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Fri, 23 Feb 2024 15:15:08 +0100 Subject: [PATCH 22/28] Improve listing endpoint test structure --- test/LiBro/WebServiceSpec.hs | 42 ++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs index 642f03d..f0d5d0a 100644 --- a/test/LiBro/WebServiceSpec.hs +++ b/test/LiBro/WebServiceSpec.hs @@ -14,25 +14,29 @@ spec = describe "RESTful JSON web service" $ do listings listings :: Spec -listings = describe "Simple data listing endpoints" $ with lws $ do - - describe "Person ID listing endpoint" $ do - it "Respond with IDs" $ do - get "/person" `shouldRespondWith` - [json|{"personIDs": [1,2]}|] - {matchStatus = 200} - - describe "Person details endpoint" $ do - it "Respond with correct details" $ do - get "/person/2" `shouldRespondWith` - [json|{"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"}|] - {matchStatus = 200} - - describe "Task ID listing endpoint" $ do - it "Respond with correct IDs" $ do - get "/task" `shouldRespondWith` - [json|{"taskIDs": [17]}|] - {matchStatus = 200} +listings = describe "Simple data listing" $ with lws $ do + + context "Person listing endpoints" $ do + + describe "ID listing" $ do + it "Correct IDs" $ do + get "/person" `shouldRespondWith` + [json|{"personIDs": [1,2]}|] + {matchStatus = 200} + + describe "details" $ do + it "Correct details" $ do + get "/person/2" `shouldRespondWith` + [json|{"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"}|] + {matchStatus = 200} + + context "Task listing endpoints" $ do + + describe "ID listing" $ do + it "Correct IDs" $ do + get "/task" `shouldRespondWith` + [json|{"taskIDs": [17]}|] + {matchStatus = 200} where lws = libro <$> initLiBroState cfg cfg = Config (def {directory = "test/storage-files/data"}) def From 0a1aa2fb13e60b31ec1b9c0e0e3dfe681f7bfa9e Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Fri, 23 Feb 2024 15:21:39 +0100 Subject: [PATCH 23/28] Handle bad person ID requests correctly --- lib/LiBro/WebService.hs | 5 +++-- test/LiBro/WebServiceSpec.hs | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/LiBro/WebService.hs b/lib/LiBro/WebService.hs index f095e72..48bbe73 100644 --- a/lib/LiBro/WebService.hs +++ b/lib/LiBro/WebService.hs @@ -3,7 +3,6 @@ module LiBro.WebService where import LiBro.Control import LiBro.Data import qualified Data.Map as M -import Data.Map ((!)) import Data.Tree import Data.Aeson import Data.Proxy @@ -38,7 +37,9 @@ libroServer = hPersonIDs hPersonDetails :: Int -> LiBroHandler Person hPersonDetails pId = do ps <- persons <$> runAction lsData - return $ ps ! pId + case M.lookup pId ps of + Just p -> return p + Nothing -> throwError err404 {errBody = "Person not found"} hTaskIDs :: LiBroHandler TaskIDs hTaskIDs = do diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs index f0d5d0a..326dd9d 100644 --- a/test/LiBro/WebServiceSpec.hs +++ b/test/LiBro/WebServiceSpec.hs @@ -29,6 +29,10 @@ listings = describe "Simple data listing" $ with lws $ do get "/person/2" `shouldRespondWith` [json|{"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"}|] {matchStatus = 200} + it "404 if person does not exist" $ do + get "/person/42" `shouldRespondWith` + "Person not found" + {matchStatus = 404} context "Task listing endpoints" $ do From 35d67ce2da318b3dbd665f1ee7891cc7a1034d28 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Fri, 23 Feb 2024 15:50:53 +0100 Subject: [PATCH 24/28] Add tasks of a person endpoint --- lib/LiBro/WebService.hs | 16 +++++++++++++--- test/LiBro/WebServiceSpec.hs | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/LiBro/WebService.hs b/lib/LiBro/WebService.hs index 48bbe73..45e7e7a 100644 --- a/lib/LiBro/WebService.hs +++ b/lib/LiBro/WebService.hs @@ -20,13 +20,16 @@ type LiBroHandler = ReaderT LiBroState Handler runAction :: Action a -> LiBroHandler a runAction action = ask >>= liftIO . runReaderT action -type LiBroAPI = "person" :> Get '[JSON] PersonIDs - :<|> "person" :> Capture "pid" Int :> Get '[JSON] Person - :<|> "task" :> Get '[JSON] TaskIDs +type LiBroAPI = + "person" :> Get '[JSON] PersonIDs + :<|> "person" :> Capture "pid" Int :> Get '[JSON] Person + :<|> "person" :> Capture "pid" Int :> "task" :> Get '[JSON] TaskIDs + :<|> "task" :> Get '[JSON] TaskIDs libroServer :: ServerT LiBroAPI LiBroHandler libroServer = hPersonIDs :<|> hPersonDetails + :<|> hPersonTasks :<|> hTaskIDs where hPersonIDs :: LiBroHandler PersonIDs @@ -41,6 +44,13 @@ libroServer = hPersonIDs Just p -> return p Nothing -> throwError err404 {errBody = "Person not found"} + hPersonTasks :: Int -> LiBroHandler TaskIDs + hPersonTasks pId = do + d <- runAction lsData + case M.lookup pId (persons d) of + Just p -> return $ TaskIDs (tid <$> assignedTasks p (tasks d)) + Nothing -> throwError err404 {errBody = "Person not found"} + hTaskIDs :: LiBroHandler TaskIDs hTaskIDs = do ts <- tasks <$> runAction lsData diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs index 326dd9d..5bab529 100644 --- a/test/LiBro/WebServiceSpec.hs +++ b/test/LiBro/WebServiceSpec.hs @@ -34,6 +34,20 @@ listings = describe "Simple data listing" $ with lws $ do "Person not found" {matchStatus = 404} + describe "tasks of a person" $ do + it "Correct tasks of person with 1 task" $ do + get "/person/1/task" `shouldRespondWith` + [json|{"taskIDs": [17]}|] + {matchStatus = 200} + it "Correct tasks of person with 2 task" $ do + get "/person/2/task" `shouldRespondWith` + [json|{"taskIDs": [17, 37]}|] + {matchStatus = 200} + it "404 if person does not exist" $ do + get "/person/42" `shouldRespondWith` + "Person not found" + {matchStatus = 404} + context "Task listing endpoints" $ do describe "ID listing" $ do From 9600bf3848f258971fb1edf2b882b2280e6a3a26 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Mon, 26 Feb 2024 17:20:07 +0100 Subject: [PATCH 25/28] Restructure and improve ID-only API --- lib/LiBro/WebService.hs | 61 +++++++++++++++++--------------- test/LiBro/WebServiceSpec.hs | 68 +++++++++++++++++++++++++----------- 2 files changed, 80 insertions(+), 49 deletions(-) diff --git a/lib/LiBro/WebService.hs b/lib/LiBro/WebService.hs index 45e7e7a..56374e8 100644 --- a/lib/LiBro/WebService.hs +++ b/lib/LiBro/WebService.hs @@ -10,51 +10,54 @@ import Servant import Control.Monad.Reader import GHC.Generics -newtype PersonIDs = PersonIDs {personIDs :: [Int]} deriving Generic -newtype TaskIDs = TaskIDs {taskIDs :: [Int]} deriving Generic -instance ToJSON PersonIDs -instance ToJSON TaskIDs - 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 + type LiBroAPI = - "person" :> Get '[JSON] PersonIDs - :<|> "person" :> Capture "pid" Int :> Get '[JSON] Person - :<|> "person" :> Capture "pid" Int :> "task" :> Get '[JSON] TaskIDs - :<|> "task" :> Get '[JSON] TaskIDs + "person" :> Get '[JSON] [Person] + :<|> "person" :> Capture "pid" Int :> Get '[JSON] PersonDetails + :<|> "task" :> Get '[JSON] [Task] + :<|> "task" :> "tree" :> Get '[JSON] TaskForest libroServer :: ServerT LiBroAPI LiBroHandler -libroServer = hPersonIDs +libroServer = hPersonList :<|> hPersonDetails - :<|> hPersonTasks - :<|> hTaskIDs + :<|> hTaskTopLevelList + :<|> hTaskFullForest where - hPersonIDs :: LiBroHandler PersonIDs - hPersonIDs = do - ps <- persons <$> runAction lsData - return $ PersonIDs (M.keys ps) + hPersonList :: LiBroHandler [Person] + hPersonList = M.elems . persons <$> runAction lsData - hPersonDetails :: Int -> LiBroHandler Person + hPersonDetails :: Int -> LiBroHandler PersonDetails hPersonDetails pId = do - ps <- persons <$> runAction lsData - case M.lookup pId ps of - Just p -> return p - Nothing -> throwError err404 {errBody = "Person not found"} - - hPersonTasks :: Int -> LiBroHandler TaskIDs - hPersonTasks pId = do d <- runAction lsData case M.lookup pId (persons d) of - Just p -> return $ TaskIDs (tid <$> assignedTasks p (tasks d)) + Just p -> let ts = assignedTasks p (tasks d) + in return $ PersonDetails p ts Nothing -> throwError err404 {errBody = "Person not found"} - hTaskIDs :: LiBroHandler TaskIDs - hTaskIDs = do - ts <- tasks <$> runAction lsData - return $ TaskIDs (tid . rootLabel <$> ts) + hTaskTopLevelList :: LiBroHandler [Task] + hTaskTopLevelList = map rootLabel . tasks <$> runAction lsData + + hTaskFullForest :: LiBroHandler TaskForest + hTaskFullForest = map convert . tasks <$> runAction lsData + where convert (Node t sts) = TaskTree t (convert <$> sts) libroApi :: Proxy LiBroAPI libroApi = Proxy diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs index 5bab529..84ca4e9 100644 --- a/test/LiBro/WebServiceSpec.hs +++ b/test/LiBro/WebServiceSpec.hs @@ -18,31 +18,32 @@ listings = describe "Simple data listing" $ with lws $ do context "Person listing endpoints" $ do - describe "ID listing" $ do - it "Correct IDs" $ do + describe "Person listing" $ do + it "Correct list" $ do get "/person" `shouldRespondWith` - [json|{"personIDs": [1,2]}|] + [json|[ + {"pid": 1, "name": "Foo Bar", "email": "foo@bar.com"}, + {"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"} + ]|] {matchStatus = 200} describe "details" $ do it "Correct details" $ do get "/person/2" `shouldRespondWith` - [json|{"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"}|] + [json|{ + "person": {"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"}, + "personTasks": [ + {"tid": 17, "title": "t17", "description": "d17", "assignees": [ + {"pid": 1, "name": "Foo Bar", "email": "foo@bar.com"}, + {"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"} + ]}, + {"tid": 37, "title": "t37", "description": "d37", "assignees": [ + {"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"} + ]} + ] + }|] {matchStatus = 200} - it "404 if person does not exist" $ do - get "/person/42" `shouldRespondWith` - "Person not found" - {matchStatus = 404} - describe "tasks of a person" $ do - it "Correct tasks of person with 1 task" $ do - get "/person/1/task" `shouldRespondWith` - [json|{"taskIDs": [17]}|] - {matchStatus = 200} - it "Correct tasks of person with 2 task" $ do - get "/person/2/task" `shouldRespondWith` - [json|{"taskIDs": [17, 37]}|] - {matchStatus = 200} it "404 if person does not exist" $ do get "/person/42" `shouldRespondWith` "Person not found" @@ -50,10 +51,37 @@ listings = describe "Simple data listing" $ with lws $ do context "Task listing endpoints" $ do - describe "ID listing" $ do - it "Correct IDs" $ do + describe "Top level tasks" $ do + it "Correct list" $ do get "/task" `shouldRespondWith` - [json|{"taskIDs": [17]}|] + [json|[ + {"tid": 17, "title": "t17", "description": "d17", "assignees": [ + {"pid": 1, "name": "Foo Bar", "email": "foo@bar.com"}, + {"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"} + ]} + ]|] + {matchStatus = 200} + + describe "Full task hierarchy" $ do + it "Correct forest" $ do + get "/task/tree" `shouldRespondWith` + [json|[ + { "task": {"tid": 17, "title": "t17", "description": "d17", "assignees": [ + {"pid": 1, "name": "Foo Bar", "email": "foo@bar.com"}, + {"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"} + ]}, + "subTasks": [ + { "task": {"tid": 37, "title": "t37", "description": "d37", "assignees": [ + {"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"} + ]}, + "subTasks": [] + }, + { "task": {"tid": 42, "title": "t42", "description": "d42", "assignees": []}, + "subTasks": [] + } + ] + } + ]|] {matchStatus = 200} where lws = libro <$> initLiBroState cfg From b00490d37d03b68e73776cd35fdcfad410a667f1 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Tue, 27 Feb 2024 11:58:47 +0100 Subject: [PATCH 26/28] Add forest search to utilities --- lib/LiBro/Util.hs | 10 +++++++++- test/LiBro/UtilSpec.hs | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/LiBro/Util.hs b/lib/LiBro/Util.hs index e374501..99d2de4 100644 --- a/lib/LiBro/Util.hs +++ b/lib/LiBro/Util.hs @@ -2,9 +2,10 @@ -- in more than one place. module LiBro.Util ( - -- * Tree building + -- * Tree utilities ParentList , readForest + , findSubtree -- * Counting monad transformer , CountingT , next @@ -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 diff --git a/test/LiBro/UtilSpec.hs b/test/LiBro/UtilSpec.hs index ade6ee0..abbafb1 100644 --- a/test/LiBro/UtilSpec.hs +++ b/test/LiBro/UtilSpec.hs @@ -18,6 +18,7 @@ import System.IO.Temp spec :: Spec spec = describe "Helper stuff" $ do forestFromParentList + findInForest countingT xlsx guarding @@ -35,6 +36,31 @@ forestFromParentList = describe "Read Forest from parent list" $ do , Node 42 [ Node 84 [ Node (168 :: Int) [] ]] ] +findInForest :: Spec +findInForest = describe "Find matching subtrees in a forest" $ do + let forest = [ Node 2 [ Node 4 [Node 8 []], Node 6 [Node 12 [Node 24 []]]] + , Node 3 [ Node 6 [Node 12 []], Node 9 [Node 18 [], Node 27 []]] + , Node 5 [ Node 10 []] + ] :: Forest Int + -- runIO $ putStr $ drawForest $ map (fmap show) forest + + context "Nothing to find" $ do + it "Get Nothing from empty forest" $ + findSubtree even ([] :: Forest Int) `shouldBe` Nothing + it "Nothing matches" $ + findSubtree (> 42) forest `shouldBe` Nothing + + context "Finding subtrees" $ do + it "Catch-all predicate: first tree" $ + findSubtree (const True) forest `shouldBe` Just (head forest) + it "Find the first '6' subtree" $ + findSubtree (== 6) forest `shouldBe` Just (Node 6 [Node 12 [Node 24[]]]) + it "Find the first odd subtree" $ + findSubtree odd forest `shouldBe` + Just ( Node 3 [ Node 6 [Node 12 []] + , Node 9 [Node 18 [], Node 27 []] + ]) + countingT :: Spec countingT = describe "The CountingT 'monad transformer'" $ do let nextTimes n = replicateM n next From 9f6a8c106208b459f59d27b77f24d6c82f08b5c7 Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Wed, 28 Feb 2024 11:24:42 +0100 Subject: [PATCH 27/28] Improve TaskForest handling --- lib/LiBro/WebService.hs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/LiBro/WebService.hs b/lib/LiBro/WebService.hs index 56374e8..fe117f4 100644 --- a/lib/LiBro/WebService.hs +++ b/lib/LiBro/WebService.hs @@ -29,6 +29,12 @@ data TaskTree = TaskTree } 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 @@ -56,8 +62,7 @@ libroServer = hPersonList hTaskTopLevelList = map rootLabel . tasks <$> runAction lsData hTaskFullForest :: LiBroHandler TaskForest - hTaskFullForest = map convert . tasks <$> runAction lsData - where convert (Node t sts) = TaskTree t (convert <$> sts) + hTaskFullForest = convertTasksForest . tasks <$> runAction lsData libroApi :: Proxy LiBroAPI libroApi = Proxy From 2efbea64451d2b37f77316cdd3ae4cbca60e50ea Mon Sep 17 00:00:00 2001 From: Mirko Westermeier Date: Wed, 28 Feb 2024 11:43:49 +0100 Subject: [PATCH 28/28] Add task subtree endpoint --- lib/LiBro/WebService.hs | 10 ++++++++++ test/LiBro/WebServiceSpec.hs | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/lib/LiBro/WebService.hs b/lib/LiBro/WebService.hs index fe117f4..08d4710 100644 --- a/lib/LiBro/WebService.hs +++ b/lib/LiBro/WebService.hs @@ -2,6 +2,7 @@ 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 @@ -40,12 +41,14 @@ type LiBroAPI = :<|> "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 @@ -64,6 +67,13 @@ libroServer = hPersonList 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 diff --git a/test/LiBro/WebServiceSpec.hs b/test/LiBro/WebServiceSpec.hs index 84ca4e9..733fe9d 100644 --- a/test/LiBro/WebServiceSpec.hs +++ b/test/LiBro/WebServiceSpec.hs @@ -84,5 +84,42 @@ listings = describe "Simple data listing" $ with lws $ do ]|] {matchStatus = 200} + describe "Subforest of a given task" $ do + + it "Task is a leaf" $ do + get "/task/37" `shouldRespondWith` + [json|{ + "task": {"tid": 37, "title": "t37", "description": "d37", "assignees": [ + {"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"} + ]}, + "subTasks": [] + }|] + {matchStatus = 200} + + it "Task is an inner node" $ do + get "/task/17" `shouldRespondWith` + [json|{ + "task": {"tid": 17, "title": "t17", "description": "d17", "assignees": [ + {"pid": 1, "name": "Foo Bar", "email": "foo@bar.com"}, + {"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"} + ]}, + "subTasks": [ + { "task": {"tid": 37, "title": "t37", "description": "d37", "assignees": [ + {"pid": 2, "name": "Baz Quux", "email": "baz@quux.com"} + ]}, + "subTasks": [] + }, + { "task": {"tid": 42, "title": "t42", "description": "d42", "assignees": []}, + "subTasks": [] + } + ] + }|] + {matchStatus = 200} + + it "404 if task does not exist" $ do + get "/task/666" `shouldRespondWith` + "Task not found" + {matchStatus = 404} + where lws = libro <$> initLiBroState cfg cfg = Config (def {directory = "test/storage-files/data"}) def