Implemented basic code generation for Page.

This commit is contained in:
Martin Sosic 2019-04-22 12:58:31 +02:00 committed by Martin Šošić
parent d44ff4ea7f
commit 6150d01f37
17 changed files with 165 additions and 87 deletions

View File

@ -6,6 +6,7 @@
"dependencies": {
"react": "^16.8.5",
"react-dom": "^16.8.5",
"react-router-dom": "^5.0.0",
"react-scripts": "2.1.8"
},
"scripts": {

View File

@ -0,0 +1,12 @@
{{={= =}=}}
import React, { Component } from 'react'
class {= page.name =} extends Component {
render() {
return (
{=& page.content =}
)
}
}
export default {= page.name =}

View File

@ -1,11 +1,13 @@
{{={{> <}}=}}
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import ReactDOM from 'react-dom'
ReactDOM.render(<App />, document.getElementById('root'));
import router from './router'
import * as serviceWorker from './serviceWorker'
import './index.css'
ReactDOM.render(router, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.

View File

@ -0,0 +1,22 @@
{{={= =}=}}
import React from 'react'
import { Route, BrowserRouter as Router } from 'react-router-dom'
import App from './App'
{=# pages =}
import {= name =} from './{= name =}'
{=/ pages =}
const router = (
<Router>
<div>
<Route exact path="/" component={App} />
{=# pages =}
<Route exact path="{= route =}" component={ {= name =} }/>
{=/ pages =}
</div>
</Router>
)
export default router

View File

@ -1,23 +1,20 @@
{-# LANGUAGE OverloadedStrings #-}
module Generator.Generators
( generateWebApp
-- EXPORTED ONLY FOR TESTS:
, generatePage
) where
import qualified Data.Aeson as Aeson
import System.FilePath (FilePath, (</>))
import Data.Aeson ((.=), object, ToJSON(..))
import System.FilePath (FilePath, (</>), (<.>))
import Generator.FileDraft
import Wasp
defaultCreateTemplateFileDraft :: FilePath -> Wasp -> FileDraft
defaultCreateTemplateFileDraft path wasp = createTemplateFileDraft path path (Aeson.toJSON wasp)
type FileDraftGenerator = Wasp -> [FileDraft]
generateWebApp :: FileDraftGenerator
generateWebApp wasp = concat $ map ($ wasp)
generateWebApp :: Wasp -> [FileDraft]
generateWebApp wasp = concatMap ($ wasp)
[ generateReadme
, generatePackageJson
, generateGitignore
@ -25,31 +22,49 @@ generateWebApp wasp = concat $ map ($ wasp)
, generateSrcDir
]
generateReadme :: FileDraftGenerator
generateReadme wasp = [defaultCreateTemplateFileDraft "README.md" wasp]
generateReadme :: Wasp -> [FileDraft]
generateReadme wasp = [simpleTemplateFileDraft "README.md" wasp]
generatePackageJson :: FileDraftGenerator
generatePackageJson wasp = [defaultCreateTemplateFileDraft "package.json" wasp]
generatePackageJson :: Wasp -> [FileDraft]
generatePackageJson wasp = [simpleTemplateFileDraft "package.json" wasp]
generateGitignore :: FileDraftGenerator
generateGitignore wasp = [createTemplateFileDraft ".gitignore" "gitignore" (Aeson.toJSON wasp)]
generateGitignore :: Wasp -> [FileDraft]
generateGitignore wasp = [createTemplateFileDraft ".gitignore" "gitignore" (toJSON wasp)]
generatePublicDir :: FileDraftGenerator
generatePublicDir :: Wasp -> [FileDraft]
generatePublicDir wasp
= createCopyFileDraft ("public" </> "favicon.ico") ("public" </> "favicon.ico")
: map (\path -> defaultCreateTemplateFileDraft ("public/" </> path) wasp)
: map (\path -> simpleTemplateFileDraft ("public/" </> path) wasp)
[ "index.html"
, "manifest.json"
]
generateSrcDir :: FileDraftGenerator
generateSrcDir :: Wasp -> [FileDraft]
generateSrcDir wasp
= (createCopyFileDraft ("src" </> "logo.png") ("src" </> "logo.png"))
: map (\path -> defaultCreateTemplateFileDraft ("src/" </> path) wasp)
: map (\path -> simpleTemplateFileDraft ("src/" </> path) wasp)
[ "App.css"
, "App.js"
, "App.test.js"
, "index.css"
, "index.js"
, "router.js"
, "serviceWorker.js"
]
++ generatePages wasp
generatePages :: Wasp -> [FileDraft]
generatePages wasp = generatePage wasp <$> getPages wasp
generatePage :: Wasp -> Page -> FileDraft
generatePage wasp page = createTemplateFileDraft dstPath srcPath templateData
where
srcPath = "src" </> "_Page.js"
dstPath = "src" </> (pageName page) <.> "js"
templateData = object ["wasp" .= wasp, "page" .= page]
-- | Creates template file draft that uses given path as both src and dst path
-- and wasp as template data.
simpleTemplateFileDraft :: FilePath -> Wasp -> FileDraft
simpleTemplateFileDraft path wasp = createTemplateFileDraft path path (toJSON wasp)

View File

@ -2,7 +2,7 @@ module Parser
( parseWasp
) where
import Text.Parsec (parse, ParseError, (<|>), many1, eof)
import Text.Parsec (ParseError, (<|>), many1, eof)
import Text.Parsec.String (Parser)
import Lexer
@ -30,7 +30,7 @@ waspParser = do
waspElems <- many1 waspElement
eof
-- TODO(matija): after we parsed everything, we should do semantic analysis
-- e.g. check there is only 1 title - if not, throw a meaningful error.

View File

@ -4,7 +4,7 @@
module Parser.Common where
import Text.Parsec
import Text.Parsec (ParseError, parse, many, noneOf)
import Text.Parsec.String (Parser)
import qualified Data.Text as T

View File

@ -2,7 +2,7 @@ module Util.Fib (
fibonacci
) where
fibonacci :: (Num a, Ord a, Num b) => a -> b
fibonacci :: Int -> Int
fibonacci 0 = 0
fibonacci 1 = 1
fibonacci n | n > 1 = (fibonacci (n - 1)) + (fibonacci (n - 2))

View File

@ -2,17 +2,23 @@
module Wasp
( Wasp
, WaspElement (..)
, fromWaspElems
, App (..)
, fromApp
, fromWaspElems
, getApp
, setApp
, Page (..)
, getPages
, addPage
) where
import qualified Data.Aeson as Aeson
import Data.Aeson ((.=), object, ToJSON(..))
-- * Wasp
data Wasp = Wasp [WaspElement] deriving (Show, Eq)
data WaspElement
@ -21,11 +27,15 @@ data WaspElement
| WaspElementEntity
deriving (Show, Eq)
-- App
fromWaspElems :: [WaspElement] -> Wasp
fromWaspElems elems = Wasp elems
-- * App
data App = App
{ appName :: !String -- Identifier
, appTitle :: !String -- Title
, appTitle :: !String
} deriving (Show, Eq)
getApp :: Wasp -> App
@ -39,10 +49,7 @@ isAppElem WaspElementApp{} = True
isAppElem _ = False
getApps :: Wasp -> [App]
getApps (Wasp elems) = map getAppFromElem $ filter isAppElem elems
where
getAppFromElem (WaspElementApp app) = app
getAppFromElem _ = error "Not an app"
getApps (Wasp elems) = [app | (WaspElementApp app) <- elems]
setApp :: Wasp -> App -> Wasp
setApp (Wasp elems) app = Wasp $ (WaspElementApp app) : (filter (not . isAppElem) elems)
@ -50,26 +57,42 @@ setApp (Wasp elems) app = Wasp $ (WaspElementApp app) : (filter (not . isAppElem
fromApp :: App -> Wasp
fromApp app = Wasp [WaspElementApp app]
-- NOTE(martin): Here I define general transformation of App into JSON that I can then easily use
-- as data for templates, but we will probably want to replace this in the future with the better tailored
-- types that are exact fit for what is neeed (for example one type per template).
instance Aeson.ToJSON App where
toJSON app = Aeson.object
[ "name" Aeson..= appName app
, "title" Aeson..= appTitle app
]
instance Aeson.ToJSON Wasp where
toJSON wasp = Aeson.object
[ "app" Aeson..= getApp wasp
]
fromWaspElems :: [WaspElement] -> Wasp
fromWaspElems elems = Wasp elems
-- Page
-- * Page
data Page = Page
{ pageName :: !String
{ pageName :: !String -- Identifier
, pageRoute :: !String
, pageContent :: !String
} deriving (Show, Eq)
getPages :: Wasp -> [Page]
getPages (Wasp elems) = [page | (WaspElementPage page) <- elems]
addPage :: Wasp -> Page -> Wasp
addPage (Wasp elems) page = Wasp $ (WaspElementPage page):elems
-- * ToJSON instances.
-- NOTE(martin): Here I define general transformation of App into JSON that I can then easily use
-- as data for templates, but we will probably want to replace this in the future with the better tailored
-- types that are exact fit for what is neeed, for example one type per template, which
-- will also enable us to check via types if template data is correctly shaped.
instance ToJSON App where
toJSON app = object
[ "name" .= appName app
, "title" .= appTitle app
]
instance ToJSON Page where
toJSON page = object
[ "name" .= pageName page
, "route" .= pageRoute page
, "content" .= pageContent page
]
instance ToJSON Wasp where
toJSON wasp = object
[ "app" .= getApp wasp
, "pages" .= getPages wasp
]

View File

@ -1,6 +1,5 @@
module Generator.FileDraft.CopyFileDraftTest where
import qualified Test.Tasty
import Test.Tasty.Hspec
import System.FilePath ((</>), takeDirectory)

View File

@ -1,12 +1,11 @@
{-# LANGUAGE OverloadedStrings #-}
module Generator.FileDraft.TemplateFileDraftTest where
import qualified Test.Tasty
import Test.Tasty.Hspec
import Data.Aeson (object, (.=))
import System.FilePath (FilePath, (</>), takeDirectory)
import Data.Text (Text, pack)
import System.FilePath ((</>), takeDirectory)
import Data.Text (Text)
import Generator.FileDraft
@ -29,7 +28,6 @@ spec_TemplateFileDraft = do
(dstDir, dstPath, templatePath) = ("a/b", "c/d/dst.txt", "e/tmpl.txt")
templateData = object [ "foo" .= ("bar" :: String) ]
fileDraft = createTemplateFileDraft dstPath templatePath templateData
expectedTemplatePath = mockTemplatesDirAbsPath </> templatePath
expectedDstPath = dstDir </> dstPath
mockTemplatesDirAbsPath = "mock/templates/dir"
mockTemplateContent = "Mock template content" :: Text

View File

@ -1,11 +1,9 @@
{-# LANGUAGE OverloadedStrings #-}
module Generator.GeneratorsTest where
import qualified Test.Tasty
import Test.Tasty.Hspec
import System.FilePath (FilePath, (</>))
import Data.Aeson as Aeson (object, (.=))
import System.FilePath (FilePath, (</>), (<.>))
import Generator.Generators
import Generator.FileDraft
@ -13,16 +11,22 @@ import Generator.FileDraft.TemplateFileDraft
import Generator.FileDraft.CopyFileDraft
import Wasp
-- TODO(martin): We could define Arbitrary instance for Wasp, define properties over
-- generator functions and then do property testing on them, that would be cool.
spec_Generators :: Spec
spec_Generators = do
let testApp = (App "TestApp" "Test App")
let testPage = (Page "TestPage" "/test-page" "<div>Test Page</div>")
let testWasp = (fromApp testApp) `addPage` testPage
describe "generateWebApp" $ do
-- NOTE: This test does not (for now) check that content of files is correct or
-- that they will successfully be written, it checks only that their
-- destinations are correct.
it "Given a simple Wasp, creates file drafts at expected destinations." $ do
let fileDrafts = generateWebApp simpleWasp
let expectedFileDrafts = concat $
it "Given a simple Wasp, creates file drafts at expected destinations" $ do
let fileDrafts = generateWebApp testWasp
let expectedFileDraftDstPaths = concat $
[ [ "README.md"
, "package.json"
, ".gitignore"
@ -39,25 +43,28 @@ spec_Generators = do
, "App.test.js"
, "index.css"
, "index.js"
, "router.js"
, "serviceWorker.js"
, (pageName testPage <.> "js")
]
]
mapM_
-- NOTE(martin): I added fd to the pair here in order to have it
-- printed when shouldBe fails, otherwise I could not know which
-- file draft failed.
(\fd -> (fd, existsFdWithDst fileDrafts fd) `shouldBe` (fd, True))
expectedFileDrafts
where
(appName, appTitle) = ("TestApp", "Test App")
(\dstPath -> (dstPath, existsFdWithDst fileDrafts dstPath)
`shouldBe` (dstPath, True))
expectedFileDraftDstPaths
simpleWasp :: Wasp
simpleWasp = fromApp $ App appName appTitle
describe "generatePage" $ do
it "Given a simple Wasp, creates template file draft from _Page.js" $ do
let (FileDraftTemplateFd (TemplateFileDraft _ srcPath _))
= generatePage testWasp (head $ getPages testWasp)
srcPath `shouldBe` "src" </> "_Page.js"
existsFdWithDst :: [FileDraft] -> FilePath -> Bool
existsFdWithDst fds dstPath =
length (filter ((== dstPath). getFileDraftDstPath) fds) == 1
existsFdWithDst :: [FileDraft] -> FilePath -> Bool
existsFdWithDst fds dstPath = any ((== dstPath) . getFileDraftDstPath) fds
getFileDraftDstPath :: FileDraft -> FilePath
getFileDraftDstPath (FileDraftTemplateFd fd) = templateFileDraftDstFilepath fd

View File

@ -11,9 +11,8 @@ module Generator.MockFileDraftIO
import System.FilePath (FilePath, (</>))
import Data.Text (Text, pack)
import Control.Monad.State
import Data.Aeson as Aeson
import qualified Data.Aeson as Aeson
import Generator.FileDraft
import Generator.FileDraft.FileDraftIO
@ -25,7 +24,7 @@ defaultMockConfig :: MockFdIOConfig
defaultMockConfig = MockFdIOConfig
{ getTemplatesDirAbsPath_impl = "mock/templates/dir"
, getTemplateFileAbsPath_impl = \path -> "mock/templates/dir" </> path
, compileAndRenderTemplate_impl = \path json -> (pack "Mock template content")
, compileAndRenderTemplate_impl = \_ _ -> (pack "Mock template content")
}
getMockLogs :: MockFdIO a -> MockFdIOConfig -> MockFdIOLogs
@ -58,6 +57,7 @@ instance FileDraftIO MockFdIO where
(_, config) <- get
return $ (compileAndRenderTemplate_impl config) path json
modifyLogs :: MonadState (a, b) m => (a -> a) -> m ()
modifyLogs f = modify (\(logs, config) -> (f logs, config))
newtype MockFdIO a = MockFdIO { unMockFdIO :: State (MockFdIOLogs, MockFdIOConfig) a }
@ -78,22 +78,28 @@ data MockFdIOConfig = MockFdIOConfig
, compileAndRenderTemplate_impl :: FilePath -> Aeson.Value -> Text
}
writeFileFromText_addCall :: FilePath -> Text -> MockFdIOLogs -> MockFdIOLogs
writeFileFromText_addCall path text logs =
logs { writeFileFromText_calls = (path, text):(writeFileFromText_calls logs) }
getTemplatesDirAbsPath_addCall :: MockFdIOLogs -> MockFdIOLogs
getTemplatesDirAbsPath_addCall logs =
logs { getTemplatesDirAbsPath_calls = ():(getTemplatesDirAbsPath_calls logs) }
getTemplateFileAbsPath_addCall :: FilePath -> MockFdIOLogs -> MockFdIOLogs
getTemplateFileAbsPath_addCall path logs =
logs { getTemplateFileAbsPath_calls = (path):(getTemplateFileAbsPath_calls logs) }
copyFile_addCall :: FilePath -> FilePath -> MockFdIOLogs -> MockFdIOLogs
copyFile_addCall srcPath dstPath logs =
logs { copyFile_calls = (srcPath, dstPath):(copyFile_calls logs) }
createDirectoryIfMissing_addCall :: Bool -> FilePath -> MockFdIOLogs -> MockFdIOLogs
createDirectoryIfMissing_addCall createParents path logs =
logs { createDirectoryIfMissing_calls =
(createParents, path):(createDirectoryIfMissing_calls logs) }
compileAndRenderTemplate_addCall :: FilePath -> Aeson.Value -> MockFdIOLogs -> MockFdIOLogs
compileAndRenderTemplate_addCall path json logs =
logs { compileAndRenderTemplate_calls =
(path, json):(compileAndRenderTemplate_calls logs) }

View File

@ -1,10 +1,8 @@
module Parser.CommonTest where
import qualified Test.Tasty
import Test.Tasty.Hspec
import Text.Parsec
import Text.Parsec.String (Parser)
import Data.Either
import Lexer
@ -58,7 +56,7 @@ spec_parseWaspCommon = do
describe "Parsing wasp closure" $ do
let parseWaspClosure input = runWaspParser waspClosure input
let closureContent = "<div>hello world</div>"
it "Returns the content of closure" $ do
parseWaspClosure ("{ " ++ closureContent ++ " }")
`shouldBe` Right closureContent
@ -66,6 +64,3 @@ spec_parseWaspCommon = do
it "Removes leading and trailing spaces" $ do
parseWaspClosure ("{ " ++ closureContent ++ " }")
`shouldBe` Right closureContent

View File

@ -1,6 +1,5 @@
module Parser.PageTest where
import qualified Test.Tasty
import Test.Tasty.Hspec
import Data.Either

View File

@ -1,6 +1,5 @@
module Parser.ParserTest where
import qualified Test.Tasty
import Test.Tasty.Hspec
import Data.Either

View File

@ -1,6 +1,5 @@
module Util.FibTest where
import qualified Test.Tasty
import Test.Tasty.Hspec
import Test.Tasty.QuickCheck
@ -20,6 +19,7 @@ spec_fibonacci = do
-- NOTE: Most likely not the best way to write QuickCheck test, I just did this in order
-- to get something working as an example.
prop_fibonacci :: Property
prop_fibonacci = forAll (choose (0, 10)) $ testFibSequence
where
testFibSequence :: Int -> Bool