dhall-haskell icon indicating copy to clipboard operation
dhall-haskell copied to clipboard

Safe input handling

Open georgefst opened this issue 3 years ago • 3 comments

I have multiple applications in which I periodically load Dhall files, and need to safely handle any ill-formed input expression without bringing down the whole app. Essentially, I want a version of input that doesn't throw exceptions. AFAICT, the current API doesn't provide any easy way to do this. I've ended up with:

EDIT: The previous version of the code below was totally wrong as it attempted to use the type in place of the value. Also it required turning off the monomorphism restriction for printError, and had an unnecessary FromDhall constraint.

import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Maybe
import Data.Either.Validation
import Data.Text (Text)
import Dhall qualified
import Dhall.Core qualified as Dhall
import Dhall.Import qualified as Dhall
import Dhall.Parser qualified as Dhall
import Dhall.TypeCheck qualified as Dhall

-- | Like 'Dhall.input', but handles any exceptions and prints them to stdout.
inputSafe :: Dhall.Decoder a -> Text -> IO (Maybe a)
inputSafe decoder =
    runMaybeT
        . ( printError . Dhall.toMonadic . Dhall.extract decoder . Dhall.renote . Dhall.normalize
                <=< checkType
                <=< printError
                <=< liftIO . try @(Dhall.SourcedException Dhall.MissingImports) . Dhall.load
                <=< printError . Dhall.exprFromText ""
          )
  where
    printError :: Show a => Either a b -> MaybeT IO b
    printError = either (\e -> liftIO (print e) >> mzero) pure
    checkType e = do
        expectedType <- printError . validationToEither $ Dhall.expected decoder
        _ <- printError . Dhall.typeOf $ Dhall.Annot e expectedType
        pure e

Is this even correct - are there other errors that could be thrown (I'm happy to treat IOException etc. separately)? Could the library export a function like this, or at least make it easier to construct? This took a lot of trial and error, and poring through documentation.

georgefst avatar Jul 15 '21 20:07 georgefst

I guess a more general function would have a type like Dhall.Decoder a -> Text -> IO (Either E a), where E is a sum of all the errors which could occur. It just happens that in my use cases, I'm happy to log to the console and return Nothing.

georgefst avatar Jul 15 '21 20:07 georgefst

Actually, this should be an equivalent definition:

inputSafe :: Dhall.FromDhall a => Dhall.Decoder a -> Text -> IO (Maybe a)
inputSafe decoder t =
    handle (\(e :: Dhall.ExtractErrors Dhall.Src Void) -> printError e)
        . handle (\(e :: Dhall.TypeError Dhall.Src Void) -> printError e)
        . handle (\(e :: Dhall.SourcedException Dhall.MissingImports) -> printError e)
        . handle (\(e :: Dhall.ParseError) -> printError e)
        $ Just <$> Dhall.input decoder t
  where
    printError = (>> pure Nothing) . print

This is less painful to write, but the issue is discovering the list of exceptions which need to be handled.

georgefst avatar Jul 15 '21 20:07 georgefst

You could catch all of them by catching SomeException or catch just synchronous exceptions by using UnliftIO.Exception.catchAny (or something equivalent to that)

Gabriella439 avatar Jul 16 '21 23:07 Gabriella439