packages feed

calamity-0.11.0.0: Calamity/HTTP/Internal/Route.hs

{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -Wno-unused-top-binds #-}

{- | The route type
 Why I did this I don't know
-}
module Calamity.HTTP.Internal.Route (
  mkRouteBuilder,
  giveID,
  giveParam,
  buildRoute,
  routeKey,
  RouteKey,
  RouteBuilder,
  RouteRequirement,
  Route (path),
  S (..),
  PS (..),
  ID (..),
  RouteFragmentable (..),
) where

import Calamity.Types.Model.Channel
import Calamity.Types.Model.Guild
import Calamity.Types.Snowflake
import Data.Hashable
import Data.Kind
import Data.List (foldl')
import Data.Maybe (fromJust)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Typeable
import Data.Word
import GHC.Generics (Generic)
import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
import Network.HTTP.Req
import Optics.TH
import TextShow qualified

data RouteFragment
  = -- | Static string fragment
    S' Text
  | -- | Parameterised string fragment
    PS' String
  | -- | ID fragment
    ID' TypeRep
  deriving (Generic, Show, Eq)

-- | A static string fragment of a route
newtype S = S Text

-- | A parameterised string fragment of a route
data PS (s :: Symbol) = PS

-- | An id fragment of a route
data ID a = ID

instance Hashable RouteFragment

data RouteRequirement
  = NotNeeded
  | Required
  | Satisfied
  deriving (Show, Eq)

data RequirementType
  = IDRequirement Type
  | PSRequirement Symbol

data RouteBuilder (reqstate :: [(RequirementType, RouteRequirement)]) = UnsafeMkRouteBuilder
  { route :: [RouteFragment]
  , ids :: [(TypeRep, Word64)]
  , params :: [(String, Text)]
  }

mkRouteBuilder :: RouteBuilder '[]
mkRouteBuilder = UnsafeMkRouteBuilder [] [] []

giveID ::
  forall t reqs.
  (Typeable t) =>
  Snowflake t ->
  RouteBuilder reqs ->
  RouteBuilder ('( 'IDRequirement t, 'Satisfied) ': reqs)
giveID (Snowflake id) (UnsafeMkRouteBuilder route ids params) =
  UnsafeMkRouteBuilder route ((typeRep $ Proxy @t, id) : ids) params

giveParam ::
  forall (s :: Symbol) reqs.
  (KnownSymbol s) =>
  Text ->
  RouteBuilder reqs ->
  RouteBuilder ('( 'PSRequirement s, 'Satisfied) ': reqs)
giveParam value (UnsafeMkRouteBuilder route ids params) =
  UnsafeMkRouteBuilder route ids ((symbolVal $ Proxy @s, value) : params)

type family (&&) (a :: Bool) (b :: Bool) :: Bool where
  'True && 'True = 'True
  _ && _ = 'False

type family Lookup (x :: k) (l :: [(k, v)]) :: Maybe v where
  Lookup k ('(k, v) ': xs) = 'Just v
  Lookup k ('(_, v) ': xs) = Lookup k xs
  Lookup _ '[] = 'Nothing

type family IsElem (x :: k) (l :: [k]) :: Bool where
  IsElem _ '[] = 'False
  IsElem k (k : _) = 'True
  IsElem k (_ : xs) = IsElem k xs

type family EnsureFulfilled (reqs :: [(RequirementType, RouteRequirement)]) :: Constraint where
  EnsureFulfilled reqs = EnsureFulfilledInner reqs '[] 'True

type family EnsureFulfilledInner (reqs :: [(RequirementType, RouteRequirement)]) (seen :: [RequirementType]) (ok :: Bool) :: Constraint where
  EnsureFulfilledInner '[] _ 'True = ()
  EnsureFulfilledInner ('(k, 'NotNeeded) ': xs) seen ok = EnsureFulfilledInner xs (k ': seen) ok
  EnsureFulfilledInner ('(k, 'Satisfied) ': xs) seen ok = EnsureFulfilledInner xs (k ': seen) ok
  EnsureFulfilledInner ('(k, 'Required) ': xs) seen ok = EnsureFulfilledInner xs (k ': seen) (IsElem k seen && ok)

type family AddRequired k (reqs :: [(RequirementType, RouteRequirement)]) :: [(RequirementType, RouteRequirement)] where
  AddRequired k reqs = '(k, AddRequiredInner (Lookup k reqs)) ': reqs

type family AddRequiredInner (k :: Maybe RouteRequirement) :: RouteRequirement where
  AddRequiredInner ('Just 'Required) = 'Required
  AddRequiredInner ('Just 'Satisfied) = 'Satisfied
  AddRequiredInner ('Just 'NotNeeded) = 'Required
  AddRequiredInner 'Nothing = 'Required

class (Typeable a) => RouteFragmentable a reqs where
  type ConsRes a reqs

  (//) :: RouteBuilder reqs -> a -> ConsRes a reqs

instance RouteFragmentable S reqs where
  type ConsRes S reqs = RouteBuilder reqs

  (UnsafeMkRouteBuilder r ids params) // (S t) =
    UnsafeMkRouteBuilder (r <> [S' t]) ids params

instance (Typeable a) => RouteFragmentable (ID (a :: Type)) (reqs :: [(RequirementType, RouteRequirement)]) where
  type ConsRes (ID a) reqs = RouteBuilder (AddRequired ('IDRequirement a) reqs)

  (UnsafeMkRouteBuilder r ids params) // ID =
    UnsafeMkRouteBuilder (r <> [ID' $ typeRep $ Proxy @a]) ids params

instance (KnownSymbol s) => RouteFragmentable (PS s) (reqs :: [(RequirementType, RouteRequirement)]) where
  type ConsRes (PS s) reqs = RouteBuilder (AddRequired ('PSRequirement s) reqs)

  (UnsafeMkRouteBuilder r ids params) // PS =
    UnsafeMkRouteBuilder (r <> [PS' $ symbolVal $ Proxy @s]) ids params

infixl 5 //

data Route = Route
  { path :: Url 'Https
  , key :: Text
  , channelID :: Maybe (Snowflake Channel)
  , guildID :: Maybe (Snowflake Guild)
  }
  deriving (Show)

type RouteKey = (Text, Maybe (Snowflake Channel), Maybe (Snowflake Guild))

routeKey :: Route -> RouteKey
routeKey Route {key, channelID, guildID} = (key, channelID, guildID)

baseURL :: Url 'Https
baseURL = https "discord.com" /: "api" /: "v10"

buildRoute ::
  forall (reqs :: [(RequirementType, RouteRequirement)]).
  (EnsureFulfilled reqs) =>
  RouteBuilder reqs ->
  Route
buildRoute (UnsafeMkRouteBuilder route ids params) =
  Route
    (foldl' (/:) baseURL $ map goRoute route)
    (T.concat (map goKey route))
    (Snowflake <$> lookup (typeRep (Proxy @Channel)) ids)
    (Snowflake <$> lookup (typeRep (Proxy @Guild)) ids)
  where
    goRoute (S' t) = t
    goRoute (PS' t) = fromJust $ lookup t params
    goRoute (ID' t) = TextShow.showt . fromJust $ lookup t ids

    goKey (S' t) = t
    goKey (PS' t) = T.pack t
    goKey (ID' t) = TextShow.showt t

$(makeFieldLabelsNoPrefix ''Route)