hpack-0.2.0: src/Config.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Config (
readPackageConfig
, Package(..)
, Dependency
, GhcOption
, Library(..)
, Executable(..)
) where
import Prelude ()
import Prelude.Compat
import Control.Applicative
import Control.Monad.Compat
import Data.List (nub, sort, (\\))
import Data.String
import Data.Maybe
import Data.Yaml
import GHC.Generics
import Data.HashMap.Lazy (HashMap)
import qualified Data.HashMap.Lazy as Map
import qualified Data.Text as T
import System.FilePath
import System.Directory
import Data.Data
import Data.Aeson.Types
import Util
genericParseJSON_ :: forall a. (Typeable a, Generic a, GFromJSON (Rep a)) => Value -> Parser a
genericParseJSON_ = genericParseJSON defaultOptions {fieldLabelModifier = hyphenize name}
where
name = (tyConName . typeRepTyCon . typeRep) (Proxy :: Proxy a)
hyphenize :: String -> String -> String
hyphenize name = camelTo '-' . drop (length name)
data CaptureUnknownFields a = CaptureUnknownFields {
captureUnknownFieldsFields :: [String]
, captureUnknownFieldsValue :: a
} deriving (Eq, Show, Generic, Data, Typeable)
instance (Data a, FromJSON a) => FromJSON (CaptureUnknownFields a) where
parseJSON v = captureUnknownFields <$> parseJSON v
where
captureUnknownFields a = case v of
Object o -> CaptureUnknownFields unknown a
where
unknown = keys \\ fields
keys = map T.unpack (Map.keys o)
constr = toConstr a
name = showConstr constr
fields = map (hyphenize name) (constrFields constr)
_ -> CaptureUnknownFields [] a
data LibrarySection = LibrarySection {
librarySectionSourceDirs :: Maybe (List FilePath)
, librarySectionExposedModules :: Maybe (List String)
, librarySectionOtherModules :: Maybe (List String)
, librarySectionDependencies :: Maybe (List Dependency)
, librarySectionDefaultExtensions :: Maybe (List String)
, librarySectionGhcOptions :: Maybe (List GhcOption)
} deriving (Eq, Show, Generic, Data, Typeable)
instance FromJSON LibrarySection where
parseJSON = genericParseJSON_
data ExecutableSection = ExecutableSection {
executableSectionMain :: FilePath
, executableSectionSourceDirs :: Maybe (List FilePath)
, executableSectionOtherModules :: Maybe (List String)
, executableSectionDependencies :: Maybe (List Dependency)
, executableSectionDefaultExtensions :: Maybe (List String)
, executableSectionGhcOptions :: Maybe (List GhcOption)
} deriving (Eq, Show, Generic, Data, Typeable)
instance FromJSON ExecutableSection where
parseJSON = genericParseJSON_
data PackageConfig = PackageConfig {
packageConfigName :: Maybe String
, packageConfigVersion :: Maybe String
, packageConfigSynopsis :: Maybe String
, packageConfigDescription :: Maybe String
, packageConfigHomepage :: Maybe (Maybe String)
, packageConfigBugReports :: Maybe (Maybe String)
, packageConfigCategory :: Maybe String
, packageConfigStability :: Maybe String
, packageConfigAuthor :: Maybe (List String)
, packageConfigMaintainer :: Maybe (List String)
, packageConfigCopyright :: Maybe (List String)
, packageConfigLicense :: Maybe String
, packageConfigExtraSourceFiles :: Maybe (List FilePath)
, packageConfigGithub :: Maybe String
, packageConfigSourceDirs :: Maybe (List FilePath)
, packageConfigDependencies :: Maybe (List Dependency)
, packageConfigDefaultExtensions :: Maybe (List String)
, packageConfigGhcOptions :: Maybe (List GhcOption)
, packageConfigLibrary :: Maybe (CaptureUnknownFields LibrarySection)
, packageConfigExecutables :: Maybe (HashMap String (CaptureUnknownFields ExecutableSection))
, packageConfigTests :: Maybe (HashMap String (CaptureUnknownFields ExecutableSection))
} deriving (Eq, Show, Generic, Data, Typeable)
instance FromJSON PackageConfig where
parseJSON value = handleNullValues <$> genericParseJSON_ value
where
handleNullValues :: PackageConfig -> PackageConfig
handleNullValues =
ifNull "homepage" (\p -> p {packageConfigHomepage = Just Nothing})
. ifNull "bug-reports" (\p -> p {packageConfigBugReports = Just Nothing})
ifNull :: String -> (a -> a) -> a -> a
ifNull name f
| isNull name value = f
| otherwise = id
isNull :: String -> Value -> Bool
isNull name value = case parseMaybe p value of
Just Null -> True
_ -> False
where
p = parseJSON >=> (.: fromString name)
readPackageConfig :: FilePath -> IO (Either String ([String], Package))
readPackageConfig file = do
config <- decodeFileEither file
either (return . Left . errToString) (fmap Right . mkPackage) config
where
errToString err = file ++ case err of
AesonException e -> ": " ++ e
InvalidYaml (Just (YamlException s)) -> ": " ++ s
InvalidYaml (Just (YamlParseException{..})) -> ":" ++ show yamlLine ++ ":" ++ show yamlColumn ++ ": " ++ yamlProblem ++ " " ++ yamlContext
where YamlMark{..} = yamlProblemMark
_ -> ": " ++ show err
type Dependency = String
type GhcOption = String
data Package = Package {
packageName :: String
, packageVersion :: String
, packageSynopsis :: Maybe String
, packageDescription :: Maybe String
, packageHomepage :: Maybe String
, packageBugReports :: Maybe String
, packageCategory :: Maybe String
, packageStability :: Maybe String
, packageAuthor :: [String]
, packageMaintainer :: [String]
, packageCopyright :: [String]
, packageLicense :: Maybe String
, packageLicenseFile :: Maybe FilePath
, packageExtraSourceFiles :: [FilePath]
, packageSourceRepository :: Maybe String
, packageLibrary :: Maybe Library
, packageExecutables :: [Executable]
, packageTests :: [Executable]
} deriving (Eq, Show)
data Library = Library {
librarySourceDirs :: [FilePath]
, libraryExposedModules :: [String]
, libraryOtherModules :: [String]
, libraryDependencies :: [[Dependency]]
, libraryDefaultExtensions :: [String]
, libraryGhcOptions :: [GhcOption]
} deriving (Eq, Show)
data Executable = Executable {
executableName :: String
, executableMain :: FilePath
, executableSourceDirs :: [FilePath]
, executableOtherModules :: [String]
, executableDependencies :: [[Dependency]]
, executableDefaultExtensions :: [String]
, executableGhcOptions :: [GhcOption]
} deriving (Eq, Show)
mkPackage :: (CaptureUnknownFields PackageConfig) -> IO ([String], Package)
mkPackage (CaptureUnknownFields unknownFields PackageConfig{..}) = do
let dependencies = fromMaybeList packageConfigDependencies
sourceDirs = fromMaybeList packageConfigSourceDirs
defaultExtensions = fromMaybeList packageConfigDefaultExtensions
ghcOptions = fromMaybeList packageConfigGhcOptions
convert f = f sourceDirs dependencies defaultExtensions ghcOptions
mLibrary <- mapM (convert toLibrary) mLibrarySection
executables <- convert toExecutables mExecutableSections
tests <- convert toExecutables mTestSections
name <- maybe (takeBaseName <$> getCurrentDirectory) return packageConfigName
licenseFileExists <- doesFileExist "LICENSE"
missingSourceDirs <- nub . sort <$> filterM (fmap not <$> doesDirectoryExist) (
maybe [] librarySourceDirs mLibrary
++ concatMap executableSourceDirs executables
++ concatMap executableSourceDirs tests
)
let package = Package {
packageName = name
, packageVersion = fromMaybe "0.0.0" packageConfigVersion
, packageSynopsis = packageConfigSynopsis
, packageDescription = packageConfigDescription
, packageHomepage = homepage
, packageBugReports = bugReports
, packageCategory = packageConfigCategory
, packageStability = packageConfigStability
, packageAuthor = fromMaybeList packageConfigAuthor
, packageMaintainer = fromMaybeList packageConfigMaintainer
, packageCopyright = fromMaybeList packageConfigCopyright
, packageLicense = packageConfigLicense
, packageLicenseFile = guard licenseFileExists >> Just "LICENSE"
, packageExtraSourceFiles = fromMaybeList packageConfigExtraSourceFiles
, packageSourceRepository = github
, packageLibrary = mLibrary
, packageExecutables = executables
, packageTests = tests
}
warnings =
formatUnknownFields "package description" unknownFields
++ maybe [] (formatUnknownFields "library section") (captureUnknownFieldsFields <$> packageConfigLibrary)
++ formatUnknownSectionFields "executable" packageConfigExecutables
++ formatUnknownSectionFields "test" packageConfigTests
++ formatMissingSourceDirs missingSourceDirs
return (warnings, package)
where
mLibrarySection :: Maybe LibrarySection
mLibrarySection = captureUnknownFieldsValue <$> packageConfigLibrary
mExecutableSections :: Maybe (HashMap String ExecutableSection)
mExecutableSections = fmap captureUnknownFieldsValue <$> packageConfigExecutables
mTestSections :: Maybe (HashMap String ExecutableSection)
mTestSections = fmap captureUnknownFieldsValue <$> packageConfigTests
formatUnknownFields name = map f . sort
where
f field = "Ignoring unknown field " ++ show field ++ " in " ++ name
formatUnknownSectionFields sectionType = maybe [] (concatMap f . Map.toList . fmap captureUnknownFieldsFields)
where
f :: (String, [String]) -> [String]
f (section, fields) = formatUnknownFields (sectionType ++ " section " ++ show section) fields
formatMissingSourceDirs = map f
where
f name = "Specified source-dir " ++ show name ++ " does not exist"
github = ("https://github.com/" ++) <$> packageConfigGithub
homepage :: Maybe String
homepage = case packageConfigHomepage of
Just Nothing -> Nothing
_ -> join packageConfigHomepage <|> fromGithub
where
fromGithub = ((++ "#readme") <$> github)
bugReports :: Maybe String
bugReports = case packageConfigBugReports of
Just Nothing -> Nothing
_ -> join packageConfigBugReports <|> fromGithub
where
fromGithub = ((++ "/issues") <$> github)
toLibrary :: [FilePath] -> [Dependency] -> [String] -> [GhcOption] -> LibrarySection -> IO Library
toLibrary globalSourceDirs globalDependencies globalDefaultExtensions globalGhcOptions LibrarySection{..} = do
modules <- concat <$> mapM getModules sourceDirs
let (exposedModules, otherModules) = determineModules modules librarySectionExposedModules librarySectionOtherModules
return (Library sourceDirs exposedModules otherModules dependencies defaultExtensions ghcOptions)
where
sourceDirs = globalSourceDirs ++ fromMaybeList librarySectionSourceDirs
dependencies = filter (not . null) [globalDependencies, fromMaybeList librarySectionDependencies]
defaultExtensions = globalDefaultExtensions ++ fromMaybeList librarySectionDefaultExtensions
ghcOptions = globalGhcOptions ++ fromMaybeList librarySectionGhcOptions
determineModules :: [String] -> Maybe (List String) -> Maybe (List String) -> ([String], [String])
determineModules modules mExposedModules mOtherModules = case (mExposedModules, mOtherModules) of
(Nothing, Nothing) -> (modules, [])
_ -> (exposedModules, otherModules)
where
otherModules = maybe (modules \\ exposedModules) fromList mOtherModules
exposedModules = maybe (modules \\ otherModules) fromList mExposedModules
getModules :: FilePath -> IO [String]
getModules src = do
exits <- doesDirectoryExist src
if exits
then toModules <$> getFilesRecursive src
else return []
where
toModules :: [[FilePath]] -> [String]
toModules = catMaybes . map toModule
toExecutables :: [FilePath] -> [Dependency] -> [String] -> [GhcOption] -> Maybe (HashMap String ExecutableSection) -> IO [Executable]
toExecutables globalSourceDirs globalDependencies globalDefaultExtensions globalGhcOptions executables = (mapM toExecutable . Map.toList) (fromMaybe mempty executables)
where
toExecutable (name, ExecutableSection{..}) = do
modules <- maybe (filterMain . concat <$> mapM getModules sourceDirs) (return . fromList) executableSectionOtherModules
return $ Executable name executableSectionMain sourceDirs modules dependencies defaultExtensions ghcOptions
where
dependencies = filter (not . null) [globalDependencies, fromMaybeList executableSectionDependencies]
sourceDirs = globalSourceDirs ++ fromMaybeList executableSectionSourceDirs
defaultExtensions = globalDefaultExtensions ++ fromMaybeList executableSectionDefaultExtensions
ghcOptions = globalGhcOptions ++ fromMaybeList executableSectionGhcOptions
filterMain :: [String] -> [String]
filterMain = maybe id (filter . (/=)) (toModule $ splitDirectories executableSectionMain)
fromMaybeList :: Maybe (List a) -> [a]
fromMaybeList = maybe [] fromList