{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE PackageImports #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Data.Swagger.Schema.ValidationSpec where import Control.Applicative import Data.Aeson import Data.Aeson.Types import Data.Int import Data.IntMap (IntMap) import Data.Hashable (Hashable) import "unordered-containers" Data.HashSet (HashSet) import qualified "unordered-containers" Data.HashSet as HashSet import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import Data.Map (Map) import Data.Monoid (mempty) import Data.Proxy import Data.Time import qualified Data.Text as T import qualified Data.Text.Lazy as TL import Data.Set (Set) import Data.Word import GHC.Generics import Data.Swagger import Data.Swagger.Declare import Test.Hspec import Test.Hspec.QuickCheck import Test.QuickCheck shouldValidate :: (ToJSON a, ToSchema a) => Proxy a -> a -> Bool shouldValidate _ x = validateToJSON x == [] shouldNotValidate :: forall a. ToSchema a => (a -> Value) -> a -> Bool shouldNotValidate f = not . null . validateJSON defs sch . f where (defs, sch) = runDeclare (declareSchema (Proxy :: Proxy a)) mempty spec :: Spec spec = do describe "Validation" $ do prop "Bool" $ shouldValidate (Proxy :: Proxy Bool) prop "Char" $ shouldValidate (Proxy :: Proxy Char) prop "Double" $ shouldValidate (Proxy :: Proxy Double) prop "Float" $ shouldValidate (Proxy :: Proxy Float) prop "Int" $ shouldValidate (Proxy :: Proxy Int) prop "Int8" $ shouldValidate (Proxy :: Proxy Int8) prop "Int16" $ shouldValidate (Proxy :: Proxy Int16) prop "Int32" $ shouldValidate (Proxy :: Proxy Int32) prop "Int64" $ shouldValidate (Proxy :: Proxy Int64) prop "Integer" $ shouldValidate (Proxy :: Proxy Integer) prop "Word" $ shouldValidate (Proxy :: Proxy Word) prop "Word8" $ shouldValidate (Proxy :: Proxy Word8) prop "Word16" $ shouldValidate (Proxy :: Proxy Word16) prop "Word32" $ shouldValidate (Proxy :: Proxy Word32) prop "Word64" $ shouldValidate (Proxy :: Proxy Word64) prop "String" $ shouldValidate (Proxy :: Proxy String) prop "()" $ shouldValidate (Proxy :: Proxy ()) prop "ZonedTime" $ shouldValidate (Proxy :: Proxy ZonedTime) prop "UTCTime" $ shouldValidate (Proxy :: Proxy UTCTime) prop "T.Text" $ shouldValidate (Proxy :: Proxy T.Text) prop "TL.Text" $ shouldValidate (Proxy :: Proxy TL.Text) prop "[String]" $ shouldValidate (Proxy :: Proxy [String]) -- prop "(Maybe [Int])" $ shouldValidate (Proxy :: Proxy (Maybe [Int])) prop "(IntMap String)" $ shouldValidate (Proxy :: Proxy (IntMap String)) prop "(Set Bool)" $ shouldValidate (Proxy :: Proxy (Set Bool)) prop "(HashSet Bool)" $ shouldValidate (Proxy :: Proxy (HashSet Bool)) prop "(Either Int String)" $ shouldValidate (Proxy :: Proxy (Either Int String)) prop "(Int, String)" $ shouldValidate (Proxy :: Proxy (Int, String)) prop "(Map String Int)" $ shouldValidate (Proxy :: Proxy (Map String Int)) prop "(Map T.Text Int)" $ shouldValidate (Proxy :: Proxy (Map T.Text Int)) prop "(Map TL.Text Bool)" $ shouldValidate (Proxy :: Proxy (Map TL.Text Bool)) prop "(HashMap String Int)" $ shouldValidate (Proxy :: Proxy (HashMap String Int)) prop "(HashMap T.Text Int)" $ shouldValidate (Proxy :: Proxy (HashMap T.Text Int)) prop "(HashMap TL.Text Bool)" $ shouldValidate (Proxy :: Proxy (HashMap TL.Text Bool)) prop "(Int, String, Double)" $ shouldValidate (Proxy :: Proxy (Int, String, Double)) prop "(Int, String, Double, [Int])" $ shouldValidate (Proxy :: Proxy (Int, String, Double, [Int])) prop "(Int, String, Double, [Int], Int)" $ shouldValidate (Proxy :: Proxy (Int, String, Double, [Int], Int)) prop "Person" $ shouldValidate (Proxy :: Proxy Person) prop "Color" $ shouldValidate (Proxy :: Proxy Color) prop "Paint" $ shouldValidate (Proxy :: Proxy Paint) prop "MyRoseTree" $ shouldValidate (Proxy :: Proxy MyRoseTree) prop "Light" $ shouldValidate (Proxy :: Proxy Light) prop "ButtonImages" $ shouldValidate (Proxy :: Proxy ButtonImages) describe "invalid cases" $ do prop "invalidPersonToJSON" $ shouldNotValidate invalidPersonToJSON prop "invalidColorToJSON" $ shouldNotValidate invalidColorToJSON prop "invalidPaintToJSON" $ shouldNotValidate invalidPaintToJSON prop "invalidLightToJSON" $ shouldNotValidate invalidLightToJSON prop "invalidButtonImagesToJSON" $ shouldNotValidate invalidButtonImagesToJSON main :: IO () main = hspec spec -- ======================================================================== -- Person (simple record with optional fields) -- ======================================================================== data Person = Person { name :: String , phone :: Integer , email :: Maybe String } deriving (Show, Generic) instance ToJSON Person instance ToSchema Person instance Arbitrary Person where arbitrary = Person <$> arbitrary <*> arbitrary <*> arbitrary invalidPersonToJSON :: Person -> Value invalidPersonToJSON Person{..} = object [ T.pack "personName" .= toJSON name , T.pack "personPhone" .= toJSON phone , T.pack "personEmail" .= toJSON email ] -- ======================================================================== -- Color (enum) -- ======================================================================== data Color = Red | Green | Blue deriving (Show, Generic, Bounded, Enum) instance ToJSON Color instance ToSchema Color instance Arbitrary Color where arbitrary = arbitraryBoundedEnum invalidColorToJSON :: Color -> Value invalidColorToJSON Red = toJSON "red" invalidColorToJSON Green = toJSON "green" invalidColorToJSON Blue = toJSON "blue" -- ======================================================================== -- Paint (record with bounded enum property) -- ======================================================================== newtype Paint = Paint { color :: Color } deriving (Show, Generic) instance ToJSON Paint instance ToSchema Paint instance Arbitrary Paint where arbitrary = Paint <$> arbitrary invalidPaintToJSON :: Paint -> Value invalidPaintToJSON = toJSON . color -- ======================================================================== -- MyRoseTree (custom datatypeNameModifier) -- ======================================================================== data MyRoseTree = MyRoseTree { root :: String , trees :: [MyRoseTree] } deriving (Show, Generic) instance ToJSON MyRoseTree instance ToSchema MyRoseTree where declareNamedSchema = genericDeclareNamedSchema defaultSchemaOptions { datatypeNameModifier = drop (length "My") } instance Arbitrary MyRoseTree where arbitrary = fmap (cut limit) $ MyRoseTree <$> arbitrary <*> (take limit <$> arbitrary) where limit = 4 cut 0 (MyRoseTree x _ ) = MyRoseTree x [] cut n (MyRoseTree x xs) = MyRoseTree x (map (cut (n - 1)) xs) -- ======================================================================== -- Light (sum type) -- ======================================================================== data Light = NoLight | LightFreq Double | LightColor Color deriving (Show, Generic) instance ToSchema Light where declareNamedSchema = genericDeclareNamedSchemaUnrestricted defaultSchemaOptions instance ToJSON Light where toJSON = genericToJSON defaultOptions { sumEncoding = ObjectWithSingleField } instance Arbitrary Light where arbitrary = oneof [ return NoLight , LightFreq <$> arbitrary , LightColor <$> arbitrary ] invalidLightToJSON :: Light -> Value invalidLightToJSON = genericToJSON defaultOptions -- ======================================================================== -- ButtonImages (bounded enum key mapping) -- ======================================================================== data ButtonState = Neutral | Focus | Active | Hover | Disabled deriving (Show, Eq, Ord, Bounded, Enum, Generic) instance ToJSON ButtonState instance ToSchema ButtonState instance ToJSONKey ButtonState where toJSONKey = toJSONKeyText (T.pack . show) instance Arbitrary ButtonState where arbitrary = arbitraryBoundedEnum type ImageUrl = T.Text newtype ButtonImages = ButtonImages { getButtonImages :: Map ButtonState ImageUrl } deriving (Show, Generic) instance ToJSON ButtonImages where toJSON = toJSON . getButtonImages instance ToSchema ButtonImages where declareNamedSchema = genericDeclareNamedSchemaNewtype defaultSchemaOptions declareSchemaBoundedEnumKeyMapping invalidButtonImagesToJSON :: ButtonImages -> Value invalidButtonImagesToJSON = genericToJSON defaultOptions instance Arbitrary ButtonImages where arbitrary = ButtonImages <$> arbitrary -- Arbitrary instances for common types instance (Eq k, Hashable k, Arbitrary k, Arbitrary v) => Arbitrary (HashMap k v) where arbitrary = HashMap.fromList <$> arbitrary instance (Eq a, Hashable a, Arbitrary a) => Arbitrary (HashSet a) where arbitrary = HashSet.fromList <$> arbitrary instance Arbitrary T.Text where arbitrary = T.pack <$> arbitrary instance Arbitrary TL.Text where arbitrary = TL.pack <$> arbitrary instance Arbitrary Day where arbitrary = liftA3 fromGregorian (fmap ((+ 1) . abs) arbitrary) arbitrary arbitrary instance Arbitrary LocalTime where arbitrary = LocalTime <$> arbitrary <*> liftA3 TimeOfDay (choose (0, 23)) (choose (0, 59)) (fromInteger <$> choose (0, 60)) instance Eq ZonedTime where ZonedTime t (TimeZone x _ _) == ZonedTime t' (TimeZone y _ _) = t == t' && x == y instance Arbitrary ZonedTime where arbitrary = ZonedTime <$> arbitrary <*> liftA3 TimeZone arbitrary arbitrary (vectorOf 3 (elements ['A'..'Z'])) instance Arbitrary UTCTime where arbitrary = UTCTime <$> arbitrary <*> fmap fromInteger (choose (0, 86400))