headroom-0.2.2.0: src/Headroom/Types.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-|
Module : Headroom.Types
Description : Application data types
Copyright : (c) 2019-2020 Vaclav Svejcar
License : BSD-3-Clause
Maintainer : vaclav.svejcar@gmail.com
Stability : experimental
Portability : POSIX
Module containing most of the data types used by the application.
-}
module Headroom.Types
(
-- * Configuration Data Types
-- ** Total Configuration
Configuration(..)
, HeaderConfig(..)
, HeadersConfig(..)
-- ** Partial Configuration
, PartialConfiguration(..)
, PartialHeaderConfig(..)
, PartialHeadersConfig(..)
-- ** Other Configuration Data Types
, HeaderSyntax(..)
-- * Command Data Types
, Command(..)
, CommandGenOptions(..)
, CommandInitOptions(..)
, CommandRunOptions(..)
, ConfigurationError(..)
, RunAction(..)
, RunMode(..)
, GenMode(..)
-- * Error Data Types
, ApplicationError(..)
, CommandGenError(..)
, CommandInitError(..)
, TemplateError(..)
-- * Other Data Types
, LicenseType(..)
, FileType(..)
, FileInfo(..)
)
where
import Control.Exception ( throw )
import Data.Aeson ( FromJSON(..)
, Value(String)
, genericParseJSON
, withObject
, (.!=)
, (.:?)
)
import Data.Monoid ( Last(..) )
import Headroom.Data.EnumExtra ( EnumExtra(..) )
import Headroom.Serialization ( aesonOptions )
import RIO
import qualified RIO.Text as T
-- | Action to be performed based on the selected 'RunMode'.
data RunAction = RunAction
{ raProcessed :: !Bool -- ^ whether the given file was processed
, raFunc :: !(Text -> Text) -- ^ function to process the file
, raProcessedMsg :: !Text -- ^ message to show when file was processed
, raSkippedMsg :: !Text -- ^ message to show when file was skipped
}
-- | Represents what action should the @run@ command perform.
data RunMode
= Add -- ^ /add mode/ for @run@ command
| Check -- ^ /check mode/ for @run@ command
| Drop -- ^ /drop mode/ for @run@ command
| Replace -- ^ /replace mode/ for @run@ command
deriving (Eq, Show)
instance FromJSON RunMode where
parseJSON = \case
String s -> case T.toLower s of
"add" -> pure Add
"check" -> pure Check
"drop" -> pure Drop
"replace" -> pure Replace
_ -> error $ "Unknown run mode: " <> T.unpack s
other -> error $ "Invalid value for run mode: " <> show other
-- | Represents what action should the @gen@ command perform.
data GenMode
= GenConfigFile -- ^ generate /YAML/ config file stub
| GenLicense !(LicenseType, FileType) -- ^ generate license header template
deriving (Eq, Show)
-- | Represents error that can occur during the application execution.
data ApplicationError
= CommandGenError !CommandGenError -- ^ error specific for the @gen@ command
| CommandInitError !CommandInitError -- ^ error specific for the @init@ command
| ConfigurationError !ConfigurationError -- ^ error processing configuration
| TemplateError !TemplateError -- ^ error processing template
deriving (Eq, Show)
instance Exception ApplicationError where
displayException = T.unpack . \case
CommandGenError error' -> commandGenError error'
CommandInitError error' -> commandInitError error'
ConfigurationError error' -> configurationError error'
TemplateError error' -> templateError error'
-- | Error specific for the @gen@ command.
data CommandGenError = NoGenModeSelected -- ^ no mode of /Gen/ command selected
deriving (Eq, Show)
-- | Error specific for the @init@ command.
data CommandInitError
= AppConfigAlreadyExists !FilePath -- ^ application configuration file already exists
| NoProvidedSourcePaths -- ^ no paths to source code files provided
| NoSupportedFileType -- ^ no supported file types found on source paths
deriving (Eq, Show)
-- | Error during processing configuration.
data ConfigurationError
= InvalidVariable !Text -- ^ invalid variable input (as @key=value@)
| MixedHeaderSyntax -- ^ illegal configuration for 'HeaderSyntax'
| NoFileExtensions !FileType -- ^ no configuration for @file-extensions@
| NoHeaderSyntax !FileType -- ^ no configuration for header syntax
| NoMarginAfter !FileType -- ^ no configuration for @margin-after@
| NoMarginBefore !FileType -- ^ no configuration for @margin-before@
| NoPutAfter !FileType -- ^ no configuration for @put-after@
| NoPutBefore !FileType -- ^ no configuration for @put-before@
| NoRunMode -- ^ no configuration for @run-mode@
| NoSourcePaths -- ^ no configuration for @source-paths@
| NoExcludedPaths -- ^ no configuration for @excluded-paths@
| NoTemplatePaths -- ^ no configuration for @template-paths@
| NoVariables -- ^ no configuration for @variables@
deriving (Eq, Show)
-- | Error during processing template.
data TemplateError
= MissingVariables !Text ![Text] -- ^ missing variable values
| ParseError !Text -- ^ error parsing raw template text
deriving (Eq, Show)
--------------------------------------------------------------------------------
-- | Application command.
data Command
= Run [FilePath] [Text] [FilePath] [Text] (Maybe RunMode) Bool Bool -- ^ @run@ command
| Gen Bool (Maybe (LicenseType, FileType)) -- ^ @gen@ command
| Init LicenseType [FilePath] -- ^ @init@ command
deriving (Show)
--------------------------------------------------------------------------------
-- | Options for the @gen@ command.
newtype CommandGenOptions = CommandGenOptions
{ cgoGenMode :: GenMode -- ^ selected mode
}
deriving (Show)
-- | Options for the @init@ command.
data CommandInitOptions = CommandInitOptions
{ cioSourcePaths :: ![FilePath] -- ^ paths to source code files
, cioLicenseType :: !LicenseType -- ^ license type
}
deriving Show
-- | Options for the @run@ command.
data CommandRunOptions = CommandRunOptions
{ croRunMode :: !(Maybe RunMode) -- ^ used /Run/ command mode
, croSourcePaths :: ![FilePath] -- ^ source code file paths
, croExcludedPaths :: ![Text] -- ^ source paths to exclude
, croTemplatePaths :: ![FilePath] -- ^ template file paths
, croVariables :: ![Text] -- ^ raw variables
, croDebug :: !Bool -- ^ whether to run in debug mode
, croDryRun :: !Bool -- ^ whether to perform dry run
}
deriving (Eq, Show)
--------------------------------------------------------------------------------
-- | Supported type of source code file.
data FileType
= C -- ^ support for /C/ programming language
| CPP -- ^ support for /C++/ programming language
| CSS -- ^ support for /CSS/
| Haskell -- ^ support for /Haskell/ programming language
| HTML -- ^ support for /HTML/
| Java -- ^ support for /Java/ programming language
| JS -- ^ support for /JavaScript/ programming language
| Rust -- ^ support for /Rust/ programming language
| Scala -- ^ support for /Scala/ programming language
| Shell -- ^ support for /Shell/
deriving (Bounded, Enum, EnumExtra, Eq, Ord, Show)
--------------------------------------------------------------------------------
-- | Supported type of open source license.
data LicenseType
= Apache2 -- ^ support for /Apache-2.0/ license
| BSD3 -- ^ support for /BSD-3-Clause/ license
| GPL2 -- ^ support for /GNU GPL2/ license
| GPL3 -- ^ support for /GNU GPL3/ license
| MIT -- ^ support for /MIT/ license
| MPL2 -- ^ support for /MPL2/ license
deriving (Bounded, Enum, EnumExtra, Eq, Ord, Show)
--------------------------------------------------------------------------------
-- | Info extracted about the concrete source code file.
data FileInfo = FileInfo
{ fiFileType :: !FileType -- ^ type of the file
, fiHeaderConfig :: !HeaderConfig -- ^ configuration for license header
, fiHeaderPos :: !(Maybe (Int, Int)) -- ^ position of existing license header
, fiVariables :: !(HashMap Text Text) -- ^ additional extracted variables
}
deriving (Eq, Show)
--------------------------------------------------------------------------------
-- | Syntax of the license header comment.
data HeaderSyntax
= BlockComment !Text !Text -- ^ block (multi-line) comment syntax (e.g. @/* */@)
| LineComment !Text -- ^ single line comment syntax (e.g. @//@)
deriving (Eq, Show)
-- | Application configuration.
data Configuration = Configuration
{ cRunMode :: !RunMode -- ^ mode of the @run@ command
, cSourcePaths :: ![FilePath] -- ^ paths to source code files
, cExcludedPaths :: ![Text] -- ^ excluded source paths
, cTemplatePaths :: ![FilePath] -- ^ paths to template files
, cVariables :: !(HashMap Text Text) -- ^ variable values for templates
, cLicenseHeaders :: !HeadersConfig -- ^ configuration of license headers
}
deriving (Eq, Show)
-- | Configuration for specific license header.
data HeaderConfig = HeaderConfig
{ hcFileExtensions :: ![Text] -- ^ list of file extensions (without dot)
, hcMarginAfter :: !Int -- ^ number of empty lines to put after header
, hcMarginBefore :: !Int -- ^ number of empty lines to put before header
, hcPutAfter :: ![Text] -- ^ /regexp/ patterns after which to put the header
, hcPutBefore :: ![Text] -- ^ /regexp/ patterns before which to put the header
, hcHeaderSyntax :: !HeaderSyntax -- ^ syntax of the license header comment
}
deriving (Eq, Show)
-- | Group of 'HeaderConfig' configurations for supported file types.
data HeadersConfig = HeadersConfig
{ hscC :: !HeaderConfig -- ^ configuration for /C/ programming language
, hscCpp :: !HeaderConfig -- ^ configuration for /C++/ programming language
, hscCss :: !HeaderConfig -- ^ configuration for /CSS/
, hscHaskell :: !HeaderConfig -- ^ configuration for /Haskell/ programming language
, hscHtml :: !HeaderConfig -- ^ configuration for /HTML/
, hscJava :: !HeaderConfig -- ^ configuration for /Java/ programming language
, hscJs :: !HeaderConfig -- ^ configuration for /JavaScript/ programming language
, hscRust :: !HeaderConfig -- ^ configuration for /Rust/ programming language
, hscScala :: !HeaderConfig -- ^ configuration for /Scala/ programming language
, hscShell :: !HeaderConfig -- ^ configuration for /Shell/
}
deriving (Eq, Show)
--------------------------------------------------------------------------------
-- | Internal representation of block comment style of 'HeaderSyntax'.
data BlockComment' = BlockComment'
{ bcStartsWith :: !Text -- ^ starting pattern (e.g. @/*@)
, bcEndsWith :: !Text -- ^ ending pattern (e.g. @*/@)
}
deriving (Eq, Generic, Show)
-- | Internal representation of the line style of 'HeaderSyntax'.
newtype LineComment' = LineComment'
{ lcPrefixedBy :: Text -- ^ prefix of the comment line (e.g. @//@)
}
deriving (Eq, Generic, Show)
-- | Partial (possibly incomplete) version of 'Configuration'.
data PartialConfiguration = PartialConfiguration
{ pcRunMode :: !(Last RunMode) -- ^ mode of the @run@ command
, pcSourcePaths :: !(Last [FilePath]) -- ^ paths to source code files
, pcExcludedPaths :: !(Last [Text]) -- ^ excluded source paths
, pcTemplatePaths :: !(Last [FilePath]) -- ^ paths to template files
, pcVariables :: !(Last (HashMap Text Text)) -- ^ variable values for templates
, pcLicenseHeaders :: !PartialHeadersConfig -- ^ configuration of license headers
}
deriving (Eq, Generic, Show)
-- | Partial (possibly incomplete) version of 'HeaderConfig'.
data PartialHeaderConfig = PartialHeaderConfig
{ phcFileExtensions :: !(Last [Text]) -- ^ list of file extensions (without dot)
, phcMarginAfter :: !(Last Int) -- ^ number of empty lines to put after header
, phcMarginBefore :: !(Last Int) -- ^ number of empty lines to put before header
, phcPutAfter :: !(Last [Text]) -- ^ /regexp/ patterns after which to put the header
, phcPutBefore :: !(Last [Text]) -- ^ /regexp/ patterns before which to put the header
, phcHeaderSyntax :: !(Last HeaderSyntax) -- ^ syntax of the license header comment
}
deriving (Eq, Generic, Show)
-- | Partial (possibly incomplete) version of 'HeadersConfig'.
data PartialHeadersConfig = PartialHeadersConfig
{ phscC :: !PartialHeaderConfig -- ^ configuration for /C/ programming language
, phscCpp :: !PartialHeaderConfig -- ^ configuration for /C++/ programming language
, phscCss :: !PartialHeaderConfig -- ^ configuration for /CSS/
, phscHaskell :: !PartialHeaderConfig -- ^ configuration for /Haskell/ programming language
, phscHtml :: !PartialHeaderConfig -- ^ configuration for /HTML/
, phscJava :: !PartialHeaderConfig -- ^ configuration for /Java/ programming language
, phscJs :: !PartialHeaderConfig -- ^ configuration for /JavaScript/ programming language
, phscRust :: !PartialHeaderConfig -- ^ configuration for /Rust/ programming language
, phscScala :: !PartialHeaderConfig -- ^ configuration for /Scala/ programming language
, phscShell :: !PartialHeaderConfig -- ^ configuration for /Shell/
}
deriving (Eq, Generic, Show)
instance FromJSON BlockComment' where
parseJSON = genericParseJSON aesonOptions
instance FromJSON LineComment' where
parseJSON = genericParseJSON aesonOptions
instance FromJSON PartialConfiguration where
parseJSON = withObject "PartialConfiguration" $ \obj -> do
pcRunMode <- Last <$> obj .:? "run-mode"
pcSourcePaths <- Last <$> obj .:? "source-paths"
pcExcludedPaths <- Last <$> obj .:? "excluded-paths"
pcTemplatePaths <- Last <$> obj .:? "template-paths"
pcVariables <- Last <$> obj .:? "variables"
pcLicenseHeaders <- obj .:? "license-headers" .!= mempty
pure PartialConfiguration { .. }
instance FromJSON PartialHeaderConfig where
parseJSON = withObject "PartialHeaderConfig" $ \obj -> do
phcFileExtensions <- Last <$> obj .:? "file-extensions"
phcMarginAfter <- Last <$> obj .:? "margin-after"
phcMarginBefore <- Last <$> obj .:? "margin-before"
phcPutAfter <- Last <$> obj .:? "put-after"
phcPutBefore <- Last <$> obj .:? "put-before"
blockComment <- obj .:? "block-comment"
lineComment <- obj .:? "line-comment"
let phcHeaderSyntax = Last $ headerSyntax blockComment lineComment
pure PartialHeaderConfig { .. }
where
headerSyntax (Just (BlockComment' s e)) Nothing = Just $ BlockComment s e
headerSyntax Nothing (Just (LineComment' p)) = Just $ LineComment p
headerSyntax Nothing Nothing = Nothing
headerSyntax _ _ = throw error'
error' = ConfigurationError MixedHeaderSyntax
instance FromJSON PartialHeadersConfig where
parseJSON = withObject "PartialHeadersConfig" $ \obj -> do
phscC <- obj .:? "c" .!= mempty
phscCpp <- obj .:? "cpp" .!= mempty
phscCss <- obj .:? "css" .!= mempty
phscHaskell <- obj .:? "haskell" .!= mempty
phscHtml <- obj .:? "html" .!= mempty
phscJava <- obj .:? "java" .!= mempty
phscJs <- obj .:? "js" .!= mempty
phscRust <- obj .:? "rust" .!= mempty
phscScala <- obj .:? "scala" .!= mempty
phscShell <- obj .:? "shell" .!= mempty
pure PartialHeadersConfig { .. }
instance Semigroup PartialConfiguration where
x <> y = PartialConfiguration
{ pcRunMode = pcRunMode x <> pcRunMode y
, pcSourcePaths = pcSourcePaths x <> pcSourcePaths y
, pcExcludedPaths = pcExcludedPaths x <> pcExcludedPaths y
, pcTemplatePaths = pcTemplatePaths x <> pcTemplatePaths y
, pcVariables = pcVariables x <> pcVariables y
, pcLicenseHeaders = pcLicenseHeaders x <> pcLicenseHeaders y
}
instance Semigroup PartialHeaderConfig where
x <> y = PartialHeaderConfig
{ phcFileExtensions = phcFileExtensions x <> phcFileExtensions y
, phcMarginAfter = phcMarginAfter x <> phcMarginAfter y
, phcMarginBefore = phcMarginBefore x <> phcMarginBefore y
, phcPutAfter = phcPutAfter x <> phcPutAfter y
, phcPutBefore = phcPutBefore x <> phcPutBefore y
, phcHeaderSyntax = phcHeaderSyntax x <> phcHeaderSyntax y
}
instance Semigroup PartialHeadersConfig where
x <> y = PartialHeadersConfig { phscC = phscC x <> phscC y
, phscCpp = phscCpp x <> phscCpp y
, phscCss = phscCss x <> phscCss y
, phscHaskell = phscHaskell x <> phscHaskell y
, phscHtml = phscHtml x <> phscHtml y
, phscJava = phscJava x <> phscJava y
, phscJs = phscJs x <> phscJs y
, phscRust = phscRust x <> phscRust y
, phscScala = phscScala x <> phscScala y
, phscShell = phscShell x <> phscShell y
}
instance Monoid PartialConfiguration where
mempty = PartialConfiguration mempty mempty mempty mempty mempty mempty
instance Monoid PartialHeaderConfig where
mempty = PartialHeaderConfig mempty mempty mempty mempty mempty mempty
instance Monoid PartialHeadersConfig where
mempty = PartialHeadersConfig mempty
mempty
mempty
mempty
mempty
mempty
mempty
mempty
mempty
mempty
--------------------------------------------------------------------------------
commandGenError :: CommandGenError -> Text
commandGenError = \case
NoGenModeSelected -> noGenModeSelected
where
noGenModeSelected = mconcat
[ "Please select at least one option what to generate "
, "(see --help for details)"
]
commandInitError :: CommandInitError -> Text
commandInitError = \case
AppConfigAlreadyExists path -> appConfigAlreadyExists path
NoProvidedSourcePaths -> noProvidedSourcePaths
NoSupportedFileType -> noSupportedFileType
where
appConfigAlreadyExists path =
mconcat ["Configuration file '", T.pack path, "' already exists"]
noProvidedSourcePaths = "No source code paths (files or directories) defined"
noSupportedFileType = "No supported file type found in scanned source paths"
configurationError :: ConfigurationError -> Text
configurationError = \case
InvalidVariable input -> invalidVariable input
MixedHeaderSyntax -> mixedHeaderSyntax
NoFileExtensions fileType -> noProp "file-extensions" fileType
NoHeaderSyntax fileType -> noProp "block-comment/line-comment" fileType
NoMarginAfter fileType -> noProp "margin-after" fileType
NoMarginBefore fileType -> noProp "margin-before" fileType
NoPutAfter fileType -> noProp "put-after" fileType
NoPutBefore fileType -> noProp "put-before" fileType
NoRunMode -> noFlag "run-mode"
NoSourcePaths -> noFlag "source-paths"
NoExcludedPaths -> noFlag "excluded-paths"
NoTemplatePaths -> noFlag "template-paths"
NoVariables -> noFlag "variables"
where
invalidVariable = ("Cannot parse variable key=value from: " <>)
noProp prop fileType = T.pack $ mconcat
["Missing '", prop, "' configuration key for file type", show fileType]
noFlag flag = mconcat ["Missing configuration key: ", flag]
mixedHeaderSyntax = mconcat
[ "Invalid configuration, combining 'block-comment' with 'line-comment' "
, "is not allowed. Either use 'block-comment' to define multi-line "
, "comment header, or 'line-comment' to define header composed of "
, "multiple single-line comments."
]
templateError :: TemplateError -> Text
templateError = \case
MissingVariables name variables -> missingVariables name variables
ParseError msg -> parseError msg
where
missingVariables name variables = mconcat
["Missing variables for template '", name, "': ", T.pack $ show variables]
parseError msg = "Error parsing template: " <> msg