content: {-# LANGUAGE  ViewPatterns #-}
  -- | Stability: unstable
  module Test.Hspec.Core.Util (
  -- * String functions
    pluralize
  , strip
  , lineBreaksAt
  , stripAnsi
  
  -- * Working with paths
  , Path
  , joinPath
  , formatRequirement
  , filterPredicate
  
  -- * Working with exceptions
  , safeTry
  , formatException
  , formatExceptionWith
  ) where
  
  import           Prelude ()
  import           Test.Hspec.Core.Compat hiding (join)
  
  import           Data.Char (isSpace)
  import           GHC.IO.Exception
  import           Control.Concurrent.Async
  
  -- |
  -- @pluralize count singular@ pluralizes the given @singular@ word unless given
  -- @count@ is 1.
  --
  -- Examples:
  --
  -- >>> pluralize 0 "example"
  -- "0 examples"
  --
  -- >>> pluralize 1 "example"
  -- "1 example"
  --
  -- >>> pluralize 2 "example"
  -- "2 examples"
  --
  -- @since 2.0.0
  pluralize :: Int -> String -> String
  pluralize 1 s = "1 " ++ s
  pluralize n s = show n ++ " " ++ s ++ "s"
  
  -- | Strip leading and trailing whitespace
  --
  -- @since 2.0.0
  strip :: String -> String
  strip = dropWhile isSpace . reverse . dropWhile isSpace . reverse
  
  -- |
  -- Ensure that lines are not longer than given `n`, insert line breaks at word
  -- boundaries
  --
  -- @since 2.0.0
  lineBreaksAt :: Int -> String -> [String]
  lineBreaksAt n = concatMap f . lines
    where
      f input = case words input of
        []   -> []
        x:xs -> go (x, xs)
  
      go :: (String, [String]) -> [String]
      go c = case c of
        (s, [])   -> [s]
        (s, y:ys) -> let r = s ++ " " ++ y in
          if length r <= n
            then go (r, ys)
            else s : go (y, ys)
  
  -- |
  -- Remove ANSI color escape sequences.
  --
  -- @since 2.11.0
  stripAnsi :: String -> String
  stripAnsi = go
    where
      go input = case input of
        '\ESC' : '[' : (dropWhile (`elem` "0123456789;") -> 'm' : xs) -> go xs
        x : xs -> x : go xs
        [] -> []
  
  -- |
  -- A `Path` describes the location of a spec item within a spec tree.
  --
  -- It consists of a list of group descriptions and a requirement description.
  --
  -- @since 2.0.0
  type Path = ([String], String)
  
  -- |
  -- Join a `Path` with slashes.  The result will have a leading and a trailing
  -- slash.
  --
  -- @since 2.5.4
  joinPath :: Path -> String
  joinPath (groups, requirement) = "/" ++ intercalate "/" (groups ++ [requirement]) ++ "/"
  
  -- |
  -- Try to create a proper English sentence from a path by applying some
  -- heuristics.
  --
  -- @since 2.0.0
  formatRequirement :: Path -> String
  formatRequirement (groups, requirement) = groups_ ++ requirement
    where
      groups_ = case break (any isSpace) groups of
        ([], ys) -> join ys
        (xs, ys) -> join (intercalate "." xs : ys)
  
      join xs = case xs of
        [x] -> x ++ " "
        ys  -> concatMap (++ ", ") ys
  
  -- | A predicate that can be used to filter a spec tree.
  --
  -- @since 2.0.0
  filterPredicate :: String -> Path -> Bool
  filterPredicate pattern path =
       pattern `isInfixOf` plain
    || pattern `isInfixOf` formatted
    where
      plain = joinPath path
      formatted = formatRequirement path
  
  -- | The function `formatException` converts an exception to a string.
  --
  -- This is different from `show`.  The type of the exception is included, e.g.:
  --
  -- >>> formatException (toException DivideByZero)
  -- "ArithException\ndivide by zero"
  --
  -- For `IOException`s the `IOErrorType` is included, as well.
  --
  -- @since 2.0.0
  formatException :: SomeException -> String
  formatException = formatExceptionWith show
  
  -- | @since 2.11.5
  formatExceptionWith :: (SomeException -> String) -> SomeException -> String
  formatExceptionWith showException err@(SomeException e) = case fromException err of
    Just ioe -> showType ioe ++ " of type " ++ showIOErrorType ioe ++ "\n" ++ showException (toException ioe)
    Nothing  -> showType e ++ "\n" ++ showException (SomeException e)
    where
      showIOErrorType :: IOException -> String
      showIOErrorType ioe = case ioe_type ioe of
        AlreadyExists -> "AlreadyExists"
        NoSuchThing -> "NoSuchThing"
        ResourceBusy -> "ResourceBusy"
        ResourceExhausted -> "ResourceExhausted"
        EOF -> "EOF"
        IllegalOperation -> "IllegalOperation"
        PermissionDenied -> "PermissionDenied"
        UserError -> "UserError"
        UnsatisfiedConstraints -> "UnsatisfiedConstraints"
        SystemError -> "SystemError"
        ProtocolError -> "ProtocolError"
        OtherError -> "OtherError"
        InvalidArgument -> "InvalidArgument"
        InappropriateType -> "InappropriateType"
        HardwareFault -> "HardwareFault"
        UnsupportedOperation -> "UnsupportedOperation"
        TimeExpired -> "TimeExpired"
        ResourceVanished -> "ResourceVanished"
        Interrupted -> "Interrupted"
  
  -- | @safeTry@ evaluates given action and returns its result.  If an exception
  -- occurs, the exception is returned instead.  Unlike `try` it is agnostic to
  -- asynchronous exceptions.
  --
  -- @since 2.0.0
  safeTry :: IO a -> IO (Either SomeException a)
  safeTry action = withAsync (action >>= evaluate) waitCatch
  