openapi3-3.2.5: test/Data/OpenApi/Schema/ValidationSpec.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Data.OpenApi.Schema.ValidationSpec where
import Control.Applicative
import Control.Lens ((&), (.~), (?~))
import Data.Aeson
#if MIN_VERSION_aeson(2,0,0)
import qualified Data.Aeson.Key as Key
import qualified Data.Aeson.KeyMap as KeyMap
#endif
import Data.Aeson.Types
import Data.Hashable (Hashable)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import "unordered-containers" Data.HashSet (HashSet)
import qualified "unordered-containers" Data.HashSet as HashSet
import Data.Int
import Data.IntMap (IntMap)
import Data.List.NonEmpty.Compat (NonEmpty (..), nonEmpty)
import Data.Map (Map, fromList)
import Data.Monoid (mempty)
import Data.Proxy
import Data.Set (Set)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import Data.Time
import Data.Version (Version)
import Data.Word
import GHC.Generics
import Data.OpenApi
import Data.OpenApi.Declare
import Data.OpenApi.Aeson.Compat (stringToKey)
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck
import Test.QuickCheck.Instances ()
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 "(NonEmpty Bool)" $ shouldValidate (Proxy :: Proxy (NonEmpty 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 "Object" $ shouldValidate (Proxy :: Proxy Object)
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 "Light TaggedObject" $ shouldValidate (Proxy :: Proxy LightTaggedObject)
prop "Light UntaggedValue" $ shouldValidate (Proxy :: Proxy LightUntaggedValue)
prop "ButtonImages" $ shouldValidate (Proxy :: Proxy ButtonImages)
prop "Version" $ shouldValidate (Proxy :: Proxy Version)
prop "FreeForm" $ shouldValidate (Proxy :: Proxy FreeForm)
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
[ stringToKey "personName" .= toJSON name
, stringToKey "personPhone" .= toJSON phone
, stringToKey "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 = genericDeclareNamedSchema defaultSchemaOptions { Data.OpenApi.sumEncoding = ObjectWithSingleField }
instance ToJSON Light where
toJSON = genericToJSON defaultOptions { Data.Aeson.Types.sumEncoding = ObjectWithSingleField }
instance Arbitrary Light where
arbitrary = oneof
[ return NoLight
, LightFreq <$> arbitrary
, LightColor <$> arbitrary
]
invalidLightToJSON :: Light -> Value
invalidLightToJSON = genericToJSON defaultOptions
-- Check all SumEncoding flavors.
newtype LightTaggedObject = LightTaggedObject Light
deriving (Show)
instance ToJSON LightTaggedObject where
toJSON (LightTaggedObject light) = genericToJSON defaultOptions { Data.Aeson.Types.sumEncoding = defaultTaggedObject } light
instance ToSchema LightTaggedObject where
declareNamedSchema _ =
genericDeclareNamedSchema defaultSchemaOptions { Data.OpenApi.sumEncoding = defaultTaggedObject } (Proxy :: Proxy Light)
instance Arbitrary LightTaggedObject where
arbitrary = LightTaggedObject <$> arbitrary
newtype LightUntaggedValue = LightUntaggedValue Light
deriving (Show)
instance ToJSON LightUntaggedValue where
toJSON (LightUntaggedValue light) = genericToJSON defaultOptions { Data.Aeson.Types.sumEncoding = UntaggedValue } light
instance ToSchema LightUntaggedValue where
declareNamedSchema _ =
genericDeclareNamedSchema defaultSchemaOptions { Data.OpenApi.sumEncoding = UntaggedValue } (Proxy :: Proxy Light)
instance Arbitrary LightUntaggedValue where
arbitrary = LightUntaggedValue <$> arbitrary
-- ========================================================================
-- 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
-- ========================================================================
-- FreeForm (wraps a raw JSON Value)
-- ========================================================================
data FreeForm = FreeForm { jsonContent :: Map T.Text Value }
deriving (Show, Generic)
instance ToJSON FreeForm where
toJSON = toJSON . jsonContent
instance ToSchema FreeForm where
declareNamedSchema _ = pure $ NamedSchema (Just $ T.pack "FreeForm") $ mempty
& type_ ?~ OpenApiObject
& additionalProperties ?~ AdditionalPropertiesAllowed True
instance Arbitrary FreeForm where
arbitrary = (FreeForm . fromList) <$> genObj
where
genObj = listOf $ do
k <- arbitrary
v <- oneof [ String <$> arbitrary, Number <$> arbitrary, Bool <$> arbitrary, pure Null ]
pure (k, v)
instance Eq ZonedTime where
ZonedTime t (TimeZone x _ _) == ZonedTime t' (TimeZone y _ _) = t == t' && x == y
-- ========================================================================
-- Arbitrary instance for Data.Aeson.Value
-- ========================================================================
-- These instances were introduces in aeson upstream in 2.0.3
#if !MIN_VERSION_aeson(2,0,3)
instance Arbitrary Value where
-- Weights are almost random
-- Uniform oneof tends not to build complex objects cause of recursive call.
arbitrary = resize 4 $ frequency
[ (3, Object <$> arbitrary)
, (3, Array <$> arbitrary)
, (3, String <$> arbitrary)
, (3, Number <$> arbitrary)
, (3, Bool <$> arbitrary)
, (1, return Null) ]
#if MIN_VERSION_aeson(2,0,0)
instance Arbitrary v => Arbitrary (KeyMap.KeyMap v) where
arbitrary = KeyMap.fromList <$> arbitrary
instance Arbitrary Key.Key where
arbitrary = Key.fromText <$> arbitrary
#endif
#endif