-- | Stability: unstable
module Test.Hspec.Core.Util (
-- * String functions
  pluralize
, strip
, lineBreaksAt

-- * Working with paths
, Path
, joinPath
, formatRequirement
, filterPredicate

-- * Working with exception
, safeTry
, formatException
) where

import           Data.List
import           Data.Char (isSpace)
import           GHC.IO.Exception
import           Control.Exception
import           Control.Concurrent.Async

import           Test.Hspec.Core.Compat (showType)

-- |
-- @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"
pluralize :: Int -> String -> String
pluralize :: Int -> String -> String
pluralize 1 s :: String
s = "1 " String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
s
pluralize n :: Int
n s :: String
s = Int -> String
forall a. Show a => a -> String
show Int
n String -> String -> String
forall a. [a] -> [a] -> [a]
++ " " String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
s String -> String -> String
forall a. [a] -> [a] -> [a]
++ "s"

-- | Strip leading and trailing whitespace
strip :: String -> String
strip :: String -> String
strip = (Char -> Bool) -> String -> String
forall a. (a -> Bool) -> [a] -> [a]
dropWhile Char -> Bool
isSpace (String -> String) -> (String -> String) -> String -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> String
forall a. [a] -> [a]
reverse (String -> String) -> (String -> String) -> String -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> Bool) -> String -> String
forall a. (a -> Bool) -> [a] -> [a]
dropWhile Char -> Bool
isSpace (String -> String) -> (String -> String) -> String -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> String
forall a. [a] -> [a]
reverse

-- |
-- ensure that lines are not longer than given `n`, insert line breaks at word
-- boundaries
lineBreaksAt :: Int -> String -> [String]
lineBreaksAt :: Int -> String -> [String]
lineBreaksAt n :: Int
n input :: String
input = case String -> [String]
words String
input of
  []   -> []
  x :: String
x:xs :: [String]
xs -> (String, [String]) -> [String]
go (String
x, [String]
xs)
  where
    go :: (String, [String]) -> [String]
    go :: (String, [String]) -> [String]
go c :: (String, [String])
c = case (String, [String])
c of
      (s :: String
s, [])   -> [String
s]
      (s :: String
s, y :: String
y:ys :: [String]
ys) -> let r :: String
r = String
s String -> String -> String
forall a. [a] -> [a] -> [a]
++ " " String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
y in
        if String -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length String
r Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
n
          then (String, [String]) -> [String]
go (String
r, [String]
ys)
          else String
s String -> [String] -> [String]
forall a. a -> [a] -> [a]
: (String, [String]) -> [String]
go (String
y, [String]
ys)

-- |
-- 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.
type Path = ([String], String)

-- |
-- Join a `Path` with slashes.  The result will have a leading and a trailing
-- slash.
joinPath :: Path -> String
joinPath :: Path -> String
joinPath (groups :: [String]
groups, requirement :: String
requirement) = "/" String -> String -> String
forall a. [a] -> [a] -> [a]
++ String -> [String] -> String
forall a. [a] -> [[a]] -> [a]
intercalate "/" ([String]
groups [String] -> [String] -> [String]
forall a. [a] -> [a] -> [a]
++ [String
requirement]) String -> String -> String
forall a. [a] -> [a] -> [a]
++ "/"

-- |
-- Try to create a proper English sentence from a path by applying some
-- heuristics.
formatRequirement :: Path -> String
formatRequirement :: Path -> String
formatRequirement (groups :: [String]
groups, requirement :: String
requirement) = String
groups_ String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
requirement
  where
    groups_ :: String
groups_ = case (String -> Bool) -> [String] -> ([String], [String])
forall a. (a -> Bool) -> [a] -> ([a], [a])
break ((Char -> Bool) -> String -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any Char -> Bool
isSpace) [String]
groups of
      ([], ys :: [String]
ys) -> [String] -> String
join [String]
ys
      (xs :: [String]
xs, ys :: [String]
ys) -> [String] -> String
join (String -> [String] -> String
forall a. [a] -> [[a]] -> [a]
intercalate "." [String]
xs String -> [String] -> [String]
forall a. a -> [a] -> [a]
: [String]
ys)

    join :: [String] -> String
join xs :: [String]
xs = case [String]
xs of
      [x :: String
x] -> String
x String -> String -> String
forall a. [a] -> [a] -> [a]
++ " "
      ys :: [String]
ys  -> (String -> String) -> [String] -> String
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (String -> String -> String
forall a. [a] -> [a] -> [a]
++ ", ") [String]
ys

-- | A predicate that can be used to filter a spec tree.
filterPredicate :: String -> Path -> Bool
filterPredicate :: String -> Path -> Bool
filterPredicate pattern :: String
pattern path :: Path
path =
     String
pattern String -> String -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isInfixOf` String
plain
  Bool -> Bool -> Bool
|| String
pattern String -> String -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isInfixOf` String
formatted
  where
    plain :: String
plain = Path -> String
joinPath Path
path
    formatted :: String
formatted = Path -> String
formatRequirement Path
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 (divide by zero)"
--
-- For `IOException`s the `IOErrorType` is included, as well.
formatException :: SomeException -> String
formatException :: SomeException -> String
formatException err :: SomeException
err@(SomeException e :: e
e) = case SomeException -> Maybe IOException
forall e. Exception e => SomeException -> Maybe e
fromException SomeException
err of
  Just ioe :: IOException
ioe -> IOException -> String
forall a. Typeable a => a -> String
showType IOException
ioe String -> String -> String
forall a. [a] -> [a] -> [a]
++ " of type " String -> String -> String
forall a. [a] -> [a] -> [a]
++ IOException -> String
showIOErrorType IOException
ioe String -> String -> String
forall a. [a] -> [a] -> [a]
++ "\n" String -> String -> String
forall a. [a] -> [a] -> [a]
++ IOException -> String
forall a. Show a => a -> String
show IOException
ioe
  Nothing  -> e -> String
forall a. Typeable a => a -> String
showType e
e String -> String -> String
forall a. [a] -> [a] -> [a]
++ "\n" String -> String -> String
forall a. [a] -> [a] -> [a]
++ e -> String
forall a. Show a => a -> String
show e
e
  where
    showIOErrorType :: IOException -> String
    showIOErrorType :: IOException -> String
showIOErrorType ioe :: IOException
ioe = case IOException -> IOErrorType
ioe_type IOException
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.
safeTry :: IO a -> IO (Either SomeException a)
safeTry :: IO a -> IO (Either SomeException a)
safeTry action :: IO a
action = IO a
-> (Async a -> IO (Either SomeException a))
-> IO (Either SomeException a)
forall a b. IO a -> (Async a -> IO b) -> IO b
withAsync (IO a
action IO a -> (a -> IO a) -> IO a
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= a -> IO a
forall a. a -> IO a
evaluate) Async a -> IO (Either SomeException a)
forall a. Async a -> IO (Either SomeException a)
waitCatch