packages feed

descript-lang-0.2.0.0: src/Descript/Misc/Build/Read/File/DepResolve.hs

module Descript.Misc.Build.Read.File.DepResolve
  ( GenDepResolver (..)
  , defaultTextResolver
  ) where

import Descript.Misc.Build.Read.File.DepError
import Descript.Misc.Build.Read.File.Scope
import Descript.Misc.Error
import Core.Data.Functor
import Data.Text (Text)
import qualified Data.Text.IO as Text
import Control.Applicative
import Core.Control.Applicative
import Control.Exception
import System.IO.Error
import Paths_descript_lang

-- | Finds the dependency at the given relative module.
data GenDepResolver u a
  = DepResolver
  { showDepResolver :: String -- ^ Used for 'Show'.
  , resolveDep :: RelScope -> GenDepResultT u a
  }

instance Show (GenDepResolver u a) where
  show = showDepResolver

instance (Functor u) => Functor (GenDepResolver u) where
  fmap f x
    = DepResolver
    { showDepResolver = "f <$> " ++ showDepResolver x
    , resolveDep = f <<$>> resolveDep x
    }

instance (Applicative u) => Applicative (GenDepResolver u) where
  pure x
    = DepResolver
    { showDepResolver = "pure x"
    , resolveDep = pure2 x
    }
  f <*> x
    = DepResolver
    { showDepResolver = showDepResolver f ++ " <*> " ++ showDepResolver x
    , resolveDep = resolveDep f <<*>> resolveDep x
    }

instance (Monad u) => Monad (GenDepResolver u) where
  return = pure
  fx >>= f
    = DepResolver
    { showDepResolver = showDepResolver fx ++ " >>= f"
    , resolveDep = resolve
    }
    where resolve scope = do
            ix <- resolveDep fx scope
            resolveDep (f ix) scope

-- | Will use the second resolver if the first resolver can't find the
-- module. If the first resolver gets another resolver or succeeds,
-- will return its result.
instance (Monad u) => Alternative (GenDepResolver u) where
  empty
    = DepResolver
    { showDepResolver = "empty"
    , resolveDep = resolve
    }
    where resolve _ = mkFailureT $ DepNotExist []
  fx <|> fy
    = DepResolver
    { showDepResolver = showDepResolver fx ++ " <|> " ++ showDepResolver fy
    , resolveDep = resolve
    }
    where resolve scope = ResultT $ do
            xres <- runResultT $ resolveDep fx scope
            case xres of
              Failure (DepNotExist xPaths) -> do
                yres <- runResultT $ resolveDep fy scope
                case yres of
                  Failure (DepNotExist yPaths)
                    -> pure $ Failure $ DepNotExist paths
                    where paths = xPaths ++ yPaths
                  _ -> pure yres
              _ -> pure xres

-- | Resolves dependencies in the filesystem within the given folder,
-- or within the user's dependency folder.
defaultTextResolver :: FilePath -> GenDepResolver IO Text
defaultTextResolver path = localTextResolver path <|> sharedGenDepTextResolver

-- | Resolves dependencies in the filesystem within the given folder.
localTextResolver :: FilePath -> GenDepResolver IO Text
localTextResolver path
  = DepResolver
  { showDepResolver = "localResolver " ++ show path
  , resolveDep = resolveLocalT path
  }

-- | Resolves shared dependencies -- installed dependencies which are
-- accessible to all modules with the same path, like @Base@.
sharedGenDepTextResolver :: GenDepResolver IO Text
sharedGenDepTextResolver
  = DepResolver
  { showDepResolver = "sharedGenDepResolver"
  , resolveDep = resolveLocalT' getSharedDepPath . globalizeRelScope
  }

resolveLocalT :: FilePath -> RelScope -> GenDepResultT IO Text
resolveLocalT path = ResultT . resolveLocal path

resolveLocalT' :: IO FilePath -> RelScope -> GenDepResultT IO Text
resolveLocalT' getPath = ResultT . resolveLocal' getPath

resolveLocal :: FilePath -> RelScope -> IO (GenDepResult Text)
resolveLocal path scope
  = handle (failResolveLocal fullPath) $ forceResolveLocal fullPath
  where fullPath = scopeFilepath path scope

resolveLocal' :: IO FilePath -> RelScope -> IO (GenDepResult Text)
resolveLocal' getPath scope = (`resolveLocal` scope) =<< getPath

forceResolveLocal :: FilePath -> IO (GenDepResult Text)
forceResolveLocal = fmap Success . Text.readFile

failResolveLocal :: FilePath -> IOError -> IO (GenDepResult Text)
failResolveLocal path = pure . Failure . ioErrorToDepError path

ioErrorToDepError :: FilePath -> IOError -> AnonDepError
ioErrorToDepError path err
  | isDoesNotExistError err = DepNotExist [path]
  | otherwise = DepNotReadable

-- | This directory contains modules which can be used by a Descript
-- file anywhere.
getSharedDepPath :: IO FilePath
getSharedDepPath = getDataFileName "resources/modules"