Skip to content
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

Linsen, you have arrived #126

Merged
merged 12 commits into from
Jun 4, 2024
2 changes: 2 additions & 0 deletions mat-chalmers.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ library
exposed-modules: Model
, Model.Types
, Model.Karen
, Model.Linsen
, Model.Wijkanders
, View
, Config
Expand Down Expand Up @@ -52,6 +53,7 @@ library
, file-embed >= 0.0.15.0 && < 1.0
, thyme >= 0.4 && <= 0.5
, word8 == 0.1.3
, extra >= 1.7.10 && <= 1.8

executable mat-chalmers
main-is: Main.hs
Expand Down
2 changes: 2 additions & 0 deletions src/Model.hs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import Config
import Model.Types
import Model.Karen
import Model.Wijkanders
import Model.Linsen

-- | Refreshes menus.
-- The refresh function evaluates to `Some monad m => m (View model, Update signal)`,
Expand Down Expand Up @@ -108,6 +109,7 @@ update = do
, karenR "L's Kitchen" "ls-kitchen" "c74da2cf-aa1a-4d3a-9ba6-08d5569587a1"
, Restaurant "Wijkanders" (fromStrict $ renderUrl wijkandersAPIURL) .
getWijkanders day' . responseBody <$> req GET wijkandersAPIURL NoReqBody lbsResponse mempty
, fetchAndCreateLinsen day'
]

for_ rest $ \r -> case menu r of
Expand Down
111 changes: 111 additions & 0 deletions src/Model/Linsen.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
{-# LANGUAGE FlexibleContexts, OverloadedStrings, LambdaCase #-}

module Model.Linsen
(
parse
, fetchAndCreateLinsen
)
where

import Control.Monad ( (>=>)
, (<=<)
, zipWithM
, ap
)
import Control.Monad.Catch ( MonadThrow )
import Control.Monad.IO.Class ( MonadIO )
import Data.Aeson ( (.:)
, withObject
, Value
)
import Data.Aeson.Types ( Parser
, parseEither
)
import Data.Bifunctor ( first )
import qualified Data.ByteString.Lazy.Char8 as BL8
import Data.Functor ( (<&>) )
import Data.List.Extra ( (!?) )
import Data.Text.Lazy ( Text
, replace
, strip )
import Data.Thyme.Calendar ( Day )
import Lens.Micro.Platform ( (^.) )
import Network.HTTP.Req
import Model.Types ( NoMenu(..)
, Menu(..)
, Restaurant
( Restaurant
)
)
import Util ( menusToEitherNoLunch )
import Data.Thyme.Calendar.WeekDate ( weekDate
, _wdDay)

fetch
:: (MonadHttp m, MonadIO m, MonadThrow m)
=> m Value -- ^ A JSON response or horrible crash
fetch =
req
GET
(https "cafe-linsen.se" /: "api" /: "menu")
NoReqBody
jsonResponse
mempty
<&> responseBody
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<&> <- pretty!


parse
:: Day -- ^ Day to parse
-> Value -- ^ JSON result from `fetch`
-> Either NoMenu [Menu] -- ^ Either list of parsed `Menu`s or `NoMenu` error
parse day =
failWithNoMenu
(parseEither
(let index = 6 - day ^. weekDate . _wdDay in
if index `notElem` [1..5] then
pure . const []
else
withObject "Parse meals"
$ (.: "docs")
>=> (\case
Nothing -> fail "Failed to index into days"
Just v -> pure v) . (!? index)
>=> (.: "richText")
>=> (.: "root")
>=> (.: "children")
>=> menuParser . (\v' -> if length v' >= 9 then v' else mempty)
)
)
>=> menusToEitherNoLunch
where
failWithNoMenu :: Show a => (a -> Either String b) -> a -> Either NoMenu b
failWithNoMenu action x =
first (\msg -> NMParseError msg . BL8.pack . show $ x) (action x)

menuParser :: [Value] -> Parser [Menu]
menuParser = pure . (zip [0 :: Integer ..] >=> \case
(2 ,vs) -> [vs] -- Index of Meat dish
(6 ,vs) -> [vs] -- Index of Fish dish
(10,vs) -> [vs] -- Index of Veg dish
_ -> [])
<=< ap (zipWithM sumFood) tail

sumFood :: Value -> Value -> Parser Menu
sumFood a b = Menu <$> getFood a <*> getFood b

getFood :: Value -> Parser Text
getFood = withObject "Menu Object"
$ (.: "children")
>=> \case
[] -> pure mempty
vs -> strip . replace "/ " ", "
<$> last vs .: "text"

fetchAndCreateLinsen
:: (MonadHttp m, MonadIO m, MonadThrow m)
=> Day -- ^ Day
-> m Restaurant -- ^ Fetched Restaurant
fetchAndCreateLinsen day =
Restaurant
"Café Linsen"
"https://plateimpact-screen.azurewebsites.net/menu/week/"
<$> fmap (parse day) fetch
2 changes: 1 addition & 1 deletion src/Model/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ data Restaurant = Restaurant
data NoMenu
= NoLunch
| NMParseError String ByteString -- ^ The parse error. The string we tried to parse.
deriving (Show)
deriving (Eq, Show)

-- | Menu of a restaurant.
-- Title, Body text
Expand Down
54 changes: 43 additions & 11 deletions test/Main.hs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
{-# LANGUAGE LambdaCase #-}
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BL8
import qualified Data.Text.Lazy as T
import Data.Aeson ( decode )
import Data.Maybe ( fromJust )
import Data.Thyme.Time.Core ( fromGregorian )
import Model.Karen ( parse )
import qualified Model.Linsen as L ( parse )
import Model.Types ( Menu(..)
, NoMenu
, NoMenu( NoLunch )
)
import Model.Wijkanders ( getWijkanders
, hasDate
Expand All @@ -19,10 +21,16 @@ import Test.HUnit ( (@?=)
, assertFailure
)

testFun :: [Menu] -> Either NoMenu [Menu] -> IO ()
testFun expected = either
(assertFailure . mappend "This is not expected, input was:\n" . show)
(@?= expected)
testFun :: Either NoMenu [Menu] -> Either NoMenu [Menu] -> IO ()
testFun = \case
Right e ->
either
(assertFailure . mappend "This is not expected, input was:\n" . show)
(@?= e)
Left e ->
either
(@?= e)
(assertFailure . mappend "This is not expected, input was:\n" . show)

main :: IO ()
main = hspec $ do
Expand All @@ -31,7 +39,7 @@ main = hspec $ do
describe "The Karen Express" $ it
"parses a blob of JSON without error"
( testFun
[ Menu
(Right [ Menu
(T.pack "Street food")
(T.pack "Chicken africana, banan, mango raja, ris")
, Menu
Expand All @@ -40,20 +48,44 @@ main = hspec $ do
, Menu
(T.pack "Nordic")
(T.pack "F\228rskost bakad sej, vitvinss\229s, broccoli, potatis")
]
])
$ parse
"Swedish"
(fromJust . decode $ BL8.pack
"{\"data\":{\"dishOccurrencesByTimeRange\":[{\"displayNames\":[{\"name\":\"Chicken africana, banan, mango raja, ris\",\"categoryName\":\"Swedish\"},{\"name\":\"Chicken africana, banana, mango raja, rice\",\"categoryName\":\"English\"}],\"startDate\":\"09/25/2023 00:00:00\",\"dishType\":{\"name\":\"Street food\"},\"dish\":{\"name\":\"Kyckling, het paprikas\195\165s & ris\"}},{\"displayNames\":[{\"name\":\"Indian linseed stew, zucchini, aubergine, ginger, coriander\",\"categoryName\":\"English\"},{\"name\":\"Indisklinsgryta, zucchini, aubergin, ingef\195\164ra, koriander, ris\",\"categoryName\":\"Swedish\"}],\"startDate\":\"09/25/2023 00:00:00\",\"dishType\":{\"name\":\"Greens\"},\"dish\":{\"name\":\"Vegan, pasta, linsbolognese\"}},{\"displayNames\":[{\"name\":\"F\195\164rskost bakad sej, vitvinss\195\165s, broccoli, potatis\",\"categoryName\":\"Swedish\"},{\"name\":\"Cream cheese baked saithe, whitewine sauce, broccoli, potatoes\",\"categoryName\":\"English\"}],\"startDate\":\"09/25/2023 00:00:00\",\"dishType\":{\"name\":\"Nordic\"},\"dish\":{\"name\":\"Bakad fisk, vitvinss\195\165s, potatispur\195\169\"}}]}}\n"
)
)

describe "Cafe Linsen" $ it
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests <3

"parses two blob of JSON without error"
(do
s1 <- BL.readFile "test/linsen1.json"
testFun
(Right [ Menu
(T.pack "Natt Överbakad Högrev.")
(T.pack "Rotfrukter, Timjansky, Persilja, Pommes Chateau.")
, Menu
(T.pack "Stekt Fisk.")
(T.pack "Remouladsås, Citron, Dill, Picklade Morötter, Rostad Potatis.")
, Menu
(T.pack "Chana Masala.")
(T.pack "Kikärtor, Grönsaker, Potatis Pakora, Nannbröd, Ris")
]) (L.parse
(fromGregorian 2024 05 31)
(fromJust $ decode s1))
s2 <- BL.readFile "test/linsen2.json" -- Test that has no lunch
testFun (Left NoLunch)
(L.parse
(fromGregorian 2024 06 06)
(fromJust $ decode s2))
)

describe "The Wijkander's"
$ it "Parses two blobs of HTML correctly on fridays"
$ do
s1 <- BL.readFile "test/190517 wijkanders.html"
testFun
[ Menu
(Right [ Menu
(T.pack "Fisk")
(T.pack
"Havets Wallenbergare, kallpressad rapsolja, ärtor, dill & potatismos"
Expand All @@ -63,11 +95,11 @@ main = hspec $ do
(T.pack
"Helstekt kotlettred, potatisgratäng, skysås & örtbakad tomat"
)
]
])
(getWijkanders (fromGregorian 2019 05 17) s1)
s2 <- BL.readFile "test/190913 wijkanders.html"
testFun
[ Menu
(Right [ Menu
(T.pack "Vegetarisk ")
(T.pack
"Pasta, svamp, grädde, citron, grana padano & rotfruktschips"
Expand All @@ -80,5 +112,5 @@ main = hspec $ do
(T.pack
"Helstekt kotlettrad, rostad potatis, svampsås & inlagd gurka"
)
]
])
(getWijkanders (fromGregorian 2019 09 13) s2)
1 change: 1 addition & 0 deletions test/linsen1.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions test/linsen2.json

Large diffs are not rendered by default.

Loading