{-# LANGUAGE DeriveDataTypeable #-} module Hpack.Util ( List(..) , toModule , getFilesRecursive , tryReadFile , sniffAlignment , extractFieldOrderHint , expandGlobs , sort , lexicographically -- exported for testing , splitField ) where import Control.Applicative import Control.DeepSeq import Control.Exception import Control.Monad import Data.Aeson.Types import Data.Char import Data.Data import Data.List hiding (sort) import Data.Maybe import Data.Ord import System.Directory import System.FilePath import System.FilePath.Glob sort :: [String] -> [String] sort = sortBy (comparing lexicographically) lexicographically :: String -> (String, String) lexicographically x = (map toLower x, x) newtype List a = List {fromList :: [a]} deriving (Eq, Show, Data, Typeable) instance FromJSON a => FromJSON (List a) where parseJSON v = List <$> (parseJSON v <|> (return <$> parseJSON v)) toModule :: [FilePath] -> Maybe String toModule path = case reverse path of [] -> Nothing x : xs -> do m <- stripSuffix ".hs" x <|> stripSuffix ".lhs" x let name = reverse (m : xs) guard (all isValidModuleName name) >> return (intercalate "." name) where stripSuffix :: String -> String -> Maybe String stripSuffix suffix x = reverse <$> stripPrefix (reverse suffix) (reverse x) -- See `Cabal.Distribution.ModuleName` (http://git.io/bj34) isValidModuleName :: String -> Bool isValidModuleName [] = False isValidModuleName (c:cs) = isUpper c && all isValidModuleChar cs isValidModuleChar :: Char -> Bool isValidModuleChar c = isAlphaNum c || c == '_' || c == '\'' getFilesRecursive :: FilePath -> IO [[String]] getFilesRecursive baseDir = go [] where go :: [FilePath] -> IO [[FilePath]] go dir = do c <- map ((dir ++) . return) . filter (`notElem` [".", ".."]) <$> getDirectoryContents (pathTo dir) subdirsFiles <- filterM (doesDirectoryExist . pathTo) c >>= mapM go files <- filterM (doesFileExist . pathTo) c return (files ++ concat subdirsFiles) where pathTo :: [FilePath] -> FilePath pathTo p = baseDir joinPath p tryReadFile :: FilePath -> IO (Maybe String) tryReadFile file = do r <- try (readFile file) :: IO (Either IOException String) return $!! either (const Nothing) Just r extractFieldOrderHint :: String -> [String] extractFieldOrderHint = map fst . catMaybes . map splitField . lines sniffAlignment :: String -> Maybe Int sniffAlignment input = case nub . catMaybes . map indentation . catMaybes . map splitField $ lines input of [n] -> Just n _ -> Nothing where indentation :: (String, String) -> Maybe Int indentation (name, value) = case span isSpace value of (_, "") -> Nothing (xs, _) -> (Just . succ . length $ name ++ xs) splitField :: String -> Maybe (String, String) splitField field = case span isNameChar field of (xs, ':':ys) -> Just (xs, ys) _ -> Nothing where isNameChar = (`elem` nameChars) nameChars = ['a'..'z'] ++ ['A'..'Z'] ++ "-" expandGlobs :: [String] -> IO ([String], [FilePath]) expandGlobs patterns = do files <- (fst <$> globDir compiledPatterns ".") >>= mapM removeDirectories let warnings = [warn pattern | ([], pattern) <- zip files patterns] return (warnings, combineResults files) where combineResults = nub . map (makeRelative ".") . sort . concat warn pattern = "Specified pattern " ++ show pattern ++ " for extra-source-files does not match any files" compiledPatterns = map (compileWith options) patterns removeDirectories = filterM doesFileExist options = CompOptions { characterClasses = False , characterRanges = False , numberRanges = False , wildcards = True , recursiveWildcards = True , pathSepInRanges = False , errorRecovery = True }