Skip to content

Instantly share code, notes, and snippets.

@ploeh
Last active December 24, 2022 22:54
Show Gist options
  • Select an option

  • Save ploeh/c999e2ae2248bd44d775 to your computer and use it in GitHub Desktop.

Select an option

Save ploeh/c999e2ae2248bd44d775 to your computer and use it in GitHub Desktop.

Revisions

  1. ploeh revised this gist Jul 4, 2016. 3 changed files with 65 additions and 4 deletions.
    16 changes: 15 additions & 1 deletion ApiModel.hs
    Original file line number Diff line number Diff line change
    @@ -16,6 +16,10 @@ data Reservation = Reservation
    , quantity :: Int }
    deriving (Show, Read)

    data Caravan = Caravan
    { caravanCapacity :: Int } -- Imagine that this type has more composite elements
    deriving (Eq, Show, Read)

    instance Eq Reservation where
    x == y =
    zonedTimeZone (date x) == zonedTimeZone (date y) &&
    @@ -47,6 +51,16 @@ checkCapacity capacity reservedSeats reservation =
    then Left CapacityExceeded
    else Right reservation

    checkCaravanCapacityOnError :: Error
    -> Maybe Caravan
    -> Reservation
    -> Either Error Reservation
    checkCaravanCapacityOnError CapacityExceeded (Just caravan) reservation =
    if caravanCapacity caravan < quantity reservation
    then Left CapacityExceeded
    else Right reservation
    checkCaravanCapacityOnError err _ _ = Left err

    data StatusCode = Forbidden | Accepted -- add more at leisure
    deriving (Eq, Show, Read)

    @@ -58,4 +72,4 @@ data HttpResult a = OK a
    toHttpResult :: Either Error () -> HttpResult ()
    toHttpResult (Left (ValidationError msg)) = BadRequest msg
    toHttpResult (Left CapacityExceeded) = StatusCode Forbidden
    toHttpResult (Right ()) = OK ()
    toHttpResult (Right ()) = OK ()
    17 changes: 14 additions & 3 deletions App.hs
    Original file line number Diff line number Diff line change
    @@ -5,15 +5,26 @@ module App

    import ApiModel
    import DB
    import Control.Monad (forM_)
    import Control.Monad.Trans (liftIO)
    import Control.Monad.Trans.Either (EitherT(..), hoistEither)
    import Control.Monad.Trans.Either (EitherT(..), hoistEither, right, eitherT)

    connStr :: ConnectionString
    connStr = "."

    svcAddr :: ServiceAddress
    svcAddr = "."

    checkCaravan :: Reservation -> Error -> EitherT Error IO Reservation
    checkCaravan reservation err = do
    c <- liftIO $ findCaravan svcAddr (quantity reservation) (date reservation)
    newRes <- hoistEither $ checkCaravanCapacityOnError err c reservation
    liftIO $ forM_ c $ reserveCaravan svcAddr (date newRes)
    return newRes

    postReservation :: ReservationRendition -> IO (HttpResult ())
    postReservation candidate = fmap toHttpResult $ runEitherT $ do
    r <- hoistEither $ validateReservation candidate
    i <- liftIO $ getReservedSeatsFromDB connStr $ date r
    hoistEither $ checkCapacity 10 i r
    >>= liftIO . saveReservation connStr
    eitherT (checkCaravan r) right $ hoistEither $ checkCapacity 10 i r
    >>= liftIO . saveReservation connStr
    36 changes: 36 additions & 0 deletions DB.hs
    Original file line number Diff line number Diff line change
    @@ -4,8 +4,12 @@ module DB
    , readReservationsFromDB
    , getReservedSeatsFromDB
    , saveReservation
    , ServiceAddress
    , findCaravan
    , reserveCaravan
    ) where

    import Data.List (find)
    import System.Directory (doesFileExist)
    import System.FilePath ((</>))
    import Data.Time (FormatTime(..), ZonedTime(..), formatTime, defaultTimeLocale)
    @@ -38,3 +42,35 @@ saveReservation dir r = do --Imagine that this inserts into a database table ins
    -- Use of `seq` as described in http://stackoverflow.com/a/2530948/126014
    length reservations `seq` writeFile fileName $ show (r : reservations)
    where fileName = dir </> fileNameForReservation r

    -- Caravan storage

    caravanPool :: [Caravan]
    caravanPool = map Caravan [4, 6, 8]

    fileNameForCaravan :: ZonedTime -> FilePath
    fileNameForCaravan = (++ ".caravan.txt") . rawFileNameForDate

    type ServiceAddress = String

    readReservedCaravans :: ServiceAddress -> ZonedTime -> IO [Caravan]
    readReservedCaravans dir d = do -- Imagine that this queries a web service instead of reading from a file
    exists <- doesFileExist fileName
    if exists
    then read <$> readFile fileName
    else return []
    where fileName = dir </> fileNameForCaravan d

    findCaravan :: ServiceAddress -> Int -> ZonedTime -> IO (Maybe Caravan)
    findCaravan dir requestedCapacity d = do
    putStrLn "Finding a caravan..."
    reservedCaravans <- readReservedCaravans dir d
    let availableCaravans = filter (`notElem` reservedCaravans) caravanPool
    return $ find (\c -> requestedCapacity <= caravanCapacity c) availableCaravans

    reserveCaravan :: ServiceAddress -> ZonedTime -> Caravan -> IO ()
    reserveCaravan dir d c = do --Imagine that this updates a web service instead of writing to a file
    caravans <- readReservedCaravans dir d
    -- Use of `seq` as described in http://stackoverflow.com/a/2530948/126014
    length caravans `seq` writeFile fileName $ show (c : caravans)
    where fileName = dir </> fileNameForCaravan d
  2. ploeh created this gist Mar 18, 2016.
    61 changes: 61 additions & 0 deletions ApiModel.hs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,61 @@
    module ApiModel where

    import Data.Time (ZonedTime(..), parseTimeM, defaultTimeLocale, iso8601DateFormat)

    data ReservationRendition = ReservationRendition
    { rDate :: String
    , rName :: String
    , rEmail :: String
    , rQuantity :: Int }
    deriving (Eq, Show, Read)

    data Reservation = Reservation
    { date :: ZonedTime
    , name :: String
    , email :: String
    , quantity :: Int }
    deriving (Show, Read)

    instance Eq Reservation where
    x == y =
    zonedTimeZone (date x) == zonedTimeZone (date y) &&
    zonedTimeToLocalTime (date x) == zonedTimeToLocalTime (date y) &&
    name x == name y &&
    email x == email y &&
    quantity x == quantity y

    data Error = ValidationError String | CapacityExceeded
    deriving (Show, Eq)

    parseDate :: String -> Maybe ZonedTime
    parseDate = parseTimeM True defaultTimeLocale (iso8601DateFormat Nothing)

    validateReservation :: ReservationRendition -> Either Error Reservation
    validateReservation r =
    case parseDate (rDate r) of
    Just d ->
    Right Reservation
    { date = d
    , name = rName r
    , email = rEmail r
    , quantity = rQuantity r }
    Nothing -> Left (ValidationError "Invalid date.")

    checkCapacity :: Int -> Int -> Reservation -> Either Error Reservation
    checkCapacity capacity reservedSeats reservation =
    if capacity < quantity reservation + reservedSeats
    then Left CapacityExceeded
    else Right reservation

    data StatusCode = Forbidden | Accepted -- add more at leisure
    deriving (Eq, Show, Read)

    data HttpResult a = OK a
    | BadRequest String
    | StatusCode StatusCode
    deriving (Eq, Show, Read)

    toHttpResult :: Either Error () -> HttpResult ()
    toHttpResult (Left (ValidationError msg)) = BadRequest msg
    toHttpResult (Left CapacityExceeded) = StatusCode Forbidden
    toHttpResult (Right ()) = OK ()
    19 changes: 19 additions & 0 deletions App.hs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,19 @@
    module App
    (
    postReservation
    ) where

    import ApiModel
    import DB
    import Control.Monad.Trans (liftIO)
    import Control.Monad.Trans.Either (EitherT(..), hoistEither)

    connStr :: ConnectionString
    connStr = "."

    postReservation :: ReservationRendition -> IO (HttpResult ())
    postReservation candidate = fmap toHttpResult $ runEitherT $ do
    r <- hoistEither $ validateReservation candidate
    i <- liftIO $ getReservedSeatsFromDB connStr $ date r
    hoistEither $ checkCapacity 10 i r
    >>= liftIO . saveReservation connStr
    40 changes: 40 additions & 0 deletions DB.hs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,40 @@
    module DB
    (
    ConnectionString
    , readReservationsFromDB
    , getReservedSeatsFromDB
    , saveReservation
    ) where

    import System.Directory (doesFileExist)
    import System.FilePath ((</>))
    import Data.Time (FormatTime(..), ZonedTime(..), formatTime, defaultTimeLocale)
    import ApiModel

    type ConnectionString = String

    rawFileNameForDate :: FormatTime t => t -> String
    rawFileNameForDate = formatTime defaultTimeLocale "%F"

    fileNameForReservation :: Reservation -> FilePath
    fileNameForReservation = (++ ".txt") . rawFileNameForDate . date

    readReservationsFromDB :: ConnectionString -> ZonedTime -> IO [Reservation]
    readReservationsFromDB dir d = do -- Imagine that this queries a database table instead of reading from a file
    exists <- doesFileExist fileName
    if exists
    then read <$> readFile fileName
    else return []
    where fileName = dir </> rawFileNameForDate d ++ ".txt"

    getReservedSeatsFromDB :: ConnectionString -> ZonedTime -> IO Int
    getReservedSeatsFromDB dir d = do
    reservations <- readReservationsFromDB dir d
    return (foldr ((+) . quantity) 0 reservations)

    saveReservation :: ConnectionString -> Reservation -> IO ()
    saveReservation dir r = do --Imagine that this inserts into a database table instead of writing to a file
    reservations <- readReservationsFromDB dir (date r)
    -- Use of `seq` as described in http://stackoverflow.com/a/2530948/126014
    length reservations `seq` writeFile fileName $ show (r : reservations)
    where fileName = dir </> fileNameForReservation r