packages feed

taiwan-id-0.1.1.0: components/lib/taiwan-id-test-common/Taiwan/ID/Test.hs

{-# LANGUAGE NamedFieldPuns #-}

module Taiwan.ID.Test where

import Control.Monad
  ( replicateM
  )
import Data.Function
  ( (&)
  )
import Data.Text
  ( Text
  )
import GHC.Stack
  ( HasCallStack
  )
import Taiwan.ID
  ( ID (ID, c0, c1, c2, c3, c4, c5, c6, c7, c8)
  )
import Taiwan.ID.Digit
  ( Digit
  )
import Taiwan.ID.Digit1289
  ( Digit1289
  )
import Taiwan.ID.Gender
  ( Gender
  )
import Taiwan.ID.Issuer
  ( Issuer
  )
import Taiwan.ID.Language
  ( Language
  )
import Taiwan.ID.Letter
  ( Letter
  )
import Taiwan.ID.Region
  ( Region
  )
import Test.QuickCheck
  ( Gen
  , arbitraryBoundedEnum
  , choose
  , elements
  , oneof
  , shrinkBoundedEnum
  )

import qualified Data.Text as Text
import qualified Taiwan.ID as ID
import qualified Taiwan.ID.Digit as Digit

--------------------------------------------------------------------------------
-- General-purpose generators and shrinkers
--------------------------------------------------------------------------------

genDigit :: Gen Digit
genDigit = arbitraryBoundedEnum

shrinkDigit :: Digit -> [Digit]
shrinkDigit = shrinkBoundedEnum

genDigit1289 :: Gen Digit1289
genDigit1289 = arbitraryBoundedEnum

shrinkDigit1289 :: Digit1289 -> [Digit1289]
shrinkDigit1289 = shrinkBoundedEnum

genGender :: Gen Gender
genGender = arbitraryBoundedEnum

shrinkGender :: Gender -> [Gender]
shrinkGender = shrinkBoundedEnum

genID :: Gen ID
genID =
  ID
    <$> genLetter
    <*> genDigit1289
    <*> genDigit
    <*> genDigit
    <*> genDigit
    <*> genDigit
    <*> genDigit
    <*> genDigit
    <*> genDigit

shrinkID :: ID -> [ID]
shrinkID i@ID {c0, c1, c2, c3, c4, c5, c6, c7, c8} =
  mconcat
    [ [i {c0 = c0'} | c0' <- c0 & shrinkLetter]
    , [i {c1 = c1'} | c1' <- c1 & shrinkDigit1289]
    , [i {c2 = c2'} | c2' <- c2 & shrinkDigit]
    , [i {c3 = c3'} | c3' <- c3 & shrinkDigit]
    , [i {c4 = c4'} | c4' <- c4 & shrinkDigit]
    , [i {c5 = c5'} | c5' <- c5 & shrinkDigit]
    , [i {c6 = c6'} | c6' <- c6 & shrinkDigit]
    , [i {c7 = c7'} | c7' <- c7 & shrinkDigit]
    , [i {c8 = c8'} | c8' <- c8 & shrinkDigit]
    ]

genIssuer :: Gen Issuer
genIssuer = arbitraryBoundedEnum

shrinkIssuer :: Issuer -> [Issuer]
shrinkIssuer = shrinkBoundedEnum

genLanguage :: Gen Language
genLanguage = arbitraryBoundedEnum

shrinkLanguage :: Language -> [Language]
shrinkLanguage = shrinkBoundedEnum

genLetter :: Gen Letter
genLetter = arbitraryBoundedEnum

shrinkLetter :: Letter -> [Letter]
shrinkLetter = shrinkBoundedEnum

genRegion :: Gen Region
genRegion = arbitraryBoundedEnum

shrinkRegion :: Region -> [Region]
shrinkRegion = shrinkBoundedEnum

--------------------------------------------------------------------------------
-- Specialised generators
--------------------------------------------------------------------------------

genInvalidChar :: Gen Char
genInvalidChar = elements "+!@#$%^&*()"

genIDText :: Gen Text
genIDText =
  oneof
    [ genIDTextValid
    , genIDTextInvalid
    ]

genIDTextValid :: Gen Text
genIDTextValid = ID.toText <$> genID

genIDTextInvalid :: Gen Text
genIDTextInvalid =
  oneof
    [ genIDTextInvalidChar
    , genIDTextInvalidChecksum
    , genIDTextInvalidLength
    ]

genIDTextInvalidChar :: Gen Text
genIDTextInvalidChar =
  genIDTextInvalidCharAtIndex =<< choose (0, 9)

genIDTextInvalidCharAtIndex :: Int -> Gen Text
genIDTextInvalidCharAtIndex index =
  unsafePokeChar index <$> genIDTextValid <*> genInvalidChar

genIDTextInvalidChecksum :: Gen Text
genIDTextInvalidChecksum = do
  idTextValid <- genIDTextValid
  let charOld = unsafePeekChar checksumCharIndex idTextValid
  let charNew = Digit.toChar (unsafeDigitFromChar charOld + 1)
  pure $ unsafePokeChar checksumCharIndex idTextValid charNew
  where
    checksumCharIndex = 9

genIDTextInvalidLength :: Gen Text
genIDTextInvalidLength =
  oneof
    [ genIDTextInvalidLengthTooLong
    , genIDTextInvalidLengthTooShort
    ]

genIDTextInvalidLengthTooShort :: Gen Text
genIDTextInvalidLengthTooShort = do
  idTextValid <- genIDTextValid
  invalidLength <- choose (0, 9)
  pure (Text.take invalidLength idTextValid)

genIDTextInvalidLengthTooLong :: Gen Text
genIDTextInvalidLengthTooLong = do
  idTextValid <- genIDTextValid
  extraCharCount <- choose (1, 4)
  extraChars <- Text.pack <$> replicateM extraCharCount genChar
  pure (idTextValid <> extraChars)
  where
    genChar = choose ('0', '9')

--------------------------------------------------------------------------------
-- Utilities
--------------------------------------------------------------------------------

unsafeDigitFromChar :: HasCallStack => Char -> Digit
unsafeDigitFromChar c = case Digit.fromChar c of
  Nothing -> error "unsafeDigitFromChar"
  Just d -> d

unsafePeekChar :: HasCallStack => Int -> Text -> Char
unsafePeekChar i t
  | i < indexMin = outOfBoundsError
  | i > indexMax = outOfBoundsError
  | otherwise = Text.index t i
  where
    indexMin = 0
    indexMax = Text.length t - 1
    outOfBoundsError = error "unsafePeekChar: index out of bounds"

unsafePokeChar :: HasCallStack => Int -> Text -> Char -> Text
unsafePokeChar i t c
  | i < indexMin = outOfBoundsError
  | i > indexMax = outOfBoundsError
  | otherwise = Text.take i t <> Text.singleton c <> Text.drop (i + 1) t
  where
    indexMin = 0
    indexMax = Text.length t - 1
    outOfBoundsError = error "unsafePokeChar: index out of bounds"