-- | Verious information about your API.
module Servant.Util.Stats
    ( methodsCoveringAPI
    ) where

import Universum

import GHC.TypeLits (ErrorMessage (..), Symbol, TypeError)
import Network.HTTP.Types.Method (Method, StdMethod)
import Servant (EmptyAPI, Raw, ReflectMethod (..), Verb, (:<|>), (:>))

-------------------------------------------------------------------------
-- CORS methods coherence
-------------------------------------------------------------------------

-- | Whether a given type item is element of a given list.
type family IsElemBool (x :: StdMethod) (l :: [StdMethod]) :: Bool where
    IsElemBool x (x : _) = 'True
    IsElemBool x (y : xs) = IsElemBool x xs
    IsElemBool x '[] = 'False

type family FailOnDissallowedMethod (method :: StdMethod) (allowed :: Bool) :: Constraint where
    FailOnDissallowedMethod _ 'True = ()
    FailOnDissallowedMethod m 'False = TypeError
        ( 'Text "Method " ':$$: 'ShowType m ':$$: 'Text " is not allowed, but appears in API"
        )

-- | Ensure that the given api uses only methods from the list provided.
type family ContainsOnlyMethods (methods :: [StdMethod]) api :: Constraint where
    ContainsOnlyMethods ms ((path :: Symbol) :> sub) = ContainsOnlyMethods ms sub
    ContainsOnlyMethods ms (part :> sub) = ContainsOnlyMethods ms sub
    ContainsOnlyMethods ms (api1 :<|> api2) = (ContainsOnlyMethods ms api1,
                                               ContainsOnlyMethods ms api2)
    ContainsOnlyMethods ms (Verb m _ _ _) = FailOnDissallowedMethod m (IsElemBool m ms)
    ContainsOnlyMethods ms Raw = ()
    ContainsOnlyMethods ms EmptyAPI = ()

-- | 'ReflectMethod' lifted to method lists.
class ReflectMethods (methods :: [StdMethod]) where
    reflectMethods :: Proxy methods -> [Method]
instance ReflectMethods '[] where
    reflectMethods :: Proxy '[] -> [Method]
reflectMethods Proxy '[]
_ = []
instance (ReflectMethod m, ReflectMethods ms) => ReflectMethods (m ': ms) where
    reflectMethods :: Proxy (m : ms) -> [Method]
reflectMethods Proxy (m : ms)
_ = Proxy m -> Method
forall k (a :: k). ReflectMethod a => Proxy a -> Method
reflectMethod @m Proxy m
forall k (t :: k). Proxy t
Proxy Method -> [Method] -> [Method]
forall a. a -> [a] -> [a]
: Proxy ms -> [Method]
forall (methods :: [StdMethod]).
ReflectMethods methods =>
Proxy methods -> [Method]
reflectMethods @ms Proxy ms
forall k (t :: k). Proxy t
Proxy

-- | For the given list of methods, ensure only they are used in API, and get corresponding
-- 'Method' terms.
--
-- A primary use case for this function is specifying CORS methods where we need to think
-- about each single method we allow, thus expecting methods list to be specified manually.
methodsCoveringAPI
    :: forall methods api.
       (ContainsOnlyMethods methods api, ReflectMethods methods)
    => [Method]
methodsCoveringAPI :: [Method]
methodsCoveringAPI = Proxy methods -> [Method]
forall (methods :: [StdMethod]).
ReflectMethods methods =>
Proxy methods -> [Method]
reflectMethods @methods Proxy methods
forall k (t :: k). Proxy t
Proxy