Implement Entity generator (code needs cleanup).

This commit is contained in:
Martin Sosic 2019-05-14 20:50:49 +02:00 committed by Martin Šošić
parent 8396974686
commit 8fafa01504
11 changed files with 284 additions and 8 deletions

View File

@ -1,11 +1,30 @@
{{={= =}=}} {{={= =}=}}
import React, { Component } from 'react' import React, { Component } from 'react'
import { connect } from 'react-redux'
{=# entities =}
import * as {= entityLowerName =}State from '{= entityStatePath =}'
import * as {= entityLowerName =}Actions from '{= entityActionsPath =}'
import {= entity.name =} from '{= entityClassPath =}'
{=/ entities =}
export default class {= page.name =} extends Component { export class {= page.name =} extends Component {
// TODO: Add propTypes.
render() { render() {
return ( return (
{=& page.content =} {=& page.content =}
) )
} }
} }
export default connect(state => ({
{=# entities =}
{= entityLowerName =}List: {= entityLowerName =}State.selectors.all(state)
{=/ entities =}
}), {
{=# entities =}
add{= entityUpperName =}: {= entityLowerName =}Actions.add
{=/ entities =}
})({= page.name =})

View File

@ -0,0 +1,22 @@
{{={= =}=}}
export default class {= entity.name =} {
_data = {}
constructor (data = {}) {
this._data = {
{=# entity.fields =}
{= name =}: data.{= name =},
{=/ entity.fields =}
}
}
{=# entity.fields =}
get {= name =} () {
return this._data.{= name =}
}
{=/ entity.fields =}
toData () {
return this._data
}
}

View File

@ -0,0 +1,2 @@
{{={= =}=}}
export const ADD = 'entities/{= entityLowerName =}/ADD'

View File

@ -0,0 +1,11 @@
{{={= =}=}}
import * as types from './actionTypes'
/**
* @param {{= entity.name =}} {= entityLowerName =}
*/
export const add = ({= entityLowerName =}) => ({
type: types.ADD,
data: {= entityLowerName =}.toData()
})

View File

@ -0,0 +1,41 @@
{{={= =}=}}
import { createSelector } from 'reselect'
import {= entity.name =} from './{= entity.name =}'
import * as types from './actionTypes'
// We assume that root reducer of the app will put this reducer under
// key ROOT_REDUCER_KEY.
const ROOT_REDUCER_KEY = 'entities/{= entity.name =}'
const initialState = {
all: []
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case types.ADD:
return {
...state,
all: [ ...state.all, action.data ]
}
default:
return state
}
}
let selectors = {}
selectors.root = (state) => state[ROOT_REDUCER_KEY]
/**
* @returns {{= entity.name =}[]}
*/
selectors.all = createSelector(selectors.root, (state) => {
return state.all.map(data => new {= entity.name =}(data))
})
export { reducer, initialState, selectors, ROOT_REDUCER_KEY }

View File

@ -1,12 +1,15 @@
{{={= =}=}} {{={= =}=}}
import { combineReducers } from 'redux' import { combineReducers } from 'redux'
// import * as dataState from './modules/data/state' {=# entities =}
import * as {= entityLowerName =}State from '{= entityStatePath =}'
{=/ entities =}
const states = [ const states = [
// dataState, {=# entities =}
// Add reducer here to add it to the app. {= entityLowerName =}State,
{=/ entities =}
] ]
const keyToReducer = states.reduce((acc, state) => { const keyToReducer = states.reduce((acc, state) => {

View File

@ -42,6 +42,7 @@ library:
- text - text
- aeson - aeson
- directory - directory
- split
executables: executables:
stic-exe: stic-exe:

View File

@ -7,10 +7,12 @@ module Generator.Generators
) where ) where
import Data.Aeson ((.=), object, ToJSON(..)) import Data.Aeson ((.=), object, ToJSON(..))
import Data.Char (toLower, toUpper)
import System.FilePath (FilePath, (</>), (<.>)) import System.FilePath (FilePath, (</>), (<.>))
import Generator.FileDraft import Generator.FileDraft
import Wasp import Wasp
import qualified Util
generateWebApp :: Wasp -> [FileDraft] generateWebApp :: Wasp -> [FileDraft]
@ -39,6 +41,8 @@ generatePublicDir wasp
, "manifest.json" , "manifest.json"
] ]
-- * Src dir
generateSrcDir :: Wasp -> [FileDraft] generateSrcDir :: Wasp -> [FileDraft]
generateSrcDir wasp generateSrcDir wasp
= (createCopyFileDraft ("src" </> "logo.png") ("src" </> "logo.png")) = (createCopyFileDraft ("src" </> "logo.png") ("src" </> "logo.png"))
@ -48,13 +52,31 @@ generateSrcDir wasp
, "App.css" , "App.css"
, "index.js" , "index.js"
, "index.css" , "index.css"
, "reducers.js"
, "router.js" , "router.js"
, "serviceWorker.js" , "serviceWorker.js"
, "store/index.js" , "store/index.js"
, "store/middleware/logger.js" , "store/middleware/logger.js"
] ]
++ generatePages wasp ++ generatePages wasp
++ generateEntities wasp
++ [generateReducersJs wasp]
generateReducersJs :: Wasp -> FileDraft
generateReducersJs wasp = createTemplateFileDraft dstPath srcPath templateData
where
srcPath = "src" </> "reducers.js"
dstPath = srcPath
templateData = object
[ "wasp" .= wasp
, "entities" .= map toEntityData (getEntities wasp)
]
toEntityData entity = object
[ "entity" .= entity
, "entityLowerName" .= entityLowerName entity
, "entityStatePath" .= ("./" ++ (entityStatePath entity))
]
-- * Pages
generatePages :: Wasp -> [FileDraft] generatePages :: Wasp -> [FileDraft]
generatePages wasp = generatePage wasp <$> getPages wasp generatePages wasp = generatePage wasp <$> getPages wasp
@ -64,8 +86,106 @@ generatePage wasp page = createTemplateFileDraft dstPath srcPath templateData
where where
srcPath = "src" </> "_Page.js" srcPath = "src" </> "_Page.js"
dstPath = "src" </> (pageName page) <.> "js" dstPath = "src" </> (pageName page) <.> "js"
templateData = object ["wasp" .= wasp, "page" .= page] templateData = object
[ "wasp" .= wasp
, "page" .= page
, "entities" .= map toEntityData (getEntities wasp)
]
toEntityData entity = object
[ "entity" .= entity
, "entityLowerName" .= entityLowerName entity
, "entityUpperName" .= entityUpperName entity
, "entityStatePath" .= ("./" ++ (entityStatePath entity))
, "entityActionsPath" .= ("./" ++ (entityActionsPath entity))
, "entityClassPath" .= ("./" ++ (entityClassPath entity))
]
-- * Entities
generateEntities :: Wasp -> [FileDraft]
generateEntities wasp = concat $ generateEntity wasp <$> getEntities wasp
-- TODO(martin): Create EntityData object that will contain more stuff,
-- like small camel case name and similar, that will be representation used in the
-- template files, instead of Entity directly.
-- Then build that from Entity and pass that to templates.
-- I could even have one data type per template.
-- Or, have function that builds json?
-- TODO(martin): Also, I should extract entity stuff into separate module,
-- there is too much logic here already.
-- TODO(martin): This file has lot of duplication + is missing tests, work on that.
generateEntity :: Wasp -> Entity -> [FileDraft]
generateEntity wasp entity =
[ generateEntityClass wasp entity
, generateEntityState wasp entity
, generateEntityActionTypes wasp entity
, generateEntityActions wasp entity
]
generateEntityClass :: Wasp -> Entity -> FileDraft
generateEntityClass wasp entity = createTemplateFileDraft dstPath srcPath templateData
where
srcPath = "src" </> "entities" </> "_entity" </> "_Entity.js"
dstPath = "src" </> entityClassPath entity
templateData = object
[ "wasp" .= wasp
, "entity" .= entity
]
generateEntityState :: Wasp -> Entity -> FileDraft
generateEntityState wasp entity = createTemplateFileDraft dstPath srcPath templateData
where
srcPath = "src" </> "entities" </> "_entity" </> "state.js"
dstPath = "src" </> entityStatePath entity
templateData = object
[ "wasp" .= wasp
, "entity" .= entity
]
generateEntityActionTypes :: Wasp -> Entity -> FileDraft
generateEntityActionTypes wasp entity = createTemplateFileDraft dstPath srcPath templateData
where
srcPath = "src" </> "entities" </> "_entity" </> "actionTypes.js"
dstPath = "src" </> "entities" </> (entityDirName entity) </> "actionTypes.js"
templateData = object
[ "wasp" .= wasp
, "entity" .= entity
, "entityLowerName" .= entityLowerName entity
]
generateEntityActions :: Wasp -> Entity -> FileDraft
generateEntityActions wasp entity = createTemplateFileDraft dstPath srcPath templateData
where
srcPath = "src" </> "entities" </> "_entity" </> "actions.js"
dstPath = "src" </> entityActionsPath entity
templateData = object
[ "wasp" .= wasp
, "entity" .= entity
, "entityLowerName" .= entityLowerName entity
]
entityDirName :: Entity -> String
entityDirName entity = Util.camelToKebabCase (entityName entity)
entityLowerName :: Entity -> String
entityLowerName Entity{entityName=name} = (toLower $ head name) : (tail name)
entityUpperName :: Entity -> String
entityUpperName Entity{entityName=name} = (toUpper $ head name) : (tail name)
entityStatePath :: Entity -> String
entityStatePath entity = "entities" </> (entityDirName entity) </> "state.js"
entityActionsPath :: Entity -> String
entityActionsPath entity = "entities" </> (entityDirName entity) </> "actions.js"
entityClassPath :: Entity -> String
entityClassPath entity = "entities" </> (entityDirName entity) </> (entityName entity) <.> "js"
-- * Helpers
-- | Creates template file draft that uses given path as both src and dst path -- | Creates template file draft that uses given path as both src and dst path
-- and wasp as template data. -- and wasp as template data.

16
stic/src/Util.hs Normal file
View File

@ -0,0 +1,16 @@
module Util
( camelToKebabCase
) where
import Data.Char (isUpper, toLower)
camelToKebabCase :: String -> String
camelToKebabCase "" = ""
camelToKebabCase camel@(camelHead:camelTail) = kebabHead:kebabTail
where
kebabHead = toLower camelHead
kebabTail = concat $ map
(\(a, b) -> (if (isCamelHump (a, b)) then ['-'] else []) ++ [toLower b])
(zip camel camelTail)
isCamelHump (a, b) = (not . isUpper) a && isUpper b

View File

@ -16,6 +16,8 @@ module Wasp
, Entity (..) , Entity (..)
, EntityField (..) , EntityField (..)
, EntityFieldType (..) , EntityFieldType (..)
, getEntities
, addEntity
) where ) where
import Data.Aeson ((.=), object, ToJSON(..)) import Data.Aeson ((.=), object, ToJSON(..))
@ -82,13 +84,19 @@ data Entity = Entity
, entityFields :: ![EntityField] , entityFields :: ![EntityField]
} deriving (Show, Eq) } deriving (Show, Eq)
data EntityField = EntityField data EntityField = EntityField
{ entityFieldName :: !String { entityFieldName :: !String
, entityFieldType :: !EntityFieldType , entityFieldType :: !EntityFieldType
} deriving (Show, Eq) } deriving (Show, Eq)
data EntityFieldType = EftString | EftBoolean deriving (Show, Eq) data EntityFieldType = EftString | EftBoolean deriving (Show, Eq)
getEntities :: Wasp -> [Entity]
getEntities (Wasp elems) = [entity | (WaspElementEntity entity) <- elems]
addEntity :: Wasp -> Entity -> Wasp
addEntity (Wasp elems) entity = Wasp $ (WaspElementEntity entity):elems
-- * ToJSON instances. -- * ToJSON instances.
-- NOTE(martin): Here I define general transformation of App into JSON that I can then easily use -- NOTE(martin): Here I define general transformation of App into JSON that I can then easily use
@ -107,9 +115,25 @@ instance ToJSON Page where
, "route" .= pageRoute page , "route" .= pageRoute page
, "content" .= pageContent page , "content" .= pageContent page
] ]
instance ToJSON Entity where
toJSON entity = object
[ "name" .= entityName entity
, "fields" .= entityFields entity
]
instance ToJSON EntityField where
toJSON entityField = object
[ "name" .= entityFieldName entityField
, "type" .= entityFieldType entityField
]
instance ToJSON EntityFieldType where
toJSON EftString = "string"
toJSON EftBoolean = "boolean"
instance ToJSON Wasp where instance ToJSON Wasp where
toJSON wasp = object toJSON wasp = object
[ "app" .= getApp wasp [ "app" .= getApp wasp
, "pages" .= getPages wasp , "pages" .= getPages wasp
] ]

17
stic/test/UtilTest.hs Normal file
View File

@ -0,0 +1,17 @@
module UtilTest where
import Test.Tasty.Hspec
import Util
spec_camelToKebabCase :: Spec
spec_camelToKebabCase = do
"foobar" ~> "foobar"
"s3" ~> "s3"
"fooBarBar" ~> "foo-bar-bar"
"s3Folder" ~> "s3-folder"
"S3Folder" ~> "s3-folder"
where
camel ~> kebab = it (camel ++ " -> " ++ kebab) $ do
camelToKebabCase camel `shouldBe` kebab