packages feed

rob (empty) → 0.0.1

raw patch · 18 files changed

+885/−0 lines, 18 filesdep +Globdep +ansi-terminaldep +basesetup-changed

Dependencies added: Glob, ansi-terminal, base, bytestring, cmdargs, directory, ede, filepath, fortytwo, pathwalk, rob, text, time, unordered-containers, vector, yaml

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Gianluca Guarini (c) 2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Gianluca Guarini nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,34 @@+<p align="center">+  <img src="rob-logo.svg" alt="rob logo"/>+</p>++<p align="center">+Simple projects generator written in Haskell+</p>++---+> Call on me in the day of trouble, and I will deliver, and thou shalt glorify me.</p>+###### Daniel Defoe, Robinson Crusoe++# Installation++```shell+$ cabal install rob+```++# Usage++## TODO++### Available commands++```shell+$ rob --help+```++# Todo++- [ ] Unit test!+- [ ] Improve performance+- [ ] Write Documentation+- [ ] Refactor the code and cleanup
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,29 @@+module Main where++import System.Environment+import System.Exit+import System.Console.CmdArgs (cmdArgsRun)+import System.Console.CmdArgs.Explicit(helpText, HelpFormat(..))+import Rob.Types (Task(..))+import Rob.Tasks (mode)+import qualified Rob.Actions.Add+import qualified Rob.Actions.New+import qualified Rob.Actions.List+import qualified Rob.Actions.Remove++main :: IO()+main = do+  args <- getArgs+  if null args then exitWithHelp+  else parse =<< cmdArgsRun mode++parse :: Task -> IO ()+parse Add  {name = n, path = p} = Rob.Actions.Add.main n p+parse New = Rob.Actions.New.main+parse List = Rob.Actions.List.main+parse Remove = Rob.Actions.Remove.main++exitWithHelp :: IO a+exitWithHelp = do+  putStr $ show $ helpText [] HelpFormatOne mode+  exitSuccess
+ rob.cabal view
@@ -0,0 +1,69 @@+name:                rob+version:0.0.1+synopsis:            Simple projects generator+description:         See README at <https://github.com/GianlucaGuarini/rob/blob/develop/README.md>+homepage:            https://github.com/gianlucaguarini/rob#readme+license:             MIT+license-file:        LICENSE+author:              Gianluca Guarini+maintainer:          gianluca.guarini@gmail.com+copyright:           Gianlua Guarini+category:            CLI+build-type:          Simple+extra-source-files:  README.md+cabal-version:       >=1.10++library+  hs-source-dirs:      src+  exposed-modules:     Rob.Config,+                       Rob.Types,+                       Rob.Project,+                       Rob.Tasks,+                       Rob.UserMessages,+                       Rob.Questionnaire,+                       Rob.Logger,+                       Rob.Package,+                       Rob.Actions.New,+                       Rob.Actions.Remove,+                       Rob.Actions.List,+                       Rob.Actions.Add+  build-depends:       base >= 4.7 && < 5,+                       fortytwo <= 1.0.2,+                       directory <= 1.3.0.0,+                       filepath <= 1.4.1.2,+                       pathwalk <= 0.3.1.2,+                       Glob <= 0.9.1,+                       yaml <= 0.8.23.3,+                       vector <= 0.12.0.1,+                       ede == 0.2.8.7,+                       text <= 1.2.2.2,+                       unordered-containers <= 0.2.8.0,+                       cmdargs >= 0.10.17 && <= 0.10.18,+                       ansi-terminal <= 0.6.3.1,+                       bytestring <= 0.10.8.1,+                       time <= 1.6.0.1+  other-modules:       Paths_rob+  default-language:    Haskell2010++executable rob+  hs-source-dirs:      app+  main-is:             Main.hs+  build-depends:       base >= 4.7 && < 5,+                       cmdargs >= 0.10.17 && <= 0.10.18+  build-depends:       base,+                       rob+  default-language:    Haskell2010++test-suite rob-test+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Spec.hs+  build-depends:       base,+                       directory,+                       rob+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N+  default-language:    Haskell2010++source-repository head+  type:     git+  location: https://github.com/gianlucaguarini/rob
+ src/Rob/Actions/Add.hs view
@@ -0,0 +1,26 @@+module Rob.Actions.Add (main) where++import Rob.Logger (err, success)+import Rob.Config (get, addTemplate)+import Rob.Project (hasPathQuestionnaire)+import Rob.UserMessages (projectPathDoesNotExist, projectQuestionnaireMissing, projectAdded)++import System.Exit (exitFailure, exitSuccess)+import System.Directory (doesPathExist)++main :: String -> FilePath -> IO()+main name path = do+  hasProjectPath <- doesPathExist path+  if hasProjectPath then do+    hasQuestionnaire <- hasPathQuestionnaire path+    if hasQuestionnaire then do+      config <- get+      _ <- addTemplate config name path+      success $ projectAdded name+      exitSuccess+    else do+      err $ projectQuestionnaireMissing path+      exitFailure+  else do+    err $ projectPathDoesNotExist path+    exitFailure
+ src/Rob/Actions/List.hs view
@@ -0,0 +1,22 @@+module Rob.Actions.List (main) where++import Rob.Logger (info)+import Rob.UserMessages (availableTemplates, emptyString)+import Rob.Config (get, errorNoTemplatesAvailable)+import Rob.Types(Config(..))++import System.Exit (exitSuccess)++main :: IO()+main = do+  config <- get+  putStrLn emptyString+  info availableTemplates+  putStrLn emptyString+  listTemplates config+  exitSuccess++-- | List all the templates as string+listTemplates :: Config -> IO ()+listTemplates (Config []) = errorNoTemplatesAvailable;+listTemplates (Config templates) = putStrLn $ unlines $ map show templates
+ src/Rob/Actions/New.hs view
@@ -0,0 +1,45 @@+module Rob.Actions.New (main) where++import Rob.Logger (err, success)+import Rob.Config (get, errorNoTemplatesAvailable)+import Rob.Types (Config(..))+import Rob.Questionnaire (run)+import Rob.Project (getTemplatePathByName, getTemplateName, createFilesFromTemplate)+import Rob.UserMessages (+    choseATemplate,+    noTemplateSelected,+    projectSuccessfullyCreated,+    projectPathDoesNotExist,+    emptyString+  )++import System.Exit (exitFailure, exitSuccess)+import System.Directory (doesPathExist)+import FortyTwo (select)++main :: IO ()+main = do+  config <- get+  createNewProject config++-- | Create a new project using one of the templates available+createNewProject :: Config -> IO ()+createNewProject (Config []) = errorNoTemplatesAvailable+createNewProject (Config templates) = do+  templateName <- select choseATemplate $ map getTemplateName templates+  putStrLn emptyString+  if not . null $ templateName then do+    let path = getTemplatePathByName templates templateName+    hasProjectPath <- doesPathExist path+    if hasProjectPath then do+      responses <- run path+      putStrLn emptyString+      createFilesFromTemplate path responses+      success projectSuccessfullyCreated+      exitSuccess+    else do+      err $ projectPathDoesNotExist path+      exitFailure+  else do+    err noTemplateSelected+    exitFailure
+ src/Rob/Actions/Remove.hs view
@@ -0,0 +1,32 @@+module Rob.Actions.Remove (main) where++import Rob.Logger (err)+import Rob.Config (get, errorNoTemplatesAvailable, deleteTemplate)+import Rob.Types (Config(..))+import Rob.Project (getTemplateName)+import Rob.UserMessages (+    choseATemplateToDelete,+    noTemplateSelected,+    emptyString+  )++import System.Exit (exitFailure, exitSuccess)+import FortyTwo (select)++main :: IO ()+main = do+  config <- get+  nukeTemplate config++-- Remove a project template from the available ones+nukeTemplate :: Config -> IO ()+nukeTemplate (Config []) = errorNoTemplatesAvailable+nukeTemplate (Config templates) = do+  templateName <- select choseATemplateToDelete $ map getTemplateName templates+  putStrLn emptyString+  if (not . null) templateName then do+    _ <- deleteTemplate (Config templates) templateName+    exitSuccess+  else do+    err noTemplateSelected+    exitFailure
+ src/Rob/Config.hs view
@@ -0,0 +1,68 @@+module Rob.Config where++import qualified Rob.Package as Package+import Rob.Logger (err, warning, success, info, flatten)+import Rob.UserMessages (configFileFound, noConfigFileFound, configFileCreated, noTemplatesAvailable, tryAddingATemplate)+import Rob.Types (Config(..), Template(..))++import Data.Maybe+import System.Exit+import System.FilePath (joinPath)+import System.Directory (getHomeDirectory, doesFileExist)+import Data.Yaml (encodeFile, decodeFile)++-- | Get the config file name+configFileName :: String+configFileName = "." ++ Package.name++-- | Get the whole path to the config file+configFilePath :: IO FilePath+configFilePath = do+  home <- getHomeDirectory+  return $ joinPath [home, configFileName]++-- | Write the config file and return it+write :: Config -> IO Config+write config = do+  configFilePath >>= \path -> encodeFile path config+  return config++-- | Get the current Config file Data+-- | If it doesn't exist it will create a new one+get :: IO Config+get = do+  path <- configFilePath+  hasConfigPath <- doesFileExist path+  if hasConfigPath+    then do+      success $ configFileFound path+      config <- decodeFile path+      return $ fromJust config+    else do+      warning $ noConfigFileFound configFileName+      flatten info $ configFileCreated path+      -- return an empty Config object and write it in the home directory+      write $ Config []++-- | Dispatch the no templates available error+errorNoTemplatesAvailable :: IO ()+errorNoTemplatesAvailable = do+  err noTemplatesAvailable+  warning tryAddingATemplate+  exitFailure++-- | Add a new template to the config object and write it+addTemplate :: Config -> String -> String -> IO Config+addTemplate (Config templates) name path = write $ Config newTemplates+  where+    newTemplate = Template name path+    newTemplates = if newTemplate `elem` templates then+        map (\t -> if t == newTemplate then newTemplate else t) templates+      else+        templates ++ [newTemplate]++-- | Delete a template from the list of templates+deleteTemplate :: Config -> String -> IO Config+deleteTemplate (Config templates) nameToRemove = write $ Config newTemplates+  where+    newTemplates = (\(Template name _) -> name /= nameToRemove) `filter` templates
+ src/Rob/Logger.hs view
@@ -0,0 +1,48 @@+module Rob.Logger where++import System.Console.ANSI+import Data.Time.Clock (getCurrentTime, UTCTime)+import Data.Time.Format (formatTime, defaultTimeLocale)++-- | Print a message highlighting it+log' :: Color -> ColorIntensity -> String -> IO()+log' color intensity message = do+  setSGR [SetColor Foreground Dull White]+  time <- getCurrentTime+  print' $ formatTime' time+  setSGR [Reset]+  setSGR [SetColor Foreground intensity color]+  print' $ message ++ "\n"+  setSGR [Reset]++-- | Format the time, showing it into the log message+formatTime' :: UTCTime -> String+formatTime' = formatTime defaultTimeLocale "[%T] "++-- | Print a message in the console+print' :: String -> IO()+print' = putStr++-- | Log success messages+success :: String -> IO()+success = log' Green Dull++-- | Log error messages+err :: String -> IO()+err = log' Red Vivid++-- | Log info messages+info :: String -> IO()+info = log' Cyan Dull++-- | Log warning messages+warning :: String -> IO()+warning = log' Yellow Dull++-- | Log a raw message without using colors+raw :: String -> IO()+raw = print'++-- | Execute log on a list of messages+flatten :: Monad m => (a -> m b) -> [a] -> m ()+flatten = mapM_
+ src/Rob/Package.hs view
@@ -0,0 +1,22 @@+module Rob.Package where++-- TODO: get these props from the .cabal file++import qualified Paths_rob+import Data.Version (showVersion)++-- | Return the Package version (stored in the .cabal file)+version :: String+version = showVersion Paths_rob.version++-- | Return the package name+name :: String+name = "rob"++-- | Return the package author+author :: String+author = "Author: Gianluca Guarini"++-- | Return the package description+description :: String+description = "Projects generator"
+ src/Rob/Project.hs view
@@ -0,0 +1,181 @@+{-# LANGUAGE OverloadedStrings #-}++module Rob.Project where++import Rob.Types (Template(..))+import Rob.Logger (warning, success)+import Rob.UserMessages (parserError, fileCreated)++import Data.Yaml (Value)+import Control.Monad (forM_, unless)+import Data.Text (Text)+import qualified Data.ByteString.Lazy as BS+import Data.Text.Lazy.Encoding (encodeUtf8)+import System.FilePath.Posix (makeRelative)+import Data.List (any, nub, intercalate)+import System.FilePath.Glob (match, simplify, compile, Pattern)+import Text.EDE (eitherRender, eitherParseFile, fromPairs)+import System.Directory.PathWalk (pathWalkInterruptible, WalkStatus(..))+import System.Directory (doesFileExist, createDirectoryIfMissing)+import System.FilePath (joinPath, takeDirectory, normalise, isDrive, isValid, pathSeparator)++-- | Get only the template name+getTemplateName :: Template -> String+getTemplateName (Template name _) = name++-- | Get only the template path+getTemplatePath :: Template -> FilePath+getTemplatePath (Template _ path) = path++-- | Get the template name by its path+getTemplatePathByName :: [Template] -> String -> FilePath+getTemplatePathByName [] [] = ""+getTemplatePathByName [] _ = ""+getTemplatePathByName (x:xs) name =+  if templateName == name then+    templatePath+  else+    getTemplatePathByName xs name+  where+    templateName = getTemplateName x+    templatePath = getTemplatePath x++-- | File where users can store all the project creation questions+projectDataFile :: String+projectDataFile = "project.yml"++ignoreFiles :: [FilePath]+ignoreFiles = [".gitignore", "svnignore.txt"]++knownIgnoredFiles :: [Pattern]+knownIgnoredFiles = globbifyList [".git", ".svn", projectDataFile]++-- | Check if the path contains the questionnaire file+hasPathQuestionnaire :: FilePath -> IO Bool+hasPathQuestionnaire = doesFileExist . questionnaireFileByPath++-- | Get the questionnaire file by project path+questionnaireFileByPath :: FilePath -> FilePath+questionnaireFileByPath path = joinPath [path, projectDataFile]++-- | Start the template creation+createFilesFromTemplate :: FilePath -> [(Text, Value)] -> IO ()+createFilesFromTemplate root = walk knownIgnoredFiles root ""++-- | Walk recursively a folder copying its files using+walk :: [Pattern] -> FilePath -> FilePath -> [(Text, Value)] -> IO ()+walk currentBlacklist templateRoot currentPath responses =+  pathWalkInterruptible absolutePath $ \_ dirs files -> do+    blacklist <- getBlacklist+    render templateRoot relativePath files blacklist responses+    mapM_ (\f ->+        walk blacklist templateRoot (joinPath [relativePath, f]) responses+      )+      (whitelist dirs blacklist)+    return StopRecursing+    where+      absolutePath = joinPath [templateRoot, currentPath]+      relativePath = makeRelative templateRoot currentPath+      getBlacklist = do+        newBlacklist <- populateBlacklist absolutePath+        return $ currentBlacklist ++ newBlacklist+      whitelist dirs blacklist = [+          f | f <- dirs,+          not $ isInBlacklist f blacklist+        ]++-- | Add eventually new ignored files to the blacklist map+populateBlacklist :: FilePath -> IO [Pattern]+populateBlacklist root = getIgnoredPatterns (map (\f -> joinPath [root, f]) ignoreFiles)++-- | Parse a directory copying the files found into the current one+-- | where rob was called, it will also render eventually the answers to the questionnaire+-- | if template token will be found in any of the files+render :: FilePath -> FilePath -> [FilePath] -> [Pattern] -> [(Text, Value)] -> IO()+render templateRoot path files blacklist responses = do+  createDirectoryIfMissing True path+  forM_ files $ \file ->+    unless (isInBlacklist file blacklist) $ do+      let fileAbsolutePath = joinPath [templateRoot, path, file]+          fileRelativePath = joinPath [path, file]+      success $ fileCreated fileRelativePath+      template <- eitherParseFile fileAbsolutePath+      case template of+        Right t ->+          case eitherRender t templateData of+            Right res -> BS.writeFile fileRelativePath (encodeUtf8 res)+            Left e -> fallback e fileAbsolutePath fileRelativePath+        Left e -> fallback e fileAbsolutePath fileRelativePath+  where+    templateData = fromPairs responses+    fallback e inPath outPath = do+      warning parserError+      putStrLn e+      file <- BS.readFile inPath+      BS.writeFile outPath file++-- | Check whether a path is blacklisted looking it up in the blacklist map+-- | here basically we try to emulate the gitignore behavior recursively+isInBlacklist :: FilePath -> [Pattern] -> Bool+isInBlacklist path = any (`match` path)++-- | Get all the files to ignore uniquelly from a list of known .ignore files+getIgnoredPatterns :: [FilePath] -> IO [Pattern]+getIgnoredPatterns files = do+  res <- mapM findIgnoredFilesList files+  return $ nub $ intercalate [] res++-- | Figure out which files must be ignored reading them from the .gitignore+findIgnoredFilesList :: FilePath -> IO [Pattern]+findIgnoredFilesList f = do+  hasFile <- doesFileExist f+  if hasFile then do+    file <- readFile f+    return $ (+        globbifyList .+        extendIgnoredFiles .+        removeSeparatorPrefix .+        cleanList+      ) $ lines file+  else return []+  where+    cleanList list = (\l -> (not . null) l && head l /= '#') `filter` list++-- | Remove the initial separator prefix+removeSeparatorPrefix :: [FilePath] -> [FilePath]+removeSeparatorPrefix = map $ \p -> if head p == pathSeparator then tail p else p+++-- | Extend the ignored files in order to enhance the patterns matching+-- | for example with the "/node_modules/*" pattern we will add also "/node_modules"+-- | to the excluded folders+extendIgnoredFiles :: [FilePath] -> [FilePath]+extendIgnoredFiles (x:xs) =+    if (not . null) extension then+      x : extension : extendIgnoredFiles xs+    else x : extendIgnoredFiles xs+  where+    extension = if all (\t -> t dirName) tests then dirName else []+    dirName   = (takeDirectory . normalise) x+    tests = [+        isValid,+        not . isDrive,+        not . isDot,+        not . isWildCard,+        not . isDoubleWildCard+      ]+extendIgnoredFiles [] = []++-- | Helpers to enhance the ignored files+isDot :: FilePath -> Bool+isDot = (==) "."++isWildCard :: FilePath -> Bool+isWildCard = (==) "*"++isDoubleWildCard :: FilePath -> Bool+isDoubleWildCard = (==) "**"++-- | Map a list of file paths to glob patterns+globbifyList :: [FilePath] -> [Pattern]+globbifyList = map (simplify . compile)
+ src/Rob/Questionnaire.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE NamedFieldPuns #-}+module Rob.Questionnaire where+import Rob.Types+import Rob.Project (questionnaireFileByPath, hasPathQuestionnaire)+import Rob.UserMessages (unableToParseQuestionnaire, projectQuestionnaireMissing)+import qualified Rob.Logger as Logger++import System.Exit (exitFailure)+import FortyTwo+import Data.Yaml+import Data.Text (Text, pack)+import Data.HashMap.Strict (toList)+import qualified Data.Vector as V++-- | Get only the questions out of a questionnaier data struct as list+getQuestions :: Questionnaire -> [(Text, Question)]+getQuestions Questionnaire { questions } = toList questions++-- | Run the questionnaire+run :: FilePath -> IO [(Text, Value)]+run path = do+  hasQuestionnaireFile <- hasPathQuestionnaire path+  if hasQuestionnaireFile then do+    questionnaire <- decodeFileEither questionnaireFile+    case questionnaire of+      Right q -> mapM mapQuestion (getQuestions q)+      Left e -> do+        Logger.err unableToParseQuestionnaire+        Logger.warning $ show e+        exitFailure+  else do+    Logger.err $ projectQuestionnaireMissing path+    exitFailure++  where+    questionnaireFile = questionnaireFileByPath path++-- Map all the questions to answers+mapQuestion :: (Text, Question) -> IO (Text, Value)+mapQuestion (key, q) = do+  answer <- ask q+  return (key, answer)++-- Ask a Question to get a response from the user+ask :: Question -> IO Value+ask (PasswordQuestion question) = do+  res <- password question+  return (String $ pack res)+ask (SimpleQuestion question defaultValue) = do+  res <- inputWithDefault question defaultValue+  return (String $ pack res)+ask (SelectQuestion question answers defaultValue) = do+  res <- selectWithDefault question answers defaultValue+  return (String $ pack res)+ask (ConfirmQuestion question defaultValue) = do+  res <- confirmWithDefault question defaultValue+  return (Bool res)+ask (MultiselectQuestion question answers defaultValues) = do+  res <- multiselectWithDefault question answers defaultValues+  return (Array $ V.fromList $ map (String . pack) res)
+ src/Rob/Tasks.hs view
@@ -0,0 +1,32 @@+module Rob.Tasks where++import qualified Rob.Package as Package+import Rob.UserMessages (newTaskHelp, addTaskHelp, listTaskHelp, removeTaskHelp)+import qualified Rob.Types as Types (Task(..))++import System.Console.CmdArgs++list :: Types.Task+list = Types.List &= help listTaskHelp++-- | New task factory function+new :: Types.Task+new = Types.New &= help newTaskHelp++-- | New task factory function+remove :: Types.Task+remove = Types.Remove &= help removeTaskHelp++-- | Add task factory function+add :: Types.Task+add = Types.Add {+    Types.name = def &= typ "template-name" &= argPos 0,+    Types.path = def &= typ "path/to/the/template/folder" &= argPos 1+  } &= help addTaskHelp++-- | Export all the command line modes+mode :: Mode (CmdArgs Types.Task)+mode = cmdArgsMode $ modes [new, add, remove, list]+     &= help Package.description+     &= program Package.name+     &= summary (unwords [Package.name, "- v", Package.version, Package.author])
+ src/Rob/Types.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE DeriveGeneric, DuplicateRecordFields, DeriveDataTypeable, OverloadedStrings #-}++module Rob.Types where++import GHC.Generics (Generic(..))+import Data.HashMap.Strict (HashMap)+import System.FilePath.Glob (Pattern)+import Data.Text (Text)+import Data.Yaml+import System.Console.CmdArgs++-- | Task struct listing all the available actions+data Task+  = Add {+    name :: String,+    path :: FilePath+  }+  | List+  | Remove+  | New deriving (Data, Typeable, Show)++-- | Template name + path+data Template = Template String FilePath deriving (Generic)++instance Show Template where+  show (Template name path) = unlines [+      "- Name: " ++ name,+      "  Path: " ++ path+    ]++instance Eq Template where+  (Template nameA _) == (Template nameB _) = nameA == nameB++instance FromJSON Template+instance ToJSON Template++-- | Config file struct+newtype Config = Config {+  templates :: [Template]+} deriving (Generic, Show, Eq)++instance FromJSON Config+instance ToJSON Config++-- | Question struct+data Question =+    SimpleQuestion String String+  | PasswordQuestion String+  | ConfirmQuestion String Bool+  | SelectQuestion String [String] String+  | MultiselectQuestion String [String] [String]+  deriving (Show, Eq)++instance FromJSON Question where+  parseJSON (Object v) = do+    questionType <- v .:? "type" .!= "simple"+    parseJSON (Object v) >>= parseQuestion questionType++parseQuestion :: String -> Value -> Parser Question+parseQuestion questionType =+  case questionType of+    "confirm" -> withObject "ConfirmQuestion"+      (\o -> ConfirmQuestion <$> o .: questionKey <*> o .:? defaultKey .!= False)+    "select" -> withObject "SelectQuestion"+      (\o -> SelectQuestion <$> o .: questionKey <*> o .:? answersKey .!= [] <*> o .:? defaultKey .!= "")+    "multiselect" -> withObject "MultiselectQuestion"+      (\o -> MultiselectQuestion <$> o .: questionKey <*> o .:? answersKey .!= [] <*> o .:? defaultKey .!= [])+    "password" -> withObject "Password"+      (\o -> PasswordQuestion <$> o .: questionKey)+    _ -> withObject "SimpleQuestion"+      (\o -> SimpleQuestion <$> o .: questionKey <*> o .:? defaultKey .!= "")+  where+    questionKey = "question"+    answersKey = "answers"+    defaultKey = "default"++-- | Questionnaiere struct+newtype Questionnaire = Questionnaire {+  questions :: HashMap Text Question+} deriving (Generic, Show, Eq)++instance FromJSON Questionnaire++type Blacklist = [(FilePath, [Pattern])]
+ src/Rob/UserMessages.hs view
@@ -0,0 +1,99 @@+module Rob.UserMessages where++import qualified Rob.Package as Package++-- Questions messages++choseATemplate :: String+choseATemplate = "Which template do you want to use?"++choseATemplateToDelete :: String+choseATemplateToDelete = "Which template do you want to delete?"++-- Config messages++configFileFound :: FilePath -> String+configFileFound path = unwords [path, "was found!"]++noConfigFileFound :: String -> String+noConfigFileFound configFileName = unwords [+    "No",+    configFileName,+    "file was found in your $HOME path"+  ]++configFileCreated :: FilePath -> [String]+configFileCreated path = [+    "Creating a new config file in:",+    path+  ]++-- Tasks help++newTaskHelp :: String+newTaskHelp = "Create a new project in the current folder"++addTaskHelp :: String+addTaskHelp = "Add a new project template"++listTaskHelp :: String+listTaskHelp = "List all the available projects templates"++removeTaskHelp :: String+removeTaskHelp = "Remove a template from the list of the ones available"++-- Errors++noTemplatesAvailable :: String+noTemplatesAvailable = "It was not possible to find any project template. Have you ever tried adding one?"++noTemplateSelected :: String+noTemplateSelected = "No template selected. Please select one the list"++tryAddingATemplate :: String+tryAddingATemplate = "Try: " ++ Package.name ++ " " ++ "new template-name path/to/the/template/folder"++unableToParseQuestionnaire :: String+unableToParseQuestionnaire = "It was not able to parse the project questionnaire"+++projectPathDoesNotExist :: FilePath -> String+projectPathDoesNotExist path = unwords [+    "The path to",+    "\"" ++ path ++ "\"",+    "does not exist!"+  ]++projectQuestionnaireMissing :: FilePath -> String+projectQuestionnaireMissing path = unwords [+    "It was not possible to find any project.yml in",+    "\"" ++ path ++ "\"",+    "Please make sure to have a questionnaire project.yml in your template!"+  ]++projectAdded :: String -> String+projectAdded name = unwords [+    name,+    "was added to your projects templates!"+  ]++parserError :: String+parserError = "There was a parser error, this file will only be copied"++-- General info++availableTemplates :: String+availableTemplates = "Available Templates:"++-- Success messages++projectSuccessfullyCreated :: String+projectSuccessfullyCreated = "Your project was successfully created"++fileCreated :: String -> String+fileCreated file = unwords ["Create:", file]++-- Helpers++emptyString :: String+emptyString = ""
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"