packages feed

pure-sum-0.1.0.0: src/Data/Sum/Pure.hs

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE UndecidableInstances #-}

-- |
-- Module        : Data.Sum.Pure
-- Copyright     : Gautier DI FOLCO
-- License       : BSD2
--
-- Maintainer    : Gautier DI FOLCO <gautier.difolco@gmail.com>
-- Stability     : Unstable
-- Portability   : GHC
--
-- Derive fromText/toText-like for pure sum types.
module Data.Sum.Pure
  ( -- * Base type
    PureSumWith (..),
    PureSum,

    -- * from/to Text converters
    ToSumText (..),
    FromSumText (..),

    -- * Transformations
    Transformation (..),
    type (<<<),
    DropPrefix,
    CamelCase,
    PascalCase,
    SnakeCase,
    SpinalCase,
    TitleCase,
    TrainCase,
  )
where

import Control.Applicative ((<|>))
import Control.Monad (guard)
import Data.Kind
import Data.Maybe (fromMaybe)
import Data.Proxy
import qualified Data.Text as T
import qualified Data.Text.Manipulate as Manipulation
import GHC.Generics
import GHC.TypeError
import GHC.TypeLits

-- * Base interface

-- | Wrapper for derivation.
-- @transformation@ is a @Transformation@ applied during derivations
newtype PureSumWith transformation a = PureSumWith {unPureSumWith :: a}
  deriving stock (Eq, Ord, Show)

-- | Basic sum derivation
type PureSum = PureSumWith IdTransformation

class ToSumText a where
  toSumText :: a -> T.Text

class FromSumText a where
  fromSumText :: T.Text -> Maybe a

-- * Text Functions

-- | Convert a type into a @Text -> Text@ function
class Transformation a where
  transform :: Proxy a -> T.Text -> T.Text

-- | Apply no transformation (like @id@)
data IdTransformation

instance Transformation IdTransformation where
  transform _ = id

-- | Compose two transformations (e.g. @f << g@ is equivalent to @f (g x)@)
data f <<< g

instance (Transformation f, Transformation g) => Transformation (f <<< g) where
  transform _ = transform (Proxy @f) . transform (Proxy @g)

-- | @DropPrefix prefix@ (e.g. @DropPrefix "A"@ on "ACase" gives "Case")
data DropPrefix (s :: Symbol)

instance (KnownSymbol s) => Transformation (DropPrefix s) where
  transform _ x = fromMaybe x $ T.stripPrefix (T.pack $ symbolVal @s Proxy) x

-- | Change case (e.g. "camelCasedPhrase")
data CamelCase

instance Transformation CamelCase where
  transform _ = Manipulation.toCamel

-- | Change case (e.g. "PascalCasedPhrase")
data PascalCase

instance Transformation PascalCase where
  transform _ = Manipulation.toPascal

-- | Change case (e.g. "snake_cased_phrase")
data SnakeCase

instance Transformation SnakeCase where
  transform _ = Manipulation.toSnake

-- | Change case (e.g. "spinal-cased-phrase")
data SpinalCase

instance Transformation SpinalCase where
  transform _ = Manipulation.toSpinal

-- | Change case (e.g. "Title Cased Phrase")
data TitleCase

instance Transformation TitleCase where
  transform _ = Manipulation.toTitle

-- | Change case (e.g. "Train-Cased-Phrase")
data TrainCase

instance Transformation TrainCase where
  transform _ = Manipulation.toTrain

-- * Generic derivation

-- * ToSumText

instance
  (Transformation transformation, Generic a, GToSumText (Rep a)) =>
  ToSumText (PureSumWith transformation a)
  where
  toSumText = gToSumText (transform $ Proxy @transformation) . from . unPureSumWith

class GToSumText f where
  gToSumText :: (T.Text -> T.Text) -> f a -> T.Text

instance (GToSumText a) => GToSumText (M1 D meta a) where -- base type
  gToSumText transformation (M1 x) = gToSumText transformation x

instance (KnownSymbol cntr, EnsureEmpty a) => GToSumText (M1 C ('MetaCons cntr p b) a) where -- constructor
  gToSumText transformation (M1 _) = transformation $ T.pack $ symbolVal $ Proxy @cntr

instance (GToSumText a, GToSumText b) => GToSumText (a :+: b) where -- sum type
  gToSumText transformation (R1 x) = gToSumText transformation x
  gToSumText transformation (L1 x) = gToSumText transformation x

instance (NonSumTypeError) => GToSumText V1 where
  gToSumText _ _ = error "impossible"

-- * FromSumText

instance
  (Transformation transformation, Generic a, GFromSumText (Rep a)) =>
  FromSumText (PureSumWith transformation a)
  where
  fromSumText x = PureSumWith . to <$> gFromSumText (transform $ Proxy @transformation) x

class GFromSumText f where
  gFromSumText :: (T.Text -> T.Text) -> T.Text -> Maybe (f a)

instance
  (GFromSumText a) =>
  GFromSumText (M1 D ('MetaData typeName c i b) a) -- base type
  where
  gFromSumText transformation x = M1 <$> gFromSumText transformation x

instance
  (GFromSumText a, KnownSymbol cntr) =>
  GFromSumText (M1 C ('MetaCons cntr p b) a) -- constructor
  where
  gFromSumText transformation v =
    M1 <$> do
      let cntrName = transformation $ T.pack $ symbolVal (Proxy @cntr)
      guard $ v == cntrName
      gFromSumText transformation v

instance (GFromSumText a, GFromSumText b) => GFromSumText (a :+: b) where -- sumtype
  gFromSumText transformation v =
    (L1 <$> gFromSumText transformation v)
      <|> (R1 <$> gFromSumText transformation v)

instance GFromSumText U1 where
  gFromSumText _ _ = return U1

instance (NonSumTypeError) => GFromSumText V1 where
  gFromSumText _ _ = error "impossible"

instance (NonSumTypeError) => GFromSumText (M1 S meta a) where
  gFromSumText _ _ = error "impossible"

-- * Utils

type family EnsureEmpty a :: Constraint where
  EnsureEmpty U1 = ()
  EnsureEmpty a = NonSumTypeError

type NonSumTypeError :: Constraint
type NonSumTypeError = TypeError (Text "Only pure sum types are supported (constructor(s) without values)")