{-# language ConstraintKinds           #-}
{-# language DataKinds                 #-}
{-# language ExistentialQuantification #-}
{-# language GADTs                     #-}
{-# language PolyKinds                 #-}
{-# language TypeFamilies              #-}
{-# language TypeOperators             #-}
{-# language UndecidableInstances      #-}
{-|
Description : Protocol-independent declaration of services

This module defines a type-level language to describe
RPC-like microservices independently of the transport
and protocol.
-}
module Mu.Rpc (
  Service', Service(..)
, ServiceAnnotation, Package, FindPackageName
, Method(..), (:-->:)
, TypeRef(..), Argument(..), Return(..)
) where

import           Data.Kind
import           GHC.TypeLits
import qualified Language.Haskell.TH as TH

import           Mu.Schema
import           Mu.Schema.Registry

-- | Services whose names are given by type-level strings.
type Service' = Service Symbol Symbol
-- | Annotations for services. At this moment, such
--   annotations can be of any type.
type ServiceAnnotation = Type

-- | A service is a set of methods.
data Service serviceName methodName
  = Service serviceName [ServiceAnnotation] [Method methodName]

-- | An annotation to define a package name.
--   This is used by some handlers, like gRPC.
data Package (s :: Symbol)

-- | Find the 'Package' for a service, to be found
--   as part of the annotations.
type family FindPackageName (anns :: [ServiceAnnotation]) :: Symbol where
  FindPackageName '[] = TypeError ('Text "Cannot find package name for the service")
  FindPackageName (Package s ': rest) = s
  FindPackageName (other     ': rest) = FindPackageName rest

-- | A method is defined by its name, arguments, and return type.
data Method methodName
  = Method methodName [ServiceAnnotation] [Argument] Return

-- | Look up a method in a service definition using its name.
--   Useful to declare handlers like @HandlerIO (MyService :-->: "MyMethod")@.
type family (:-->:) (s :: Service snm mnm) (m :: mnm) :: Method mnm where
  'Service sname anns methods :-->: m = LookupMethod methods m

type family LookupMethod (s :: [Method mnm]) (m :: snm) :: Method snm where
  LookupMethod '[] m = TypeError ('Text "could not find method " ':<>: 'ShowType m)
  LookupMethod ('Method m anns args r ': ms) m = 'Method m anns args r
  LookupMethod (other                 ': ms) m = LookupMethod ms m

-- | Defines how to handle the type
data TypeRef where
  ViaSchema   :: Schema typeName fieldName -> typeName -> TypeRef
  -- | Registry subject, type to convert to, and preferred serialization version
  ViaRegistry :: Registry -> Type -> Nat -> TypeRef
  -- | To be used only during TH generation!
  ViaTH       :: TH.Type -> TypeRef

-- | Defines the way in which arguments are handled.
data Argument where
  -- | Use a single value.
  ArgSingle :: TypeRef -> Argument
  -- | Consume a stream of values.
  ArgStream :: TypeRef -> Argument

-- | Defines the different possibilities for returning
--   information from a method.
data Return where
  -- | Fire and forget.
  RetNothing :: Return
  -- | Return a single value.
  RetSingle :: TypeRef -> Return
  -- | Return a value or an error
  --   (this can be found in Avro IDL).
  RetThrows :: TypeRef -> TypeRef -> Return
  -- | Return a stream of values
  --   (this can be found in gRPC).
  RetStream :: TypeRef -> Return