packages feed

btc-lsp-0.1.0.0: src/BtcLsp/Grpc/Combinator.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}

module BtcLsp.Grpc.Combinator
  ( fromReqT,
    fromReqE,
    newGenFailure,
    newSpecFailure,
    newInternalFailure,
    throwSpec,
    mkFieldLocation,
    GrpcReq,
    GrpcRes,
  )
where

import BtcLsp.Data.Type
import BtcLsp.Import.External as Ext
import Data.Map as Map
import Data.ProtoLens.Field
import Data.ProtoLens.Message
import Data.Text as T
import Language.Haskell.TH.Syntax as TH
import qualified Proto.BtcLsp.Data.HighLevel as Proto
import qualified Proto.BtcLsp.Data.HighLevel_Fields as Proto
import qualified Universum
import qualified Witch

type GrpcReq req =
  ( HasField req "maybe'ctx" (Maybe Proto.Ctx)
  )

type GrpcRes res failure specific =
  ( HasField res "ctx" Proto.Ctx,
    HasField res "failure" failure,
    HasField failure "generic" [Proto.InputFailure],
    HasField failure "specific" [specific],
    HasField failure "internal" [Proto.InternalFailure],
    Message res,
    Message failure
  )

fromReqT ::
  forall a b res failure specific m.
  ( From a b,
    'False ~ (a == b),
    GrpcRes res failure specific,
    Monad m
  ) =>
  ReversedFieldLocation ->
  Maybe a ->
  ExceptT res m b
fromReqT loc =
  except
    . fromReqE loc

fromReqE ::
  forall a b res failure specific.
  ( From a b,
    'False ~ (a == b),
    GrpcRes res failure specific
  ) =>
  ReversedFieldLocation ->
  Maybe a ->
  Either res b
fromReqE loc =
  (from <$>)
    . maybeToRight msg
  where
    msg =
      defMessage
        & field @"failure"
          .~ ( defMessage
                 & field @"generic"
                   .~ [ defMessage
                          & Proto.fieldLocation .~ from loc
                          & Proto.kind .~ Proto.REQUIRED
                      ]
             )

newGenFailure ::
  forall res failure specific.
  ( GrpcRes res failure specific
  ) =>
  Proto.InputFailureKind ->
  ReversedFieldLocation ->
  res
newGenFailure kind loc =
  defMessage
    & field @"failure"
      .~ ( defMessage
             & field @"generic"
               .~ [ defMessage
                      & Proto.fieldLocation .~ from loc
                      & Proto.kind .~ kind
                  ]
         )

newSpecFailure ::
  forall res failure specific.
  ( GrpcRes res failure specific
  ) =>
  specific ->
  res
newSpecFailure spec =
  defMessage
    & field @"failure"
      .~ ( defMessage
             & field @"specific"
               .~ [ spec
                  ]
         )

newInternalFailure ::
  forall res failure specific.
  ( GrpcRes res failure specific
  ) =>
  FailureInternal ->
  res
newInternalFailure hFailure =
  defMessage
    & field @"failure"
      .~ ( defMessage
             & field @"internal"
               .~ [ gFailure
                  ]
         )
  where
    gFailure =
      defMessage
        & case hFailure of
          FailureGrpcServer x -> Proto.grpcServer .~ x
          FailureGrpcClient {} -> Proto.redacted .~ True
          FailureMath x -> Proto.math .~ x
          FailurePrivate {} -> Proto.redacted .~ True
          FailureRedacted -> Proto.redacted .~ True

throwSpec ::
  forall a res failure specific m.
  ( GrpcRes res failure specific,
    Monad m
  ) =>
  specific ->
  ExceptT res m a
throwSpec =
  throwE . newSpecFailure

--
-- TH FieldIndex combinators
--

newtype FieldIndex
  = FieldIndex Word32
  deriving newtype
    ( Eq,
      Ord,
      Show
    )
  deriving stock
    ( TH.Lift
    )

newtype ReversedFieldLocation
  = ReversedFieldLocation [FieldIndex]
  deriving newtype
    ( Eq,
      Ord,
      Show,
      Semigroup,
      Monoid
    )
  deriving stock
    ( TH.Lift
    )

instance From ReversedFieldLocation [Proto.FieldIndex] where
  from (ReversedFieldLocation xs) =
    ( \x ->
        defMessage
          & Proto.val .~ coerce x
    )
      <$> Ext.reverse xs

mkFieldLocation ::
  forall a.
  ( Message a
  ) =>
  [String] ->
  Q Exp
mkFieldLocation ns =
  [|
    $(mkPushFieldIndexes @a ns) $
      ReversedFieldLocation []
    |]

mkPushFieldIndexes ::
  forall a.
  ( Message a
  ) =>
  [String] ->
  Q Exp
mkPushFieldIndexes ns = do
  addLoc <- getFieldLocation @a ns
  [|(<>) $(TH.lift addLoc)|]

getFieldLocation ::
  forall a m.
  ( Message a,
    MonadFail m
  ) =>
  [String] ->
  m ReversedFieldLocation
getFieldLocation =
  (ReversedFieldLocation <$>)
    . getFieldLocation0 @a []

getFieldLocation0 ::
  forall a m.
  ( Message a,
    MonadFail m
  ) =>
  [FieldIndex] ->
  [String] ->
  m [FieldIndex]
getFieldLocation0 acc0 [] = pure acc0
getFieldLocation0 acc0 (n : ns) =
  case Ext.find ((\(FieldDescriptor x _ _) -> x == n) . snd) $
    Map.toList (fieldsByTag :: Map Tag (FieldDescriptor a)) of
    Just (_, FieldDescriptor _ _ MapField {}) ->
      fieldFail
        "is MapField (not supported by current TH combinators)"
    Just (_, FieldDescriptor _ _ RepeatedField {})
      | not (Ext.null ns) ->
        fieldFail
          "is RepeatedField and not last field in TH splice"
    Just (it, FieldDescriptor _ ftd _) -> do
      acc <- case tryFrom $ unTag it of
        Right x -> pure $ FieldIndex x : acc0
        Left e ->
          fieldFail $
            "tag overflow "
              <> Universum.show e
              <> " of "
              <> Universum.show it
      case ftd of
        (MessageField {} :: FieldTypeDescriptor nextA) ->
          getFieldLocation0 @nextA acc ns
        ScalarField {} ->
          if Ext.null ns
            then pure acc
            else
              fieldFail $
                "scalar got extra tags "
                  <> inspectStr ns
    Nothing ->
      fieldFail "not found"
  where
    msgName =
      T.unpack $
        messageName (Proxy :: Proxy a)
    fieldFail x =
      fail $
        "Field " <> n <> " of " <> msgName <> " " <> x