From 8fafa015040d65ae48bfbd12edd0f973c76e35e9 Mon Sep 17 00:00:00 2001 From: Martin Sosic Date: Tue, 14 May 2019 20:50:49 +0200 Subject: [PATCH] Implement Entity generator (code needs cleanup). --- .../templates/react-app/src/_Page.js | 21 ++- .../react-app/src/entities/_entity/_Entity.js | 22 ++++ .../src/entities/_entity/actionTypes.js | 2 + .../react-app/src/entities/_entity/actions.js | 11 ++ .../react-app/src/entities/_entity/state.js | 41 ++++++ .../templates/react-app/src/reducers.js | 9 +- stic/package.yaml | 1 + stic/src/Generator/Generators.hs | 124 +++++++++++++++++- stic/src/Util.hs | 16 +++ stic/src/Wasp.hs | 28 +++- stic/test/UtilTest.hs | 17 +++ 11 files changed, 284 insertions(+), 8 deletions(-) create mode 100644 stic/data/Generator/templates/react-app/src/entities/_entity/_Entity.js create mode 100644 stic/data/Generator/templates/react-app/src/entities/_entity/actionTypes.js create mode 100644 stic/data/Generator/templates/react-app/src/entities/_entity/actions.js create mode 100644 stic/data/Generator/templates/react-app/src/entities/_entity/state.js create mode 100644 stic/src/Util.hs create mode 100644 stic/test/UtilTest.hs diff --git a/stic/data/Generator/templates/react-app/src/_Page.js b/stic/data/Generator/templates/react-app/src/_Page.js index 3d70e83e4..c074ce00f 100644 --- a/stic/data/Generator/templates/react-app/src/_Page.js +++ b/stic/data/Generator/templates/react-app/src/_Page.js @@ -1,11 +1,30 @@ {{={= =}=}} 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() { return ( {=& page.content =} ) } } + +export default connect(state => ({ +{=# entities =} + {= entityLowerName =}List: {= entityLowerName =}State.selectors.all(state) +{=/ entities =} +}), { +{=# entities =} + add{= entityUpperName =}: {= entityLowerName =}Actions.add +{=/ entities =} +})({= page.name =}) diff --git a/stic/data/Generator/templates/react-app/src/entities/_entity/_Entity.js b/stic/data/Generator/templates/react-app/src/entities/_entity/_Entity.js new file mode 100644 index 000000000..b94e78792 --- /dev/null +++ b/stic/data/Generator/templates/react-app/src/entities/_entity/_Entity.js @@ -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 + } +} diff --git a/stic/data/Generator/templates/react-app/src/entities/_entity/actionTypes.js b/stic/data/Generator/templates/react-app/src/entities/_entity/actionTypes.js new file mode 100644 index 000000000..a30f2b7da --- /dev/null +++ b/stic/data/Generator/templates/react-app/src/entities/_entity/actionTypes.js @@ -0,0 +1,2 @@ +{{={= =}=}} +export const ADD = 'entities/{= entityLowerName =}/ADD' diff --git a/stic/data/Generator/templates/react-app/src/entities/_entity/actions.js b/stic/data/Generator/templates/react-app/src/entities/_entity/actions.js new file mode 100644 index 000000000..89fa9a795 --- /dev/null +++ b/stic/data/Generator/templates/react-app/src/entities/_entity/actions.js @@ -0,0 +1,11 @@ +{{={= =}=}} +import * as types from './actionTypes' + + +/** + * @param {{= entity.name =}} {= entityLowerName =} + */ +export const add = ({= entityLowerName =}) => ({ + type: types.ADD, + data: {= entityLowerName =}.toData() +}) diff --git a/stic/data/Generator/templates/react-app/src/entities/_entity/state.js b/stic/data/Generator/templates/react-app/src/entities/_entity/state.js new file mode 100644 index 000000000..35bba16a8 --- /dev/null +++ b/stic/data/Generator/templates/react-app/src/entities/_entity/state.js @@ -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 } diff --git a/stic/data/Generator/templates/react-app/src/reducers.js b/stic/data/Generator/templates/react-app/src/reducers.js index 1d5e6c563..6cb2c57a4 100644 --- a/stic/data/Generator/templates/react-app/src/reducers.js +++ b/stic/data/Generator/templates/react-app/src/reducers.js @@ -1,12 +1,15 @@ {{={= =}=}} import { combineReducers } from 'redux' -// import * as dataState from './modules/data/state' +{=# entities =} +import * as {= entityLowerName =}State from '{= entityStatePath =}' +{=/ entities =} const states = [ - // dataState, - // Add reducer here to add it to the app. + {=# entities =} + {= entityLowerName =}State, + {=/ entities =} ] const keyToReducer = states.reduce((acc, state) => { diff --git a/stic/package.yaml b/stic/package.yaml index da2a9db75..ffc8d799a 100644 --- a/stic/package.yaml +++ b/stic/package.yaml @@ -42,6 +42,7 @@ library: - text - aeson - directory + - split executables: stic-exe: diff --git a/stic/src/Generator/Generators.hs b/stic/src/Generator/Generators.hs index d1661652c..f9e24934e 100644 --- a/stic/src/Generator/Generators.hs +++ b/stic/src/Generator/Generators.hs @@ -7,10 +7,12 @@ module Generator.Generators ) where import Data.Aeson ((.=), object, ToJSON(..)) +import Data.Char (toLower, toUpper) import System.FilePath (FilePath, (), (<.>)) import Generator.FileDraft import Wasp +import qualified Util generateWebApp :: Wasp -> [FileDraft] @@ -39,6 +41,8 @@ generatePublicDir wasp , "manifest.json" ] +-- * Src dir + generateSrcDir :: Wasp -> [FileDraft] generateSrcDir wasp = (createCopyFileDraft ("src" "logo.png") ("src" "logo.png")) @@ -48,13 +52,31 @@ generateSrcDir wasp , "App.css" , "index.js" , "index.css" - , "reducers.js" , "router.js" , "serviceWorker.js" , "store/index.js" , "store/middleware/logger.js" ] ++ 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 = generatePage wasp <$> getPages wasp @@ -64,8 +86,106 @@ generatePage wasp page = createTemplateFileDraft dstPath srcPath templateData where srcPath = "src" "_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 -- and wasp as template data. diff --git a/stic/src/Util.hs b/stic/src/Util.hs new file mode 100644 index 000000000..36529fc18 --- /dev/null +++ b/stic/src/Util.hs @@ -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 diff --git a/stic/src/Wasp.hs b/stic/src/Wasp.hs index 481d7b940..b82a49ac5 100644 --- a/stic/src/Wasp.hs +++ b/stic/src/Wasp.hs @@ -16,6 +16,8 @@ module Wasp , Entity (..) , EntityField (..) , EntityFieldType (..) + , getEntities + , addEntity ) where import Data.Aeson ((.=), object, ToJSON(..)) @@ -82,13 +84,19 @@ data Entity = Entity , entityFields :: ![EntityField] } deriving (Show, Eq) -data EntityField = EntityField +data EntityField = EntityField { entityFieldName :: !String , entityFieldType :: !EntityFieldType } 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. -- 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 , "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 toJSON wasp = object [ "app" .= getApp wasp , "pages" .= getPages wasp ] - diff --git a/stic/test/UtilTest.hs b/stic/test/UtilTest.hs new file mode 100644 index 000000000..2ceaa4105 --- /dev/null +++ b/stic/test/UtilTest.hs @@ -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