module Web.Bower.PackageMeta.Internal where
import Control.Applicative
import Control.Monad
import Control.Category ((>>>))
import Control.Monad.Error.Class (MonadError(..))
import Data.Monoid
import Data.List
import Data.Char
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.ByteString.Lazy as B
import Data.Aeson ((.=))
import qualified Data.Aeson as A
import qualified Data.Aeson.Types as Aeson
import Data.Aeson.BetterErrors
data PackageMeta = PackageMeta
{ bowerName :: PackageName
, bowerDescription :: Maybe String
, bowerMain :: [FilePath]
, bowerModuleType :: [ModuleType]
, bowerLicence :: [String]
, bowerIgnore :: [String]
, bowerKeywords :: [String]
, bowerAuthors :: [Author]
, bowerHomepage :: Maybe String
, bowerRepository :: Maybe Repository
, bowerDependencies :: [(PackageName, VersionRange)]
, bowerDevDependencies :: [(PackageName, VersionRange)]
, bowerResolutions :: [(PackageName, Version)]
, bowerPrivate :: Bool
}
deriving (Show, Eq, Ord)
newtype PackageName
= PackageName String
deriving (Show, Eq, Ord)
runPackageName :: PackageName -> String
runPackageName (PackageName s) = s
mkPackageName :: String -> Either PackageNameError PackageName
mkPackageName = fmap PackageName . validateAll validators
where
dashOrDot = ['-', '.']
validateAll vs x = mapM_ (validateWith x) vs >> return x
validateWith x (p, err)
| p x = Right x
| otherwise = Left (err x)
validChar c = isAscii c && (isLower c || isDigit c || c `elem` dashOrDot)
validators =
[ (not . null, const NotEmpty)
, (all validChar, InvalidChars . filter (not . validChar))
, (headMay >>> isJustAnd (`notElem` dashOrDot), const MustNotBeginSeparator)
, (lastMay >>> isJustAnd (`notElem` dashOrDot), const MustNotEndSeparator)
, (not . isInfixOf "--", const RepeatedSeparators)
, (not . isInfixOf "..", const RepeatedSeparators)
, (length >>> (<= 50), TooLong . length)
]
isJustAnd = maybe False
headMay :: [a] -> Maybe a
headMay [] = Nothing
headMay (x:_) = Just x
lastMay :: [a] -> Maybe a
lastMay [] = Nothing
lastMay [x] = Just x
lastMay (_:xs) = lastMay xs
data Author = Author
{ authorName :: String
, authorEmail :: Maybe String
, authorHomepage :: Maybe String
}
deriving (Show, Eq, Ord)
data ModuleType
= Globals
| AMD
| Node
| ES6
| YUI
deriving (Show, Eq, Ord, Enum, Bounded)
moduleTypes :: [(String, ModuleType)]
moduleTypes = map (\t -> (map toLower (show t), t)) [minBound .. maxBound]
data Repository = Repository
{ repositoryUrl :: String
, repositoryType :: String
}
deriving (Show, Eq, Ord)
newtype Version
= Version { runVersion :: String }
deriving (Show, Eq, Ord)
newtype VersionRange
= VersionRange { runVersionRange :: String }
deriving (Show, Eq, Ord)
data BowerError
= InvalidPackageName PackageNameError
| InvalidModuleType String
showBowerError :: BowerError -> Text
showBowerError (InvalidPackageName err) =
"Invalid package name: " <> showPackageNameError err
showBowerError (InvalidModuleType str) =
"Invalid module type: " <> T.pack str <>
". Must be one of: " <> renderList moduleTypes
where
renderList =
map (T.pack . show . fst)
>>> T.intercalate ", "
data PackageNameError
= NotEmpty
| TooLong Int
| InvalidChars String
| RepeatedSeparators
| MustNotBeginSeparator
| MustNotEndSeparator
showPackageNameError :: PackageNameError -> Text
showPackageNameError err = case err of
NotEmpty ->
"A package name may not be empty"
TooLong x ->
"Package names must be no more than 50 characters, yours was " <>
T.pack (show x)
InvalidChars str ->
"The following characters are not permitted in package names: " <>
T.intercalate " " (map T.singleton str)
RepeatedSeparators ->
"The substrings \"--\" and \"..\" may not appear in "<>
"package names"
MustNotBeginSeparator ->
"Package names may not begin with a dash or a dot"
MustNotEndSeparator ->
"Package names may not end with a dash or a dot"
decodeFile :: FilePath -> IO (Either (ParseError BowerError) PackageMeta)
decodeFile = fmap (parse asPackageMeta) . B.readFile
asPackageMeta :: Parse BowerError PackageMeta
asPackageMeta =
PackageMeta <$> key "name" (withString parsePackageName)
<*> keyMay "description" asString
<*> keyOrDefault "main" [] (eachInArray asString)
<*> keyOrDefault "moduleType" [] (eachInArray (withString parseModuleType))
<*> keyOrDefault "licence" [] (eachInArray asString)
<*> keyOrDefault "ignore" [] (eachInArray asString)
<*> keyOrDefault "keywords" [] (eachInArray asString)
<*> keyOrDefault "authors" [] (eachInArray asAuthor)
<*> keyMay "homepage" asString
<*> keyMay "repository" asRepository
<*> keyOrDefault "dependencies" [] (asAssocListOf VersionRange)
<*> keyOrDefault "devDependencies" [] (asAssocListOf VersionRange)
<*> keyOrDefault "resolutions" [] (asAssocListOf Version)
<*> keyOrDefault "private" False asBool
where
asAssocListOf :: (String -> a) -> Parse BowerError [(PackageName, a)]
asAssocListOf g =
eachInObjectWithKey (parsePackageName . T.unpack) (g <$> asString)
parseModuleType :: String -> Either BowerError ModuleType
parseModuleType str =
case lookup str moduleTypes of
Nothing -> Left (InvalidModuleType str)
Just mt -> Right mt
parsePackageName :: String -> Either BowerError PackageName
parsePackageName str =
case mkPackageName str of
Left err -> Left (InvalidPackageName err)
Right n -> Right n
asAuthor :: Parse e Author
asAuthor = catchError asAuthorString (const asAuthorObject)
asAuthorString :: Parse e Author
asAuthorString = withString $ \s ->
let (email, s1) = takeDelim "<" ">" (words s)
(homepage, s2) = takeDelim "(" ")" s1
in pure (Author (unwords s2) email homepage)
takeDelim :: String -> String -> [String] -> (Maybe String, [String])
takeDelim start end = foldr go (Nothing, [])
where
go str (Just x, strs) =
(Just x, str : strs)
go str (Nothing, strs) =
case stripWrapper start end str of
Just str' -> (Just str', strs)
Nothing -> (Nothing, str : strs)
stripWrapper :: String -> String -> String -> Maybe String
stripWrapper start end =
stripPrefix start
>>> fmap reverse
>=> stripPrefix (reverse end)
>>> fmap reverse
asAuthorObject :: Parse e Author
asAuthorObject =
Author <$> key "name" asString
<*> keyMay "email" asString
<*> keyMay "homepage" asString
asRepository :: Parse e Repository
asRepository =
Repository <$> key "url" asString
<*> key "type" asString
instance A.ToJSON PackageMeta where
toJSON PackageMeta{..} =
A.object $ concat
[ [ "name" .= bowerName ]
, maybePair "description" bowerDescription
, maybeArrayPair "main" bowerMain
, maybeArrayPair "moduleType" bowerModuleType
, maybeArrayPair "licence" bowerLicence
, maybeArrayPair "ignore" bowerIgnore
, maybeArrayPair "keywords" bowerKeywords
, maybeArrayPair "authors" bowerAuthors
, maybePair "homepage" bowerHomepage
, maybePair "repository" bowerRepository
, assoc "dependencies" bowerDependencies
, assoc "devDependencies" bowerDevDependencies
, assoc "resolutions" bowerResolutions
, if bowerPrivate then [ "private" .= True ] else []
]
where
toText = T.pack . runPackageName
assoc :: A.ToJSON a => Text -> [(PackageName, a)] -> [Aeson.Pair]
assoc = maybeArrayAssocPair toText
instance A.ToJSON PackageName where
toJSON = A.toJSON . runPackageName
instance A.ToJSON ModuleType where
toJSON = A.toJSON . map toLower . show
instance A.ToJSON Repository where
toJSON Repository{..} =
A.object [ "url" .= repositoryUrl
, "type" .= repositoryType
]
instance A.ToJSON Author where
toJSON Author{..} =
A.object $
[ "name" .= authorName ] ++
maybePair "email" authorEmail ++
maybePair "homepage" authorHomepage
instance A.ToJSON Version where
toJSON = A.toJSON . runVersion
instance A.ToJSON VersionRange where
toJSON = A.toJSON . runVersionRange
maybePair :: A.ToJSON a => Text -> Maybe a -> [Aeson.Pair]
maybePair k = maybe [] (\val -> [k .= val])
maybeArrayPair :: A.ToJSON a => Text -> [a] -> [Aeson.Pair]
maybeArrayPair _ [] = []
maybeArrayPair k xs = [k .= xs]
maybeArrayAssocPair :: A.ToJSON b => (a -> Text) -> Text -> [(a,b)] -> [Aeson.Pair]
maybeArrayAssocPair _ _ [] = []
maybeArrayAssocPair f k xs = [k .= A.object (map (\(k', v) -> f k' .= v) xs)]
instance A.FromJSON PackageMeta where
parseJSON = toAesonParser showBowerError asPackageMeta
instance A.FromJSON PackageName where
parseJSON = toAesonParser showBowerError (withString parsePackageName)
instance A.FromJSON ModuleType where
parseJSON = toAesonParser showBowerError (withString parseModuleType)
instance A.FromJSON Repository where
parseJSON = toAesonParser' asRepository
instance A.FromJSON Author where
parseJSON = toAesonParser' asAuthor
instance A.FromJSON Version where
parseJSON = toAesonParser' (Version <$> asString)
instance A.FromJSON VersionRange where
parseJSON = toAesonParser' (VersionRange <$> asString)