packages feed

conversions-0.0.2: src/Data/Conversions.hs

{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE UndecidableInstances #-}
module Data.Conversions where

import           Control.Applicative
import           Control.Bool                   ( guard' )
import           Control.Exception              ( Exception )
import           Control.Monad                  ( MonadPlus(..) )
import           Control.Monad.Catch            ( MonadThrow
                                                , throwM
                                                )
import           Control.Monad.Except           ( MonadError
                                                , throwError
                                                )
import           Data.Coerce                    ( Coercible
                                                , coerce
                                                )
import           Data.Int                       ( Int16
                                                , Int32
                                                , Int64
                                                , Int8
                                                )
import           Data.Text                      ( Text )
import           Data.Typeable                  ( Typeable )
import           Data.Word                      ( Word16
                                                , Word32
                                                , Word64
                                                , Word8
                                                )
import           Numeric.Natural                ( Natural )
import           Prelude                 hiding ( max
                                                , min
                                                )

import qualified Data.ByteString               as BS
import qualified Data.ByteString.Lazy          as LBS
import qualified Data.Text                     as Text
import qualified Data.Text.Encoding            as Text
import qualified Data.Text.Lazy                as Text.Lazy
import qualified GHC.Show                      as Show
import qualified Language.Haskell.TH.Syntax    as TH

data BoundError a b = (Bounded b, Show a, Show b) => BoundError a

instance Show.Show (BoundError a b) where
  show (BoundError value) = boundError value (minBound @b) (maxBound @b)

instance (Typeable a, Typeable b) => Exception (BoundError a b)

data UserBoundError a b = UserBoundError a b b
  deriving stock (Typeable)

instance (Show a, Show b) => Show.Show (UserBoundError a b) where
  show (UserBoundError value min max) = boundError value min max

instance (Show a, Show b, Typeable a, Typeable b) => Exception (UserBoundError a b)

class Conversion b a where
  convert :: a -> b

  default convert :: (Coercible a b) => a -> b
  convert = coerce

instance (MonadError (UserBoundError Int Natural) m) => Conversion (m Natural) Int where
  convert = convertErrorFromIntegral

instance (MonadError (UserBoundError Int64 Natural) m) => Conversion (m Natural) Int64 where
  convert = convertErrorFromIntegral

instance (MonadError (UserBoundError Int32 Natural) m) => Conversion (m Natural) Int32 where
  convert = convertErrorFromIntegral

instance (MonadError (UserBoundError Int16 Natural) m) => Conversion (m Natural) Int16 where
  convert = convertErrorFromIntegral

instance (MonadError (UserBoundError Int8 Natural) m) => Conversion (m Natural) Int8 where
  convert = convertErrorFromIntegral

instance (MonadError (UserBoundError Word Natural) m) => Conversion (m Natural) Word where
  convert = convertErrorFromIntegral

instance (MonadError (UserBoundError Word64 Natural) m) => Conversion (m Natural) Word64 where
  convert = convertErrorFromIntegral

instance (MonadError (UserBoundError Word32 Natural) m) => Conversion (m Natural) Word32 where
  convert = convertErrorFromIntegral

instance (MonadError (UserBoundError Word16 Natural) m) => Conversion (m Natural) Word16 where
  convert = convertErrorFromIntegral

instance (MonadError (UserBoundError Integer Text) m) => Conversion (m Natural) Integer where
  convert value = maybe (throwError $ UserBoundError value "0" "Natural") pure
    $ checkedFromIntegral value

instance (MonadError (UserBoundError Natural Int) m) => Conversion (m Int) Natural where
  convert = convertErrorFromNatural

instance (MonadError (UserBoundError Natural Int16) m) => Conversion (m Int16) Natural where
  convert = convertErrorFromNatural

instance (MonadError (UserBoundError Natural Int32) m) => Conversion (m Int32) Natural where
  convert = convertErrorFromNatural

instance (MonadError (UserBoundError Natural Int64) m) => Conversion (m Int64) Natural where
  convert = convertErrorFromNatural

instance Conversion a a where
  convert = id

instance Conversion Integer Int where
  convert = fromIntegral

instance Conversion Integer Word32 where
  convert = fromIntegral

instance Conversion Integer Word16 where
  convert = fromIntegral

instance Conversion Integer Word8 where
  convert = fromIntegral

instance Conversion Natural Word32 where
  convert = fromIntegral

instance Conversion Natural Word16 where
  convert = fromIntegral

instance Conversion Natural Word8 where
  convert = fromIntegral

instance Conversion Integer Natural where
  convert = fromIntegral

instance (MonadError (BoundError Integer Int) m) => Conversion (m Int) Integer where
  convert = convertBoundedFromIntegral

instance (MonadError (BoundError Integer Word32) m) => Conversion (m Word32)  Integer where
  convert = convertBoundedFromIntegral

instance (MonadError (BoundError Integer Word16) m) => Conversion (m Word16) Integer where
  convert = convertBoundedFromIntegral

instance (MonadError (BoundError Integer Word8) m) => Conversion (m Word8) Integer where
  convert = convertBoundedFromIntegral

instance Conversion LBS.ByteString BS.ByteString where
  convert = LBS.fromStrict

instance Conversion BS.ByteString LBS.ByteString where
  convert = LBS.toStrict

instance Conversion BS.ByteString Text where
  convert = Text.encodeUtf8

instance Conversion Text.Lazy.Text Text where
  convert = Text.Lazy.fromStrict

instance Conversion Text Text.Lazy.Text where
  convert = Text.Lazy.toStrict

instance Conversion Text String where
  convert = Text.pack

instance Conversion String Text where
  convert = Text.unpack

type ToText a = Conversion Text a

convertErrorFromNatural
  :: forall a m
   . (Integral a, Bounded a, MonadError (UserBoundError Natural a) m)
  => Natural
  -> m a
convertErrorFromNatural value =
  maybe (throwError $ UserBoundError value minBound maxBound) pure
    $ checkedFromIntegral value

convertErrorFromIntegral
  :: forall a m
   . (Integral a, Bounded a, MonadError (UserBoundError a Natural) m)
  => a
  -> m Natural
convertErrorFromIntegral value =
  maybe (throwError $ UserBoundError value 0 maxBound') pure
    $ checkedFromIntegral value
 where
  maxBound' :: Natural
  maxBound' = fromIntegral $ maxBound @a

convertBoundedFromIntegral
  :: forall a b m
   . ( Integral a
     , Show a
     , Show b
     , Num b
     , Bounded b
     , Conversion a b
     , MonadError (BoundError a b) m
     )
  => a
  -> m b
convertBoundedFromIntegral value =
  if convert (minBound @b) <= value && value <= convert (maxBound @b)
    then pure $ fromIntegral value
    else throwError $ BoundError value

checkedFromIntegral
  :: forall a b m . (MonadPlus m, Integral a, Integral b) => a -> m b
checkedFromIntegral value = guard' (fromIntegral converted == value) converted
 where
  converted :: b
  converted = fromIntegral value

convertEither :: forall b a e . (Conversion (Either e b) a) => a -> Either e b
convertEither = convert

convertUnsafe :: forall b a e . (Conversion (Either e b) a, Show e) => a -> b
convertUnsafe = either (error . show) id . convertEither @b @a @e

convertThrow
  :: forall b a e m
   . (Conversion (Either e b) a, Exception e, MonadThrow m)
  => a
  -> m b
convertThrow = either throwM pure . convertEither @b @a @e

convertFail
  :: forall b a e m
   . (Conversion (Either e b) a, Show e, MonadFail m)
  => a
  -> m b
convertFail = either (fail . show) pure . convertEither @b @a @e

convertMaybe :: forall b a e . (Conversion (Either e b) a) => a -> Maybe b
convertMaybe = either (const empty) pure . convertEither @b @a @e

boundError :: forall a b . (Show a, Show b) => a -> b -> b -> String
boundError value min max =
  "Value should be between "
    <> show min
    <> " and "
    <> show max
    <> " but was "
    <> show value

mkTH
  :: forall a b e
   . (TH.Lift b, Exception e, Conversion (Either e b) a)
  => a
  -> TH.Q (TH.TExp b)
mkTH input =
  TH.TExp <$> either (fail . show) (TH.lift @b) (convertThrow @b @a @e input)

toText :: ToText a => a -> Text
toText = convert