jordan-servant-0.1.0.0: test/Jordan/Servant/Query/RoundtripSpec.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
module Jordan.Servant.Query.RoundtripSpec where
import Control.Applicative
import Control.Monad (guard)
import Data.Attoparsec.ByteString (parseOnly)
import Data.Coerce
import Data.Functor.Contravariant
import Data.Proxy
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import GHC.Generics
import Jordan
import Jordan.Servant.Query.Parse (bracedValue, parseQueryAtKey, parseQueryToKeys, transformToKey, unbracedValue)
import Jordan.Servant.Query.Render (addBracked, escapeRawComponent, renderQueryAtKey)
import Network.HTTP.Types.URI
import Test.Hspec
import Test.QuickCheck
import Test.QuickCheck.Utf8
data Color = Red | Green | Blue | Black | Orange | Yellow | White
deriving (Show, Read, Eq, Ord, Generic, Bounded, Enum)
deriving anyclass (ToJSON, FromJSON)
data HomogenousCoord = HomogenousCoord {x :: Int, y :: Int, z :: Int}
deriving (Show, Read, Eq, Ord, Generic)
instance FromJSON HomogenousCoord where
fromJSON =
parseObject
( HomogenousCoord
<$> parseField "x"
<*> parseField "y"
<*> parseFieldWithDefault "z" fromJSON 0
)
instance ToJSON HomogenousCoord where
toJSON =
serializeObject $
divide
(\(HomogenousCoord x y z) -> (x, (y, z)))
(serializeField "x")
$ divide
id
(serializeField "y")
$ contramap
(\x -> if x == 0 then Nothing else Just x)
(serializeJust "z" toJSON)
instance Arbitrary HomogenousCoord where
arbitrary =
HomogenousCoord
<$> arbitrary
<*> arbitrary
<*> arbitrary
shrink (HomogenousCoord x y z) =
HomogenousCoord <$> shrink x <*> shrink y <*> shrink z
newtype Coords = Coords {getCoords :: [HomogenousCoord]}
deriving (Show, Read, Eq, Ord, Generic)
deriving (Arbitrary) via [HomogenousCoord]
deriving via [HomogenousCoord] instance (FromJSON Coords)
deriving via [HomogenousCoord] instance (ToJSON Coords)
instance Arbitrary Color where
arbitrary = arbitraryBoundedEnum
shrinkText :: T.Text -> [T.Text]
shrinkText t
| T.length t == 0 = []
| T.length t == 1 = [mempty]
| otherwise = mempty : (T.pack . pure <$> T.unpack t)
data Person = Person
{ firstName :: T.Text,
lastName :: T.Text,
age :: Int
}
deriving (Show, Read, Eq, Ord, Generic)
deriving anyclass (ToJSON, FromJSON)
instance Arbitrary Person where
arbitrary = Person <$> genValidUtf8 <*> genValidUtf8 <*> arbitrary
shrink (Person fn ln a) = Person <$> shrinkText fn <*> shrinkText ln <*> shrink a
data WeirdEnum
= WeirdNothing
| WeirdDuo Person Person
| WeirdSingle Person
| WeirdColor Color
deriving (Show, Read, Eq, Ord, Generic)
deriving anyclass (ToJSON, FromJSON)
instance Arbitrary WeirdEnum where
arbitrary =
oneof
[ pure WeirdNothing,
WeirdDuo <$> arbitrary <*> arbitrary,
WeirdSingle <$> arbitrary,
WeirdColor <$> arbitrary
]
shrink (WeirdDuo lhs rhs) =
(WeirdDuo <$> shrink lhs <*> shrink rhs)
<|> (WeirdDuo lhs <$> shrink rhs)
<|> (WeirdDuo <$> shrink lhs <*> pure rhs)
shrink (WeirdSingle a) = WeirdSingle <$> shrink a
shrink _ = []
newtype Feedback = Feedback {ratings :: [(T.Text, Int)]}
deriving (Show, Read, Eq, Ord, Generic)
instance FromJSON Feedback where
fromJSON = Feedback <$> parseDictionary fromJSON
instance ToJSON Feedback where
toJSON = contramap ratings $ serializeDictionary toJSON
instance Arbitrary Feedback where
arbitrary = Feedback <$> liftArbitrary ((,) <$> genValidUtf8 <*> arbitrary)
shrink (Feedback xs) = Feedback <$> liftShrink shrinkPair xs
where
shrinkPair (l, k) = do
l' <- shrinkText l
k' <- shrink k
pure (l', k')
data PersonFeedback = PersonFeedback {feedbackGiver :: Person, feedbackGiven :: Feedback}
deriving (Show, Read, Eq, Ord, Generic)
deriving anyclass (ToJSON, FromJSON)
instance Arbitrary PersonFeedback where
arbitrary = PersonFeedback <$> arbitrary <*> arbitrary
shrink (PersonFeedback p f) = PersonFeedback <$> shrink p <*> shrink f
data CoverBases
= CoverNest PersonFeedback
| CoverNull ()
| CoverArray [PersonFeedback]
deriving (Show, Read, Eq, Ord, Generic)
deriving anyclass (ToJSON, FromJSON)
instance Arbitrary CoverBases where
arbitrary =
oneof
[ CoverNest <$> arbitrary,
pure (CoverNull ()),
CoverArray <$> arbitrary
]
shrink = \case
CoverNest pf -> CoverNest <$> shrink pf
CoverNull x0 -> []
CoverArray xs -> CoverArray <$> shrink xs
robustShrinkWith :: (a -> [a]) -> [a] -> [[a]]
robustShrinkWith _ [] = []
robustShrinkWith shrink (x : xs) =
robustShrinkWith shrink xs
++ [x : xs' | xs' <- robustShrinkWith shrink xs]
++ [x' : xs | x' <- shrink x]
++ [shrink x]
newtype QueryKey = QueryKey {getQueryKey :: T.Text}
deriving (Show, Read, Eq, Ord, Generic)
instance Arbitrary QueryKey where
arbitrary =
QueryKey
<$> genValidUtf8 `suchThat` (\t -> T.length t /= 0)
shrink (QueryKey k)
| T.length k < 2 = []
| otherwise = QueryKey . T.pack . pure <$> T.unpack k
queryEquals :: (ToJSON a, FromJSON a, Show a, Arbitrary a, Eq a) => Proxy a -> Property
queryEquals (Proxy :: Proxy a) = forAllShrink arbitrary shrink cb
where
cb :: (QueryKey, a) -> Property
cb (key, v) =
let key' = getQueryKey key
renderedKey = urlEncode True (encodeUtf8 key')
unrenderedKey = decodeUtf8 $ urlDecode True renderedKey
rendered = renderQuery False $ renderQueryAtKey key' v
parsedBS = parseQuery rendered
transformed = transformToKey key' parsedBS
toKeys = parseQueryToKeys parsedBS
parsed = parseQueryAtKey @a key' $ parseQuery rendered
in counterexample
("Raw was " <> show rendered)
(parsed === Right v)
serializesInBraces :: T.Text -> Bool
serializesInBraces t =
let inBraces = addBracked t mempty
outOfBraces = parseOnly bracedValue inBraces
in outOfBraces == Right t
testBraces :: T.Text -> Spec
testBraces t =
it ("properly serializes the key " <> show t <> " in braces") $
let inBraces = addBracked t mempty
outOfBraces = parseOnly bracedValue inBraces
in outOfBraces `shouldBe` Right t
spec :: Spec
spec = do
it "round-trips the base key" $
property $
forAll arbitrary $ \k ->
let key = getQueryKey k
escaped = escapeRawComponent key
in counterexample ("Escaped was " <> show escaped) $
parseOnly unbracedValue escaped === Right key
describe "round-tripping" $ do
it "works for enums" $ queryEquals (Proxy @Color)
it "works for structs" $ queryEquals (Proxy @Person)
it "works for weird stuff" $ queryEquals (Proxy @WeirdEnum)
it "works for dictionaries" $ queryEquals (Proxy @Feedback)
it "works for person feedback" $ queryEquals (Proxy @PersonFeedback)
it "works for a weird sum type crap" $ queryEquals (Proxy @CoverBases)
describe "round-tripping bracked values" $ do
testBraces "[foo"
testBraces "\\[foo"
testBraces "foo]"
testBraces "[foo]"
it "works for arbitrary values" $
forAllShrink genValidUtf8 shrinkText $ \t ->
let inBraces = addBracked t mempty
outOfBraces = parseOnly bracedValue inBraces
in outOfBraces == Right t