packages feed

queue-sheet-0.7.0.2: src/QueueSheet/File.hs

------------------------------------------------------------------------------
-- |
-- Module      : QueueSheet.File
-- Description : queue sheet file loading
-- Copyright   : Copyright (c) 2020-2022 Travis Cardwell
-- License     : MIT
------------------------------------------------------------------------------

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

module QueueSheet.File
  ( -- * YAML
    loadYaml
  , loadYaml'
  ) where

-- https://hackage.haskell.org/package/base
import Control.Exception (displayException)
import Control.Monad (forM, forM_, unless, when)
import Data.Bifunctor (first)
import System.IO.Error (tryIOError)

-- https://hackage.haskell.org/package/bytestring
import qualified Data.ByteString as BS
import Data.ByteString (ByteString)

-- https://hackage.haskell.org/package/directory
import System.Directory (makeAbsolute)

-- https://hackage.haskell.org/package/filepath
import System.FilePath ((</>), isAbsolute, normalise, takeDirectory)

-- https://hackage.haskell.org/package/transformers
import Control.Monad.Trans.Except (ExceptT(ExceptT), runExceptT, throwE)

-- https://hackage.haskell.org/package/ttc
import qualified Data.TTC as TTC

-- https://hackage.haskell.org/package/yaml
import qualified Data.Yaml as Yaml

-- (queue-sheet)
import QueueSheet.Types
  ( Import(Import, importPath, importSection)
  , ImportOrQueue(IQImport, IQQueue)
  , Queue(Queue, queueName, queueSection)
  , QueuesFile(QueuesFile, qfSections, qfImportOrQueues)
  , QueueSheet(QueueSheet, qsSections, qsQueues)
  )

------------------------------------------------------------------------------
-- $Yaml

-- | Load a queues YAML file, resolving imports
--
-- @since 0.3.0.0
loadYaml
  :: FilePath
  -> IO (Either String QueueSheet)
loadYaml path = runExceptT $ do
    let tryIOError' = fmap (first displayException) . tryIOError
    absPath <- ExceptT . tryIOError' $ makeAbsolute path
    ExceptT $ loadYaml' (tryIOError' . BS.readFile) absPath

-- | Load a queues YAML file using the given file loader, resolving imports
--
-- This function defines the logic for 'loadYaml' using an arbitrary monad.
-- It is exposed for testing purposes.
--
-- @since 0.3.0.0
loadYaml'
  :: forall m. Monad m
  => (FilePath -> m (Either String ByteString))  -- ^ file loader
  -> FilePath                                    -- ^ absolute path
  -> m (Either String QueueSheet)
loadYaml' loadFile = runExceptT . go []
  where
    go :: [FilePath] -> FilePath -> ExceptT String m QueueSheet
    go seenPaths path = do
      let error' = throwE . (("error loading " ++ path ++ ": ") ++)
          yamlError = error' . Yaml.prettyPrintParseException
      content <- ExceptT $ loadFile path
      QueuesFile{..} <- either yamlError pure $ Yaml.decodeEither' content
      queues <- fmap concat . forM qfImportOrQueues $ \case
        IQImport Import{..} -> do
          let seenPaths' = path : seenPaths
              importAbsPath
                | isAbsolute importPath = importPath
                | otherwise = normalise $ takeDirectory path </> importPath
          when (importAbsPath `elem` seenPaths') . error' $
            "cyclic import: " ++ importAbsPath
          queueSheet <- go seenPaths' importAbsPath
          let queues' = qsQueues queueSheet
          case importSection of
            Just section -> do
              unless (section `elem` qfSections) . error' . unwords $
                [ "import", importPath
                , "has unknown section", TTC.render section
                ]
              return [queue{ queueSection = section } | queue <- queues']
            Nothing -> do
              forM_ queues' $ \Queue{..} ->
                unless (queueSection `elem` qfSections) . error' . unwords $
                  [ "queue", TTC.render queueName
                  , "imported from", importPath
                  , "has unknown section", TTC.render queueSection
                  ]
              return queues'
        IQQueue queue@Queue{..} -> do
          unless (queueSection `elem` qfSections) . error' . unwords $
            [ "queue", TTC.render queueName
            , "has unknown section", TTC.render queueSection
            ]
          return [queue]
      return $ QueueSheet
        { qsSections = qfSections
        , qsQueues   = queues
        }