packages feed

pinch-0.2.0.0: tests/Pinch/Internal/PinchableSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies      #-}
module Pinch.Internal.PinchableSpec (spec) where

import Control.Applicative
import Control.Monad
import Data.ByteString       (ByteString)
import Data.HashMap.Strict   (HashMap)
import Data.HashSet          (HashSet)
import Data.Int              (Int16, Int32, Int8)
import Data.Map.Strict       (Map)
import Data.Set              (Set)
import Data.Text             (Text)
import Data.Vector           (Vector)
import Test.Hspec
import Test.Hspec.QuickCheck

import qualified Data.ByteString     as B
import qualified Data.HashMap.Strict as HM
import qualified Data.HashSet        as HS
import qualified Data.Map.Strict     as M
import qualified Data.Set            as S
import qualified Data.Vector         as Vec

import Pinch.Arbitrary
import Pinch.Expectations
import Pinch.Internal.Pinchable ((.:), (.:?), (.=), (?=))
import Pinch.Internal.Util

import qualified Pinch.Internal.Pinchable as P
import qualified Pinch.Internal.TType     as T
import qualified Pinch.Internal.Value     as V


data AnEnum = EnumA | EnumB | EnumC
    deriving (Show, Ord, Eq)

instance P.Pinchable AnEnum where
    type Tag AnEnum = T.TEnum

    pinch EnumA = P.pinch (1 :: Int32)
    pinch EnumB = P.pinch (2 :: Int32)
    pinch EnumC = P.pinch (3 :: Int32)

    unpinch = P.unpinch >=> \v -> case (v :: Int32) of
        1 -> return EnumA
        2 -> return EnumB
        3 -> return EnumC
        _ -> fail "Unknown enum value"

unpinch' :: P.Pinchable a => V.Value (P.Tag a) -> Either String a
unpinch' = P.runParser . P.unpinch

enumSpec :: Spec
enumSpec = describe "Enum" $ do

    it "can pinch and unpinch" $ do
        P.pinch EnumA `shouldBe` vi32 1
        P.pinch EnumB `shouldBe` vi32 2
        P.pinch EnumC `shouldBe` vi32 3

        unpinch' (vi32 1) `shouldBe` Right EnumA
        unpinch' (vi32 2) `shouldBe` Right EnumB
        unpinch' (vi32 3) `shouldBe` Right EnumC

    it "reject invalid values" $
        unpinch' (vi32 4) `shouldBe`
            (Left "Unknown enum value" :: Either String AnEnum)


data AUnion
    = UnionDouble Double
    | UnionByte Int8
    | UnionSet (Set AnEnum)
  deriving (Show, Ord, Eq)

instance P.Pinchable AUnion where
    type Tag AUnion = T.TUnion

    pinch (UnionDouble d) = P.union 1 d
    pinch (UnionByte i) = P.union 2 i
    pinch (UnionSet s) = P.union 5 s

    unpinch m =
            UnionDouble <$> m .: 1
        <|> UnionByte   <$> m .: 2
        <|> UnionSet    <$> m .: 5

unionSpec :: Spec
unionSpec = describe "Union" $ do

    prop "can pinch (1)" $ \dub ->
        P.pinch (UnionDouble dub) `shouldBe`
            vstruct [(1, vdub_ dub)]

    prop "can pinch (2)" $ \byt ->
        P.pinch (UnionByte byt) `shouldBe`
            vstruct [(2, vbyt_ byt)]

    it "can pinch (3)" $
        P.pinch (UnionSet $ S.fromList [EnumA, EnumB]) `shouldBe`
            vstruct [(5, vset_ [vi32 1, vi32 2])]

    it "can unpinch" $ do
        unpinch' (vstruct [(1, vdub_ 12.34)])
            `shouldBe` Right (UnionDouble 12.34)

        unpinch' (vstruct [(2, vbyt_ 123)])
            `shouldBe` Right (UnionByte 123)

        unpinch'
            (vstruct [(5, vset_ [vi32 1, vi32 2])])
            `shouldBe` Right (UnionSet $ S.fromList [EnumA, EnumB])

    it "reject invalid types" $ do
        (unpinch' :: V.Value T.TUnion -> Either String AUnion)
          (vstruct [(1, vi32_ 1)])
            `leftShouldContain` "is absent"

        (unpinch' :: V.Value T.TUnion -> Either String AUnion)
          (vstruct [(2, vbool_ True)])
            `leftShouldContain` "is absent"

        (unpinch' :: V.Value T.TUnion -> Either String AUnion)
          (vstruct [(5, vlist_ [vi32 1, vi32 2])])
            `leftShouldContain` "has the incorrect type"

    it "reject invalid IDs" $
        (unpinch' :: V.Value T.TUnion -> Either String AUnion)
          (vstruct [(3, vdub_ 1.0)])
            `leftShouldContain` "is absent"


data AStruct = AStruct ByteString (Maybe Int32)
  deriving (Show, Ord, Eq)

instance P.Pinchable AStruct where
    type Tag AStruct = T.TStruct

    pinch (AStruct a b) = P.struct [1 .= a, 5 ?= b]
    unpinch m = AStruct <$> m .: 1 <*> m .:? 5

structSpec :: Spec
structSpec = describe "Struct" $ do

    it "can pinch and unpinch" $ do
        P.pinch (AStruct "foo" Nothing)
            `shouldBe` vstruct [(1, vbin_ "foo")]

        P.pinch (AStruct "bar" (Just 42))
            `shouldBe` vstruct
                [ (1, vbin_ "bar")
                , (5, vi32_ 42)
                ]

        unpinch' (vstruct [(1, vbin_ "hello")])
            `shouldBe` Right (AStruct "hello" Nothing)

        unpinch'
          (vstruct
            [ (1, vbin_ "hello")
            , (5, vi32_ 42)
            ]) `shouldBe` Right (AStruct "hello" (Just 42))

    it "ignores unrecognized fields" $ do
        unpinch'
          (vstruct
            [ (1, vbin_ "foo")
            , (2, vi32_ 42)
            ]) `shouldBe` Right (AStruct "foo" Nothing)

        unpinch'
          (vstruct
            [ (1, vbin_ "foo")
            , (4, vbyt_ 12)
            , (5, vi32_ 34)
            ]) `shouldBe` Right (AStruct "foo" (Just 34))

    it "rejects missing required fields" $
        (unpinch' :: V.Value T.TStruct -> Either String AStruct)
          (vstruct
            [ (4, vbyt_ 12)
            , (5, vi32_ 34)
            ]) `leftShouldContain` "1 is absent"

    it "rejects invalid types" $
        (unpinch' :: V.Value T.TStruct -> Either String AStruct)
          (vstruct
            [ (1, vlist_ [vi32 42])
            ]) `leftShouldContain` "has the incorrect type"


primitivesSpec :: Spec
primitivesSpec = do

    it "can pinch and unpinch Bools" $ do
        P.pinch True `shouldBe` vbool True
        P.pinch False `shouldBe` vbool False

        unpinch' (vbool True) `shouldBe` Right True
        unpinch' (vbool False) `shouldBe` Right False

    prop "can pinch and unpinch Int8" $ \i -> do
        P.pinch i `shouldBe` vbyt i
        unpinch' (vbyt i) `shouldBe` Right i

    prop "can pinch and unpinch Int32" $ \i -> do
        P.pinch i `shouldBe` vi32 i
        unpinch' (vi32 i) `shouldBe` Right i

    prop "can pinch and unpinch Int64" $ \i -> do
        P.pinch i `shouldBe` vi64 i
        unpinch' (vi64 i) `shouldBe` Right i

    prop "can pinch and unpinch Double" $ \d -> do
        P.pinch d `shouldBe` vdub d
        unpinch' (vdub d) `shouldBe` Right d

    prop "can pinch and unpinch ByteString" $ \(SomeByteString bs) -> do
        P.pinch bs `shouldBe` vbin bs
        unpinch' (vbin bs) `shouldBe` Right bs

    it "can pinch and unpinch Text" $ do
        P.pinch ("☕️" :: Text)
            `shouldBe` vbin (B.pack [0xe2, 0x98, 0x95, 0xef, 0xb8, 0x8f])

        unpinch' (vbin (B.pack [0xe2, 0x98, 0x95, 0xef, 0xb8, 0x8f]))
            `shouldBe` Right ("☕️" :: Text)


containerSpec :: Spec
containerSpec = do

    describe "Vector" $ do
        it "can pinch and unpinch" $ do

            P.pinch (Vec.fromList [1, 2, 3 :: Int32])
                `shouldBe` vlist [vi32 1, vi32 2, vi32 3]

            unpinch' (vlist [vi32 1, vi32 2, vi32 3])
                `shouldBe` Right (Vec.fromList [1, 2, 3 :: Int32])

        it "rejects type mismatch" $
          (unpinch' :: V.Value T.TList -> Either String (Vector Int8))
            (vlist [vi32 1, vi32 2, vi32 3])
                `leftShouldContainAll`
                    ["Type mismatch", "Expected TByte", "Got TInt32"]

    describe "List" $ do

        it "can pinch and unpinch" $ do

            P.pinch ([1, 2, 3] :: [Int32])
                `shouldBe` vlist [vi32 1, vi32 2, vi32 3]

            unpinch' (vlist [vi32 1, vi32 2, vi32 3])
                `shouldBe` Right ([1, 2, 3] :: [Int32])

        it "rejects type mismatch" $
          (unpinch' :: V.Value T.TList -> Either String [Int8])
            (vlist [vi32 1, vi32 2, vi32 3])
                `leftShouldContain` "Type mismatch"

    describe "HashSet" $ do
        it "can pinch and unpinch" $ do

            P.pinch (HS.fromList [1, 2, 3 :: Int32])
                `shouldBe` vset [vi32 1, vi32 2, vi32 3]

            unpinch' (vset [vi32 1, vi32 2, vi32 3])
                `shouldBe` Right (HS.fromList [1, 2, 3 :: Int32])

        it "rejects type mismatch" $
          (unpinch' :: V.Value T.TSet -> Either String (HashSet Int8))
            (vset [vi32 1, vi32 2, vi32 3])
                `leftShouldContain` "Type mismatch"

    describe "Set" $ do
        it "can pinch and unpinch" $ do

            P.pinch (S.fromList [1, 2, 3 :: Int32])
                `shouldBe` vset [vi32 1, vi32 2, vi32 3]

            unpinch' (vset [vi32 1, vi32 2, vi32 3])
                `shouldBe` Right (S.fromList [1, 2, 3 :: Int32])

        it "rejects type mismatch" $
          (unpinch' :: V.Value T.TSet -> Either String (Set Int8))
            (vset [vi32 1, vi32 2, vi32 3])
                `leftShouldContain` "Type mismatch"

    describe "HashMap" $ do

        it "can pinch and unpinch" $ do

            P.pinch (HM.fromList [("a", 1), ("b", 2) :: (ByteString, Int16)])
                `shouldBe` vmap
                    [ (vbin "a", vi16 1)
                    , (vbin "b", vi16 2)
                    ]

            unpinch'
              (vmap [ (vbin "a", vi16 1)
                      , (vbin "b", vi16 2)
                      ]) `shouldBe`
                        Right
                          (HM.fromList
                            [("a", 1), ("b", 2) :: (ByteString, Int16)])

        it "rejects key type mismatch" $
          (unpinch' :: V.Value T.TMap -> Either String (HashMap Int32 Int16))
              (vmap [(vbin "a", vi16 1)])
                  `leftShouldContain` "Type mismatch"

        it "rejects value type mismatch" $
          (unpinch' :: V.Value T.TMap -> Either String (HashMap ByteString Bool))
              (vmap [(vbin "a", vi16 1)])
                  `leftShouldContain` "Type mismatch"

    describe "Map" $ do

        it "can pinch and unpinch" $ do

            P.pinch (M.fromList [("a", 1), ("b", 2) :: (ByteString, Int16)])
                `shouldBe` vmap
                    [ (vbin "a", vi16 1)
                    , (vbin "b", vi16 2)
                    ]

            unpinch'
              (vmap [ (vbin "a", vi16 1)
                      , (vbin "b", vi16 2)
                      ]) `shouldBe`
                        Right
                          (M.fromList
                            [("a", 1), ("b", 2) :: (ByteString, Int16)])

        it "rejects key type mismatch" $
          (unpinch' :: V.Value T.TMap -> Either String (Map Int32 Int16))
              (vmap [(vbin "a", vi16 1)])
                  `leftShouldContain` "Type mismatch"

        it "rejects value type mismatch" $
          (unpinch' :: V.Value T.TMap -> Either String (Map ByteString Bool))
              (vmap [(vbin "a", vi16 1)])
                  `leftShouldContain` "Type mismatch"


spec :: Spec
spec = describe "Pinchable" $ do
    primitivesSpec
    containerSpec
    enumSpec
    unionSpec
    structSpec