Structured TermIdentifier & tests for it

This commit is contained in:
Gregor Kleen 2017-10-04 16:20:29 +02:00
parent 12d7fbd10f
commit 1877fb81b4
6 changed files with 121 additions and 23 deletions

7
models
View File

@ -3,14 +3,13 @@ User
ident Text ident Text
UniqueAuthentication plugin ident UniqueAuthentication plugin ident
Term json Term json
name Text name TermIdentifier
shorthand Text
start Day start Day
end Day end Day
holidays [Day] holidays [Day]
-- UniqueTerm shorthand -- UniqueTerm shorthand
Primary shorthand Primary name
deriving Eq deriving Show
School json School json
name Text name Text
shorthand Text shorthand Text

View File

@ -49,6 +49,7 @@ dependencies:
- cryptonite-conduit - cryptonite-conduit
- base64-bytestring - base64-bytestring
- memory - memory
- http-api-data
# The library contains all of our application code. The executable # The library contains all of our application code. The executable
# defined below is just a thin wrapper. # defined below is just a thin wrapper.
@ -61,6 +62,7 @@ library:
- -Wall - -Wall
- -fwarn-tabs - -fwarn-tabs
- -O0 - -O0
- -ddump-splices
cpp-options: -DDEVELOPMENT cpp-options: -DDEVELOPMENT
else: else:
ghc-options: ghc-options:
@ -92,6 +94,7 @@ tests:
dependencies: dependencies:
- uniworx - uniworx
- hspec >=2.0.0 - hspec >=2.0.0
- QuickCheck
- yesod-test - yesod-test
# Define flags used by "yesod devel" to make compilation faster # Define flags used by "yesod devel" to make compilation faster

View File

@ -25,8 +25,5 @@ import Model.Types
share [mkPersist sqlSettings, mkMigrate "migrateAll"] share [mkPersist sqlSettings, mkMigrate "migrateAll"]
$(persistFileWith lowerCaseSettings "models") $(persistFileWith lowerCaseSettings "models")
instance Show Term where
show = ClassyPrelude.Yesod.unpack . termName

View File

@ -1,31 +1,94 @@
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-}
module Model.Types where module Model.Types where
import ClassyPrelude
import Database.Persist.TH import Database.Persist.TH
import Database.Persist.Class
import Database.Persist.Sql
import Web.HttpApiData
import Data.Text (Text)
import qualified Data.Text as Text
import Text.Read (readMaybe)
-- import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import Yesod.Core.Dispatch (PathPiece(..))
import Data.Aeson (FromJSON(..), ToJSON(..), withText, Value(..))
import GHC.Generics (Generic)
import Data.Typeable (Typeable)
data SheetType = Regular | Bonus | Extra data SheetType = Regular | Bonus | Extra
deriving (Show, Read, Eq, Ord, Enum, Bounded) deriving (Show, Read, Eq, Ord, Enum, Bounded)
derivePersistField "SheetType" derivePersistField "SheetType"
data ExamStatus = Attended | NoShow | Voided data ExamStatus = Attended | NoShow | Voided
deriving (Show, Read, Eq, Ord, Enum, Bounded) deriving (Show, Read, Eq, Ord, Enum, Bounded)
derivePersistField "ExamStatus" derivePersistField "ExamStatus"
{- Modelled in DB
data Season = WS | SO
deriving (Show, Read, Eq, Enum)
derivePersistField "Season"
data Term = Term { season:: Season, year :: Int } data Season = Summer | Winter
deriving (Show, Read, Eq, Ord, Enum, Bounded, Generic, Typeable)
instance Show Term where seasonToChar :: Season -> Char
show (Term {season, year}) = take 1 (show season) ++ (show year) seasonToChar Summer = 'S'
seasonToChar Winter = 'W'
instance PersistField Term where seasonFromChar :: Char -> Either Text Season
toPersistValue (Term {season, year}) = undefined seasonFromChar c
fromPersistValue (Term {season, year}) = undefined | c ~= 'S' = Right Summer
sqlType _ = SqlInteger | c ~= 'W' = Right Winter
isNullable _ = False | otherwise = Left $ "Invalid season character: " <> tshow c <> ""
-} where
(~=) = (==) `on` CI.mk
data TermIdentifier = TermIdentifier
{ year :: Integer -- ^ Using 'Integer' to model years is consistent with 'Data.Time.Calendar'
, season :: Season
} deriving (Show, Read, Eq, Ord, Generic, Typeable)
termToText :: TermIdentifier -> Text
termToText TermIdentifier{..} = Text.pack $ seasonToChar season : show year
termFromText :: Text -> Either Text TermIdentifier
termFromText t
| (s:ys) <- Text.unpack t
, Just year <- readMaybe ys
, Right season <- seasonFromChar s
= Right TermIdentifier{..}
| otherwise = Left $ "Invalid TermIdentifier: “" <> t <> ""
instance PersistField TermIdentifier where
toPersistValue = PersistText . termToText
fromPersistValue (PersistText t) = termFromText t
fromPersistValue x = Left $ "Expected TermIdentifier, received: " <> tshow x
instance PersistFieldSql TermIdentifier where
sqlType _ = SqlString
instance ToHttpApiData TermIdentifier where
toUrlPiece = termToText
instance FromHttpApiData TermIdentifier where
parseUrlPiece = termFromText
instance PathPiece TermIdentifier where
fromPathPiece = either (const Nothing) Just . termFromText
toPathPiece = termToText
instance ToJSON TermIdentifier where
toJSON = String . termToText
instance FromJSON TermIdentifier where
parseJSON = withText "Term" $ either (fail . Text.unpack) return . termFromText

34
test/Model/TypesSpec.hs Normal file
View File

@ -0,0 +1,34 @@
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Model.TypesSpec where
import TestImport
instance Arbitrary Season where
arbitrary = elements [minBound..maxBound]
shrink = genericShrink
instance Arbitrary TermIdentifier where
arbitrary = do
season <- arbitrary
year <- arbitrary
return $ TermIdentifier{..}
shrink = genericShrink
spec :: Spec
spec = do
describe "TermIdentifier" $ do
it "has compatible encoding/decoding to/from Text" . property $
\term -> termFromText (termToText term) == Right term
it "works for some examples" . mapM_ termExample $
[ (TermIdentifier 2017 Summer, "S2017")
, (TermIdentifier 1995 Winter, "W1995")
]
termExample :: (TermIdentifier, Text) -> Expectation
termExample (term, encoded) = example $ do
termFromText encoded `shouldBe` Right term
encoded `shouldBe` termToText term

View File

@ -20,6 +20,8 @@ import Yesod.Default.Config2 (useEnv, loadYamlSettings)
import Yesod.Auth as X import Yesod.Auth as X
import Yesod.Test as X import Yesod.Test as X
import Yesod.Core.Unsafe (fakeHandlerGetLogger) import Yesod.Core.Unsafe (fakeHandlerGetLogger)
import Test.QuickCheck as X
import Test.QuickCheck.Gen as X
runDB :: SqlPersistM a -> YesodExample UniWorX a runDB :: SqlPersistM a -> YesodExample UniWorX a
runDB query = do runDB query = do