content: {-# LANGUAGE LambdaCase, OverloadedStrings, RecordWildCards #-}
  {-|
  Module : Yarn.Lock
  Description : High-level parser of yarn.lock files
  Maintainer : Profpatsch
  Stability : experimental
  
  The <https://yarnpkg.com/ Yarn package manager> improves on npm,
  because it writes @yarn.lock@ files that contain a complete
  version resolution of all dependencies. This way a deterministic
  deployment can be guaranteed.
  -}
  module Yarn.Lock
  ( T.Lockfile
  , parseFile, parse
  -- * Errors
  , prettyLockfileError
  , LockfileError(..), PackageErrorInfo(..)
  ) where
  
  import qualified Data.Text as T
  import qualified Data.List.NonEmpty as NE
  import qualified Text.Megaparsec as MP
  import qualified Data.Either.Validation as V
  
  import qualified Yarn.Lock.Types as T
  import qualified Yarn.Lock.File as File
  import qualified Yarn.Lock.Parse as Parse
  import Data.Text (Text)
  import Data.Functor ((<&>))
  import Control.Monad ((>=>))
  import qualified Data.Text as Text
  import qualified Data.Text.IO as Text.IO
  import Data.Bifunctor (first, Bifunctor (bimap))
  
  -- | Everything that can go wrong when parsing a 'Lockfile'.
  data LockfileError
    = ParseError Text
    -- ^ The initial parsing step failed
    | PackageErrors (NE.NonEmpty PackageErrorInfo)
    -- ^ a package could not be parsed from the AST
    deriving (Show, Eq)
  
  -- | Information about package parsing errors.
  data PackageErrorInfo = PackageErrorInfo
    { srcPos :: MP.SourcePos
    -- ^ the position of the package in the original file
    , convErrs :: NE.NonEmpty File.ConversionError
    -- ^ list of reasons for failure
    } deriving (Show, Eq)
  
  -- | Convenience function, combining all parsing steps.
  --
  -- The resulting 'Lockfile' structure might not yet be optimal,
  -- see 'File.fromPackages'.
  parseFile :: FilePath -- ^ file to read
            -> IO (Either LockfileError T.Lockfile)
  parseFile fp = Text.IO.readFile fp <&> parse fp
  
  -- | For when you want to provide only the file contents.
  parse :: FilePath -- ^ name of the input file, used for the parser
        -> Text     -- ^ content of a @yarn.lock@
        -> Either LockfileError T.Lockfile
  parse fp = astParse fp >=> toPackages >=> toLockfile
  
  -- | Pretty print a parsing error with sane default formatting.
  prettyLockfileError :: LockfileError -> Text
  prettyLockfileError = \case
    (ParseError t) -> "Error while parsing the yarn.lock:\n"
      <> T.unlines (indent 2 (T.lines t))
    (PackageErrors errs) -> "Some packages could not be made sense of:\n"
      <> T.unlines (NE.toList $ indent 2 (errs >>= errText))
    where
      indent :: Functor f => Int -> f Text -> f Text
      indent i = fmap (T.replicate i " " <>)
      errText PackageErrorInfo{..} =
        (pure $ "Package at " <> (Text.pack $ MP.sourcePosPretty srcPos) <> ":")
        <> indent 2 (fmap convErrText convErrs)
      convErrText = \case
        (File.MissingField t) -> "Field " <> qu t <> " is missing."
        (File.WrongType{..})  -> "Field " <> qu fieldName
                                 <> " should be of type " <> fieldType <> "."
        (File.UnknownRemoteType) -> "We don’t know this remote type."
      qu t = "\"" <> t <> "\""
  
  -- helpers
  astParse :: FilePath -> Text -> Either LockfileError [Parse.Package]
  astParse fp = first (ParseError . Text.pack . MP.errorBundlePretty)
                  . MP.parse Parse.packageList fp
  
  toPackages :: [Parse.Package] -> Either LockfileError [T.Keyed T.Package]
  toPackages = first PackageErrors . V.validationToEither
                  . traverse validatePackage
  
  validatePackage :: Parse.Package
                  -> V.Validation (NE.NonEmpty PackageErrorInfo) (T.Keyed T.Package)
  validatePackage (T.Keyed keys (pos, fields)) = V.eitherToValidation
    $ bimap (pure . PackageErrorInfo pos) (T.Keyed keys)
      $ File.astToPackage fields
  
  toLockfile :: [T.Keyed T.Package] -> Either LockfileError T.Lockfile
  toLockfile = pure . File.fromPackages
  
  