App spec validation (#459)

This commit is contained in:
Martin Šošić 2022-03-17 12:36:36 +01:00 committed by GitHub
parent 244e66cec3
commit 2a16bfd3cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 318 additions and 46 deletions

View File

@ -11,3 +11,5 @@
- ignore: {name: Use newtype instead of data} # We can decide this on our own. - ignore: {name: Use newtype instead of data} # We can decide this on our own.
- ignore: {name: Use $>} # I find it makes code harder to read if enforced. - ignore: {name: Use $>} # I find it makes code harder to read if enforced.
- ignore: {name: Use list comprehension} # We can decide this on our own. - ignore: {name: Use list comprehension} # We can decide this on our own.
- ignore: {name: Use list comprehension} # We can decide this on our own.
- ignore: {name: Use ++} # I sometimes prefer concat over ++ due to the nicer formatting / extensibility.

View File

@ -2,6 +2,7 @@ module Main where
import Control.Concurrent (threadDelay) import Control.Concurrent (threadDelay)
import qualified Control.Concurrent.Async as Async import qualified Control.Concurrent.Async as Async
import qualified Control.Exception as E
import Control.Monad (void) import Control.Monad (void)
import Data.Char (isSpace) import Data.Char (isSpace)
import Data.Version (showVersion) import Data.Version (showVersion)
@ -21,10 +22,11 @@ import Wasp.Cli.Command.Info (info)
import Wasp.Cli.Command.Start (start) import Wasp.Cli.Command.Start (start)
import qualified Wasp.Cli.Command.Telemetry as Telemetry import qualified Wasp.Cli.Command.Telemetry as Telemetry
import Wasp.Cli.Terminal (title) import Wasp.Cli.Terminal (title)
import Wasp.Util (indent)
import qualified Wasp.Util.Terminal as Term import qualified Wasp.Util.Terminal as Term
main :: IO () main :: IO ()
main = do main = (`E.catch` handleInternalErrors) $ do
args <- getArgs args <- getArgs
let commandCall = case args of let commandCall = case args of
["new", projectName] -> Command.Call.New projectName ["new", projectName] -> Command.Call.New projectName
@ -68,6 +70,9 @@ main = do
let microsecondsInASecond = 1000000 let microsecondsInASecond = 1000000
in threadDelay . (* microsecondsInASecond) in threadDelay . (* microsecondsInASecond)
handleInternalErrors :: E.ErrorCall -> IO ()
handleInternalErrors e = putStrLn $ "\nInternal Wasp error (bug in compiler):\n" ++ indent 2 (show e)
printUsage :: IO () printUsage :: IO ()
printUsage = printUsage =
putStrLn $ putStrLn $

View File

@ -7,21 +7,19 @@ module Wasp.AppSpec
takeDecls, takeDecls,
Ref, Ref,
refName, refName,
getApp,
getActions, getActions,
getQueries, getQueries,
getEntities, getEntities,
getPages, getPages,
getRoutes, getRoutes,
isAuthEnabled, resolveRef,
) )
where where
import Data.Maybe (isJust) import Data.List (find)
import Data.Maybe (fromMaybe)
import StrongPath (Abs, Dir, File', Path') import StrongPath (Abs, Dir, File', Path')
import Wasp.AppSpec.Action (Action) import Wasp.AppSpec.Action (Action)
import Wasp.AppSpec.App (App)
import qualified Wasp.AppSpec.App as App
import Wasp.AppSpec.Core.Decl (Decl, IsDecl, takeDecls) import Wasp.AppSpec.Core.Decl (Decl, IsDecl, takeDecls)
import Wasp.AppSpec.Core.Ref (Ref, refName) import Wasp.AppSpec.Core.Ref (Ref, refName)
import Wasp.AppSpec.Entity (Entity) import Wasp.AppSpec.Entity (Entity)
@ -60,36 +58,26 @@ data AppSpec = AppSpec
getDecls :: IsDecl a => AppSpec -> [(String, a)] getDecls :: IsDecl a => AppSpec -> [(String, a)]
getDecls = takeDecls . decls getDecls = takeDecls . decls
-- TODO: This will fail with an error if there is no `app` declaration (because of `head`)!
-- However, returning a Maybe here would be PITA later in the code.
-- It would be cool instead if we had an extra step that somehow ensures that app exists and
-- throws nice error if it doesn't. Some step that validated AppSpec. Maybe we could
-- have a function that returns `Validated AppSpec` -> so basically smart constructor,
-- validates AppSpec and returns it wrapped with `Validated`,
-- I created a github issue for it: https://github.com/wasp-lang/wasp/issues/425 .
getApp :: AppSpec -> (String, App)
getApp spec = case takeDecls @App (decls spec) of
[app] -> app
apps ->
error $
"Compiler error: expected exactly 1 'app' declaration in your wasp code, but you have "
++ show (length apps)
++ "!"
getQueries :: AppSpec -> [(String, Query)] getQueries :: AppSpec -> [(String, Query)]
getQueries spec = takeDecls @Query (decls spec) getQueries = getDecls @Query
getActions :: AppSpec -> [(String, Action)] getActions :: AppSpec -> [(String, Action)]
getActions spec = takeDecls @Action (decls spec) getActions = getDecls @Action
getEntities :: AppSpec -> [(String, Entity)] getEntities :: AppSpec -> [(String, Entity)]
getEntities spec = takeDecls @Entity (decls spec) getEntities = getDecls @Entity
getPages :: AppSpec -> [(String, Page)] getPages :: AppSpec -> [(String, Page)]
getPages spec = takeDecls @Page (decls spec) getPages = getDecls @Page
getRoutes :: AppSpec -> [(String, Route)] getRoutes :: AppSpec -> [(String, Route)]
getRoutes spec = takeDecls @Route (decls spec) getRoutes = getDecls @Route
isAuthEnabled :: AppSpec -> Bool resolveRef :: (IsDecl d) => AppSpec -> Ref d -> (String, d)
isAuthEnabled spec = isJust (App.auth $ snd $ getApp spec) resolveRef spec ref =
fromMaybe
( error $
"Failed to resolve declaration reference: " ++ refName ref ++ "."
++ " This should never happen, as Analyzer should ensure all references in AppSpec are valid."
)
$ find ((== refName ref) . fst) $ getDecls spec

View File

@ -0,0 +1,99 @@
{-# LANGUAGE TypeApplications #-}
module Wasp.AppSpec.Valid
( validateAppSpec,
ValidationError (..),
getApp,
isAuthEnabled,
)
where
import Data.List (find)
import Data.Maybe (isJust)
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import Wasp.AppSpec.App (App)
import qualified Wasp.AppSpec.App as App
import qualified Wasp.AppSpec.App.Auth as Auth
import Wasp.AppSpec.Core.Decl (takeDecls)
import qualified Wasp.AppSpec.Entity as Entity
import qualified Wasp.AppSpec.Entity.Field as Entity.Field
import qualified Wasp.AppSpec.Page as Page
data ValidationError = GenericValidationError String
deriving (Show, Eq)
validateAppSpec :: AppSpec -> [ValidationError]
validateAppSpec spec =
case validateExactlyOneAppExists spec of
Just err -> [err]
Nothing ->
-- NOTE: We check these only if App exists because they all rely on it existing.
concat
[ validateAppAuthIsSetIfAnyPageRequiresAuth spec,
validateAuthUserEntityHasCorrectFieldsIfEmailAndPasswordAuthIsUsed spec
]
validateExactlyOneAppExists :: AppSpec -> Maybe ValidationError
validateExactlyOneAppExists spec =
case AS.takeDecls @App (AS.decls spec) of
[] -> Just $ GenericValidationError "You are missing an 'app' declaration in your Wasp app."
[_] -> Nothing
apps ->
Just $
GenericValidationError $
"You have more than one 'app' declaration in your Wasp app. You have " ++ show (length apps) ++ "."
validateAppAuthIsSetIfAnyPageRequiresAuth :: AppSpec -> [ValidationError]
validateAppAuthIsSetIfAnyPageRequiresAuth spec =
if anyPageRequiresAuth && not (isAuthEnabled spec)
then
[ GenericValidationError
"Expected app.auth to be defined since there are Pages with authRequired set to true."
]
else []
where
anyPageRequiresAuth = any ((== Just True) . Page.authRequired) (snd <$> AS.getPages spec)
validateAuthUserEntityHasCorrectFieldsIfEmailAndPasswordAuthIsUsed :: AppSpec -> [ValidationError]
validateAuthUserEntityHasCorrectFieldsIfEmailAndPasswordAuthIsUsed spec = case App.auth (snd $ getApp spec) of
Nothing -> []
Just auth ->
if Auth.EmailAndPassword `notElem` Auth.methods auth
then []
else
let userEntity = snd $ AS.resolveRef spec (Auth.userEntity auth)
userEntityFields = Entity.getFields userEntity
maybeEmailField = find ((== "email") . Entity.Field.fieldName) userEntityFields
maybePasswordField = find ((== "password") . Entity.Field.fieldName) userEntityFields
in concat
[ case maybeEmailField of
Just emailField
| Entity.Field.fieldType emailField == Entity.Field.FieldTypeScalar Entity.Field.String -> []
_ ->
[ GenericValidationError
"Expected an Entity referenced by app.auth.userEntity to have field 'email' of type 'string'."
],
case maybePasswordField of
Just passwordField
| Entity.Field.fieldType passwordField == Entity.Field.FieldTypeScalar Entity.Field.String -> []
_ ->
[ GenericValidationError
"Expected an Entity referenced by app.auth.userEntity to have field 'password' of type 'string'."
]
]
-- | This function assumes that @AppSpec@ it operates on was validated beforehand (with @validateAppSpec@ function).
-- TODO: It would be great if we could ensure this at type level, but we decided that was too much work for now.
-- Check https://github.com/wasp-lang/wasp/pull/455 for considerations on this and analysis of different approaches.
getApp :: AppSpec -> (String, App)
getApp spec = case takeDecls @App (AS.decls spec) of
[app] -> app
apps ->
error $
("Expected exactly 1 'app' declaration in your wasp code, but you have " ++ show (length apps) ++ ".")
++ " This should never happen as it should have been caught during validation of AppSpec."
-- | This function assumes that @AppSpec@ it operates on was validated beforehand (with @validateAppSpec@ function).
isAuthEnabled :: AppSpec -> Bool
isAuthEnabled spec = isJust (App.auth $ snd $ getApp spec)

View File

@ -18,6 +18,7 @@ import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Db as AS.Db import qualified Wasp.AppSpec.App.Db as AS.Db
import qualified Wasp.AppSpec.Entity as AS.Entity import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.AppSpec.Valid (getApp)
import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.DbGenerator.Common import Wasp.Generator.DbGenerator.Common
( dbMigrationsDirInDbRootDir, ( dbMigrationsDirInDbRootDir,
@ -59,7 +60,7 @@ genPrismaSchema spec = do
where where
dstPath = dbSchemaFileInProjectRootDir dstPath = dbSchemaFileInProjectRootDir
tmplSrcPath = dbTemplatesDirInTemplatesDir </> dbSchemaFileInDbTemplatesDir tmplSrcPath = dbTemplatesDirInTemplatesDir </> dbSchemaFileInDbTemplatesDir
dbSystem = fromMaybe AS.Db.SQLite (AS.Db.system =<< AS.App.db (snd $ AS.getApp spec)) dbSystem = fromMaybe AS.Db.SQLite (AS.Db.system =<< AS.App.db (snd $ getApp spec))
entityToPslModelSchema :: (String, AS.Entity.Entity) -> String entityToPslModelSchema :: (String, AS.Entity.Entity) -> String
entityToPslModelSchema (entityName, entity) = entityToPslModelSchema (entityName, entity) =

View File

@ -23,9 +23,9 @@ import Data.Maybe (fromMaybe)
import qualified Data.Maybe as Maybe import qualified Data.Maybe as Maybe
import GHC.Generics import GHC.Generics
import Wasp.AppSpec (AppSpec) import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Dependency as D import qualified Wasp.AppSpec.App.Dependency as D
import qualified Wasp.AppSpec.Valid as ASV
import Wasp.Generator.Monad (Generator, GeneratorError (..), logAndThrowGeneratorError) import Wasp.Generator.Monad (Generator, GeneratorError (..), logAndThrowGeneratorError)
data NpmDepsForFullStack = NpmDepsForFullStack data NpmDepsForFullStack = NpmDepsForFullStack
@ -108,7 +108,7 @@ buildNpmDepsForFullStack spec forServer forWebApp =
getUserNpmDepsForPackage :: AppSpec -> NpmDepsForUser getUserNpmDepsForPackage :: AppSpec -> NpmDepsForUser
getUserNpmDepsForPackage spec = getUserNpmDepsForPackage spec =
NpmDepsForUser NpmDepsForUser
{ userDependencies = fromMaybe [] $ AS.App.dependencies $ snd $ AS.getApp spec, { userDependencies = fromMaybe [] $ AS.App.dependencies $ snd $ ASV.getApp spec,
-- Should we allow user devDependencies? https://github.com/wasp-lang/wasp/issues/456 -- Should we allow user devDependencies? https://github.com/wasp-lang/wasp/issues/456
userDevDependencies = [] userDevDependencies = []
} }

View File

@ -21,6 +21,7 @@ import qualified Wasp.AppSpec.App.Auth as AS.App.Auth
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import qualified Wasp.AppSpec.App.Server as AS.App.Server import qualified Wasp.AppSpec.App.Server as AS.App.Server
import qualified Wasp.AppSpec.Entity as AS.Entity import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.AppSpec.Valid (getApp, isAuthEnabled)
import Wasp.Generator.Common (nodeVersionAsText, prismaVersion) import Wasp.Generator.Common (nodeVersionAsText, prismaVersion)
import Wasp.Generator.ExternalCodeGenerator (generateExternalCodeDir) import Wasp.Generator.ExternalCodeGenerator (generateExternalCodeDir)
import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir) import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
@ -158,7 +159,7 @@ genSrcDir spec =
genDbClient :: AppSpec -> Generator FileDraft genDbClient :: AppSpec -> Generator FileDraft
genDbClient spec = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) genDbClient spec = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
maybeAuth = AS.App.auth $ snd $ AS.getApp spec maybeAuth = AS.App.auth $ snd $ getApp spec
dbClientRelToSrcP = [relfile|dbClient.js|] dbClientRelToSrcP = [relfile|dbClient.js|]
tmplFile = C.asTmplFile $ [reldir|src|] </> dbClientRelToSrcP tmplFile = C.asTmplFile $ [reldir|src|] </> dbClientRelToSrcP
@ -187,7 +188,7 @@ genServerJs spec =
] ]
) )
where where
maybeSetupJsFunction = AS.App.Server.setupFn =<< AS.App.server (snd $ AS.getApp spec) maybeSetupJsFunction = AS.App.Server.setupFn =<< AS.App.server (snd $ getApp spec)
maybeSetupJsFnImportDetails = getJsImportDetailsForExtFnImport relPosixPathFromSrcDirToExtSrcDir <$> maybeSetupJsFunction maybeSetupJsFnImportDetails = getJsImportDetailsForExtFnImport relPosixPathFromSrcDirToExtSrcDir <$> maybeSetupJsFunction
(maybeSetupJsFnImportIdentifier, maybeSetupJsFnImportStmt) = (maybeSetupJsFnImportIdentifier, maybeSetupJsFnImportStmt) =
(fst <$> maybeSetupJsFnImportDetails, snd <$> maybeSetupJsFnImportDetails) (fst <$> maybeSetupJsFnImportDetails, snd <$> maybeSetupJsFnImportDetails)
@ -207,7 +208,7 @@ genRoutesDir spec =
( Just $ ( Just $
object object
[ "operationsRouteInRootRouter" .= (operationsRouteInRootRouter :: String), [ "operationsRouteInRootRouter" .= (operationsRouteInRootRouter :: String),
"isAuthEnabled" .= (AS.isAuthEnabled spec :: Bool) "isAuthEnabled" .= (isAuthEnabled spec :: Bool)
] ]
) )
] ]

View File

@ -9,6 +9,7 @@ import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.Auth import qualified Wasp.AppSpec.App.Auth as AS.Auth
import Wasp.AppSpec.Valid (getApp)
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator) import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.ServerGenerator.Common as C import qualified Wasp.Generator.ServerGenerator.Common as C
@ -28,7 +29,7 @@ genAuth spec = case maybeAuth of
] ]
Nothing -> return [] Nothing -> return []
where where
maybeAuth = AS.App.auth $ snd $ AS.getApp spec maybeAuth = AS.App.auth $ snd $ getApp spec
-- | Generates core/auth file which contains auth middleware and createUser() function. -- | Generates core/auth file which contains auth middleware and createUser() function.
genCoreAuth :: AS.Auth.Auth -> Generator FileDraft genCoreAuth :: AS.Auth.Auth -> Generator FileDraft
@ -48,6 +49,12 @@ genCoreAuth auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmpl
genAuthMiddleware :: AS.Auth.Auth -> Generator FileDraft genAuthMiddleware :: AS.Auth.Auth -> Generator FileDraft
genAuthMiddleware auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) genAuthMiddleware auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
-- TODO(martin): In prismaMiddleware.js, we assume that 'email' and 'password' are defined in user entity.
-- This was promised to us by AppSpec, which has validation checks for this.
-- Names of these fields are currently hardcoded, and we are not in any way relyin on AppSpec directly here.
-- In the future we might want to figure out a way to better encode these assumptions, either by
-- reusing the names for 'email' and 'password' fields by importing them from AppSpec, or smth similar
-- in that direction.
authMiddlewareRelToSrc = [relfile|core/auth/prismaMiddleware.js|] authMiddlewareRelToSrc = [relfile|core/auth/prismaMiddleware.js|]
tmplFile = C.asTmplFile $ [reldir|src|] </> authMiddlewareRelToSrc tmplFile = C.asTmplFile $ [reldir|src|] </> authMiddlewareRelToSrc
dstFile = C.serverSrcDirInServerRootDir </> C.asServerSrcFile authMiddlewareRelToSrc dstFile = C.serverSrcDirInServerRootDir </> C.asServerSrcFile authMiddlewareRelToSrc

View File

@ -8,7 +8,7 @@ import Data.Aeson (object, (.=))
import StrongPath (File', Path', Rel, relfile, (</>)) import StrongPath (File', Path', Rel, relfile, (</>))
import qualified StrongPath as SP import qualified StrongPath as SP
import Wasp.AppSpec (AppSpec) import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS import Wasp.AppSpec.Valid (isAuthEnabled)
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator) import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.ServerGenerator.Common as C import qualified Wasp.Generator.ServerGenerator.Common as C
@ -20,7 +20,7 @@ genConfigFile spec = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tm
dstFile = C.serverSrcDirInServerRootDir </> configFileInSrcDir dstFile = C.serverSrcDirInServerRootDir </> configFileInSrcDir
tmplData = tmplData =
object object
[ "isAuthEnabled" .= (AS.isAuthEnabled spec :: Bool) [ "isAuthEnabled" .= (isAuthEnabled spec :: Bool)
] ]
configFileInSrcDir :: Path' (Rel C.ServerSrcDir) File' configFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'

View File

@ -11,13 +11,14 @@ import qualified Data.Aeson as Aeson
import Data.Maybe (fromJust, fromMaybe, isJust) import Data.Maybe (fromJust, fromMaybe, isJust)
import StrongPath (Dir, File', Path, Path', Posix, Rel, reldir, reldirP, relfile, (</>)) import StrongPath (Dir, File', Path, Path', Posix, Rel, reldir, reldirP, relfile, (</>))
import qualified StrongPath as SP import qualified StrongPath as SP
import Wasp.AppSpec (AppSpec, getApp) import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.Action as AS.Action import qualified Wasp.AppSpec.Action as AS.Action
import qualified Wasp.AppSpec.App as AS.App import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.Auth import qualified Wasp.AppSpec.App.Auth as AS.Auth
import qualified Wasp.AppSpec.Operation as AS.Operation import qualified Wasp.AppSpec.Operation as AS.Operation
import qualified Wasp.AppSpec.Query as AS.Query import qualified Wasp.AppSpec.Query as AS.Query
import Wasp.AppSpec.Valid (getApp, isAuthEnabled)
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator, GeneratorError (GenericGeneratorError), logAndThrowGeneratorError) import Wasp.Generator.Monad (Generator, GeneratorError (GenericGeneratorError), logAndThrowGeneratorError)
import qualified Wasp.Generator.ServerGenerator.Common as C import qualified Wasp.Generator.ServerGenerator.Common as C
@ -112,7 +113,7 @@ genOperationsRouter spec
"isUsingAuth" .= isAuthEnabledForOperation operation "isUsingAuth" .= isAuthEnabledForOperation operation
] ]
isAuthEnabledGlobally = AS.isAuthEnabled spec isAuthEnabledGlobally = isAuthEnabled spec
isAuthEnabledForOperation operation = fromMaybe isAuthEnabledGlobally (AS.Operation.getAuth operation) isAuthEnabledForOperation operation = fromMaybe isAuthEnabledGlobally (AS.Operation.getAuth operation)
isAuthSpecifiedForOperation operation = isJust $ AS.Operation.getAuth operation isAuthSpecifiedForOperation operation = isJust $ AS.Operation.getAuth operation

View File

@ -14,10 +14,11 @@ import StrongPath
relfile, relfile,
(</>), (</>),
) )
import Wasp.AppSpec (AppSpec, getApp) import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import Wasp.AppSpec.Valid (getApp)
import Wasp.Generator.ExternalCodeGenerator (generateExternalCodeDir) import Wasp.Generator.ExternalCodeGenerator (generateExternalCodeDir)
import Wasp.Generator.FileDraft import Wasp.Generator.FileDraft
import Wasp.Generator.Monad (Generator) import Wasp.Generator.Monad (Generator)

View File

@ -8,9 +8,9 @@ import Data.Aeson.Types (Pair)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import StrongPath (File', Path', Rel', reldir, relfile, (</>)) import StrongPath (File', Path', Rel', reldir, relfile, (</>))
import Wasp.AppSpec (AppSpec) import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.Auth import qualified Wasp.AppSpec.App.Auth as AS.Auth
import Wasp.AppSpec.Valid (getApp)
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator) import Wasp.Generator.Monad (Generator)
import Wasp.Generator.WebAppGenerator.Common as C import Wasp.Generator.WebAppGenerator.Common as C
@ -30,7 +30,7 @@ genAuth spec =
<++> genAuthForms auth <++> genAuthForms auth
Nothing -> return [] Nothing -> return []
where where
maybeAuth = AS.App.auth $ snd $ AS.getApp spec maybeAuth = AS.App.auth $ snd $ getApp spec
-- | Generates file with signup function to be used by Wasp developer. -- | Generates file with signup function to be used by Wasp developer.
genSignup :: Generator FileDraft genSignup :: Generator FileDraft

View File

@ -16,6 +16,7 @@ import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.ExtImport as AS.ExtImport import qualified Wasp.AppSpec.ExtImport as AS.ExtImport
import qualified Wasp.AppSpec.Page as AS.Page import qualified Wasp.AppSpec.Page as AS.Page
import qualified Wasp.AppSpec.Route as AS.Route import qualified Wasp.AppSpec.Route as AS.Route
import Wasp.AppSpec.Valid (isAuthEnabled)
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator) import Wasp.Generator.Monad (Generator)
import Wasp.Generator.WebAppGenerator.Common (asTmplFile, asWebAppSrcFile) import Wasp.Generator.WebAppGenerator.Common (asTmplFile, asWebAppSrcFile)
@ -77,7 +78,7 @@ createRouterTemplateData spec =
RouterTemplateData RouterTemplateData
{ _routes = routes, { _routes = routes,
_pagesToImport = pages, _pagesToImport = pages,
_isAuthEnabled = AS.isAuthEnabled spec _isAuthEnabled = isAuthEnabled spec
} }
where where
routes = map (createRouteTemplateData spec) $ AS.getRoutes spec routes = map (createRouteTemplateData spec) $ AS.getRoutes spec

View File

@ -13,6 +13,7 @@ import System.Directory (doesDirectoryExist, doesFileExist)
import qualified Wasp.Analyzer as Analyzer import qualified Wasp.Analyzer as Analyzer
import Wasp.Analyzer.AnalyzeError (getErrorMessageAndCtx) import Wasp.Analyzer.AnalyzeError (getErrorMessageAndCtx)
import qualified Wasp.AppSpec as AS import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.Valid as ASV
import Wasp.Common (DbMigrationsDir, WaspProjectDir, dbMigrationsDirInWaspProjectDir) import Wasp.Common (DbMigrationsDir, WaspProjectDir, dbMigrationsDirInWaspProjectDir)
import Wasp.CompileOptions (CompileOptions, sendMessage) import Wasp.CompileOptions (CompileOptions, sendMessage)
import qualified Wasp.CompileOptions as CompileOptions import qualified Wasp.CompileOptions as CompileOptions
@ -60,8 +61,12 @@ compile waspDir outDir options = do
AS.dotEnvFile = maybeDotEnvFile, AS.dotEnvFile = maybeDotEnvFile,
AS.isBuild = CompileOptions.isBuild options AS.isBuild = CompileOptions.isBuild options
} }
(generatorWarnings, generatorErrors) <- Generator.writeWebAppCode appSpec outDir (sendMessage options) case ASV.validateAppSpec appSpec of
return (map show generatorWarnings, map show generatorErrors) [] -> do
(generatorWarnings, generatorErrors) <- Generator.writeWebAppCode appSpec outDir (sendMessage options)
return (map show generatorWarnings, map show generatorErrors)
validationErrors -> do
return ([], map show validationErrors)
findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs File')) findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs File'))
findWaspFile waspDir = do findWaspFile waspDir = do

View File

@ -0,0 +1,159 @@
{-# LANGUAGE TypeApplications #-}
module AppSpec.ValidTest where
import Data.Maybe (fromJust)
import Fixtures (systemSPRoot)
import qualified StrongPath as SP
import Test.Tasty.Hspec
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.Auth
import qualified Wasp.AppSpec.Core.Decl as AS.Decl
import qualified Wasp.AppSpec.Core.Ref as AS.Core.Ref
import qualified Wasp.AppSpec.Entity as AS.Entity
import qualified Wasp.AppSpec.ExtImport as AS.ExtImport
import qualified Wasp.AppSpec.Page as AS.Page
import qualified Wasp.AppSpec.Valid as ASV
import qualified Wasp.Psl.Ast.Model as PslM
spec_AppSpecValid :: Spec
spec_AppSpecValid = do
describe "validateAppSpec" $ do
describe "should validate that AppSpec has exactly 1 'app' declaration." $ do
it "returns no error if there is exactly 1 'app' declaration." $ do
ASV.validateAppSpec (basicAppSpec {AS.decls = [basicAppDecl]}) `shouldBe` []
it "returns an error if there is no 'app' declaration." $ do
ASV.validateAppSpec (basicAppSpec {AS.decls = []})
`shouldBe` [ ASV.GenericValidationError
"You are missing an 'app' declaration in your Wasp app."
]
it "returns an error if there are 2 'app' declarations." $ do
ASV.validateAppSpec
( basicAppSpec
{ AS.decls =
[ AS.Decl.makeDecl "app1" basicApp,
AS.Decl.makeDecl "app2" basicApp
]
}
)
`shouldBe` [ ASV.GenericValidationError
"You have more than one 'app' declaration in your Wasp app. You have 2."
]
describe "auth-related validation" $ do
let userEntityName = "User"
let validUserEntity =
AS.Entity.makeEntity
( PslM.Body
[ PslM.ElementField $ makeBasicPslField "email" PslM.String,
PslM.ElementField $ makeBasicPslField "password" PslM.String
]
)
let validAppAuth =
AS.Auth.Auth
{ AS.Auth.userEntity = AS.Core.Ref.Ref userEntityName,
AS.Auth.methods = [AS.Auth.EmailAndPassword],
AS.Auth.onAuthFailedRedirectTo = "/",
AS.Auth.onAuthSucceededRedirectTo = Nothing
}
describe "should validate that when a page has authRequired, app.auth is also set." $ do
let makeSpec = \appAuth pageAuthRequired ->
basicAppSpec
{ AS.decls =
[ AS.Decl.makeDecl "TestApp" $
basicApp {AS.App.auth = appAuth},
AS.Decl.makeDecl "TestPage" $
basicPage {AS.Page.authRequired = pageAuthRequired},
AS.Decl.makeDecl userEntityName validUserEntity
]
}
it "returns no error if there is no page with authRequired and app.auth is not set" $ do
ASV.validateAppSpec (makeSpec Nothing Nothing) `shouldBe` []
ASV.validateAppSpec (makeSpec Nothing (Just False)) `shouldBe` []
it "returns no error if there is a page with authRequired and app.auth is set" $ do
ASV.validateAppSpec (makeSpec (Just validAppAuth) (Just True)) `shouldBe` []
it "returns an error if there is a page with authRequired and app.auth is not set" $ do
ASV.validateAppSpec (makeSpec Nothing (Just True))
`shouldBe` [ ASV.GenericValidationError
"Expected app.auth to be defined since there are Pages with authRequired set to true."
]
describe "should validate that when app.auth is using EmailAndPassword, user entity is of valid shape." $ do
let makeSpec = \appAuth userEntity ->
basicAppSpec
{ AS.decls =
[ AS.Decl.makeDecl "TestApp" $
basicApp {AS.App.auth = appAuth},
AS.Decl.makeDecl userEntityName (userEntity :: AS.Entity.Entity)
]
}
let invalidUserEntity =
AS.Entity.makeEntity
( PslM.Body
[ PslM.ElementField $ makeBasicPslField "username" PslM.String,
PslM.ElementField $ makeBasicPslField "password" PslM.String
]
)
let invalidUserEntity2 =
AS.Entity.makeEntity
( PslM.Body
[ PslM.ElementField $ makeBasicPslField "email" PslM.String
]
)
it "returns no error if app.auth is not set, regardless of shape of user entity" $ do
ASV.validateAppSpec (makeSpec Nothing invalidUserEntity) `shouldBe` []
ASV.validateAppSpec (makeSpec Nothing validUserEntity) `shouldBe` []
it "returns no error if app.auth is set and user entity is of valid shape" $ do
ASV.validateAppSpec (makeSpec (Just validAppAuth) validUserEntity) `shouldBe` []
it "returns an error if app.auth is set and user entity is of invalid shape" $ do
ASV.validateAppSpec (makeSpec (Just validAppAuth) invalidUserEntity)
`shouldBe` [ ASV.GenericValidationError
"Expected an Entity referenced by app.auth.userEntity to have field 'email' of type 'string'."
]
ASV.validateAppSpec (makeSpec (Just validAppAuth) invalidUserEntity2)
`shouldBe` [ ASV.GenericValidationError
"Expected an Entity referenced by app.auth.userEntity to have field 'password' of type 'string'."
]
where
makeBasicPslField name typ =
PslM.Field
{ PslM._name = name,
PslM._type = typ,
PslM._typeModifiers = [],
PslM._attrs = []
}
basicApp =
AS.App.App
{ AS.App.title = "Test App",
AS.App.db = Nothing,
AS.App.server = Nothing,
AS.App.auth = Nothing,
AS.App.dependencies = Nothing,
AS.App.head = Nothing
}
basicAppDecl = AS.Decl.makeDecl "TestApp" basicApp
basicAppSpec =
AS.AppSpec
{ AS.decls = [basicAppDecl],
AS.externalCodeDirPath = systemSPRoot SP.</> [SP.reldir|test/src|],
AS.externalCodeFiles = [],
AS.isBuild = False,
AS.migrationsDir = Nothing,
AS.dotEnvFile = Nothing
}
basicPage =
AS.Page.Page
{ AS.Page.component =
AS.ExtImport.ExtImport
(AS.ExtImport.ExtImportModule "Home")
(fromJust $ SP.parseRelFileP "pages/Main"),
AS.Page.authRequired = Nothing
}

View File

@ -171,6 +171,7 @@ library
Wasp.AppSpec.Page Wasp.AppSpec.Page
Wasp.AppSpec.Query Wasp.AppSpec.Query
Wasp.AppSpec.Route Wasp.AppSpec.Route
Wasp.AppSpec.Valid
Wasp.Common Wasp.Common
Wasp.CompileOptions Wasp.CompileOptions
Wasp.Data Wasp.Data
@ -320,6 +321,7 @@ test-suite waspc-test
Analyzer.TypeChecker.InternalTest Analyzer.TypeChecker.InternalTest
Analyzer.TypeCheckerTest Analyzer.TypeCheckerTest
AnalyzerTest AnalyzerTest
AppSpec.ValidTest
ErrorTest ErrorTest
FilePath.ExtraTest FilePath.ExtraTest
Fixtures Fixtures