{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE TemplateHaskell #-} -- | Defines an api spec (to be built after the static checking of the AST) and helper methods over -- it. module TypeCheck.ApiSpec where import Control.Monad (liftM) import Data.DeriveTH import qualified Data.Map as M import qualified Data.Set as S import Test.QuickCheck -- | Identifier of an enum, struct, field... type Id = String -- | The route of a resource. type Route = String -- | An enum value. type EnumValue = String -- | An enum is a list of values. type EnumInfo = [EnumValue] -- | A field modifier. data Modifier = Hidden -- ^ The field will not be returned when read | Immutable -- ^ The field cannot be modified | Required -- ^ The field can't be absent | PrimaryKey -- ^ The field is the primary key (thus 'Unique' as well) | Unique -- ^ The field can't have repeated values throughout the collection | UserLogin -- ^ The field value will contain the user's login deriving (Eq, Ord, Show) derive makeArbitrary ''Modifier -- | A field has a type, an identifier and a set of modifiers. newtype FieldInfo = FI (Id, Type, S.Set Modifier) deriving (Show, Eq, Ord) -- | A struct is a list of fields. type StructInfo = [FieldInfo] -- | A resource has a route and a write mode. type ResourceInfo = (Route, Writable) -- | A type can be a primitive one (int, long, double, bool...), an enum, a struct, or a list of -- another type. data Type = TInt | TLong | TFloat | TDouble | TBool | TString | TEnum Id | TStruct Id | TList Type deriving (Eq, Ord, Show) instance (CoArbitrary Type) where coarbitrary = coarbitraryShow -- | Map from enum id to its info. type Enums = M.Map Id EnumInfo -- | Map from struct id to its info. type Structs = [(Id, StructInfo)] -- | Map from resource id to the route and the mode. type Resources = M.Map Id ResourceInfo -- | Writable is a boolean type. type Writable = Bool -- | The spec of an api is a set of enums and structs, along with the resources. data ApiSpec = AS { name :: String -- ^ Name of the service , version :: String -- ^ Version of the service , requiresAuth :: Bool -- ^ Whether it should support authentication or not , enums :: Enums -- ^ Information about the user defined enums , structs :: Structs -- ^ Information about the user defined structs , resources :: Resources -- ^ Information about the resources defined } derive makeShow ''ApiSpec -- | Gets the primary key of a struct if it was specified. getPrimaryKey :: StructInfo -- ^ The info of the struct -> Maybe Id -- ^ The result (Nothing if there was no PK defined) getPrimaryKey structInfo = case filter hasPkModifier structInfo of [] -> Nothing [FI (x, _, _)] -> Just x _ -> error "A struct should have at most one specified primary key." where hasPkModifier (FI (_, _, modifiers)) = PrimaryKey `S.member` modifiers -- | Finds out if a fieldinfo is relative to a struct isStructField :: FieldInfo -> Bool isStructField (FI (_, t, _)) = isStruct t -- | Find outs if a type is a struct isStruct :: Type -> Bool isStruct (TStruct _) = True isStruct (TList t) = isStruct t isStruct _ = False -- | Get the name of a struct strName :: Type -> String strName (TStruct t) = t strName (TList t) = strName t strName other = error $ "strName of a non struct type (" ++ show other ++ ")" -- Testing -- | Generates a non-empty arbitrary 'String'. nonEmptyString :: Gen String nonEmptyString = listOf1 arbitrary -- | Generates an arbitrary 'FieldInfo', making sure that the ids used for enums and structs are valid. generateRandomFieldInfo :: [Id] -> [Id] -> Gen FieldInfo generateRandomFieldInfo enumIds structIds = do id <- nonEmptyString t <- generateRandomType enumIds structIds modifiers <- listOf arbitrary return $ FI (id, t, S.fromList modifiers) where generateRandomType :: [Id] -> [Id] -> Gen Type generateRandomType enumIds structIds = do t <- arbitrary case t of (TStruct _) -> processStruct (TEnum _) -> processEnum (TList t') | needsEnumId t' -> if null enumIds then generateRandomType enumIds structIds else do newId <- elements enumIds return $ TList $ setNewId newId t' | needsStructId t' -> if null structIds then generateRandomType enumIds structIds else do newId <- elements structIds return $ TList $ setNewId newId t' | otherwise -> return $ TList t' other -> return other where processEnum = if null enumIds then generateRandomType enumIds structIds else liftM TEnum $ elements enumIds processStruct = if null structIds then generateRandomType enumIds structIds else liftM TStruct $ elements structIds needsEnumId (TEnum _) = True needsEnumId (TList t) = needsEnumId t needsEnumId _ = False needsStructId (TStruct _) = True needsStructId (TList t) = needsStructId t needsStructId _ = False setNewId newId' (TEnum _) = TEnum newId' setNewId newId' (TStruct _) = TStruct newId' setNewId newId' (TList t) = TList $ setNewId newId' t setNewId _ t = t instance (Arbitrary ApiSpec) where arbitrary = do name' <- nonEmptyString version' <- nonEmptyString enumIds <- listOf nonEmptyString enums' <- mapM createEnum enumIds structIds <- listOf nonEmptyString structs' <- mapM (createStruct enumIds structIds) structIds resources' <- mapM (createResource . fst) structs' return AS { name = name' , version = version' , requiresAuth = False , enums = M.fromList enums' , structs = structs' , resources = M.fromList resources' } where createEnum :: Id -> Gen (Id, EnumInfo) createEnum id = do values <- listOf1 nonEmptyString return (id, values) createStruct :: [Id] -> [Id] -> Id -> Gen (Id, StructInfo) createStruct enumIds structIds thisStructId = do rawFields <- listOf1 $ generateRandomFieldInfo enumIds structIds shouldHavePk <- arbitrary let fields = filterPrimaryKey shouldHavePk rawFields return (thisStructId, fields) where filterPrimaryKey _ [] = [] filterPrimaryKey True (FI (id, t, mods):fs) | PrimaryKey `S.member` mods = FI (id, t, PrimaryKey `S.delete` mods) : filterPrimaryKey False fs filterPrimaryKey True (FI (id, t, mods):fs) = FI (id, t, mods) : filterPrimaryKey True fs filterPrimaryKey False (FI (id, t, mods):fs) = FI (id, t, PrimaryKey `S.delete` mods) : filterPrimaryKey False fs createResource :: Id -> Gen (Id, ResourceInfo) createResource id = do route <- nonEmptyString writable <- arbitrary return (id, (route, writable)) instance (Arbitrary Type) where arbitrary = do t <- elements [TInt, TLong, TFloat, TDouble, TBool, TString, TEnum "", TStruct "", TList TInt] case t of TEnum _ -> liftM TEnum nonEmptyString TStruct _ -> liftM TStruct nonEmptyString TList _ -> liftM TList arbitrary other -> return other