packages feed

grpc-spec-1.0.0: src/Network/GRPC/Spec/RPC/JSON.hs

{-# LANGUAGE OverloadedStrings #-}

module Network.GRPC.Spec.RPC.JSON (
    JsonRpc

    -- Aeson support
  , JsonObject(..)
  , Required(..)
  , Optional(..)
  , DecodeFields -- opaque
  , EncodeFields -- opaque
  ) where

import Control.DeepSeq (NFData(..))
import Data.Aeson (ToJSON(..), FromJSON(..), (.=), (.:), (.:?))
import Data.Aeson qualified as Aeson
import Data.Aeson.Types qualified as Aeson
import Data.ByteString.Char8 qualified as BS.Char8
import Data.Kind
import Data.Proxy
import Data.String
import GHC.TypeLits

import Network.GRPC.Spec.CustomMetadata.Typed
import Network.GRPC.Spec.RPC
import Network.GRPC.Spec.RPC.StreamType

-- | gRPC using JSON as the message encoding
--
-- "JSON over gRPC" is a bit of an ambiguous phrase. It can be a very general
-- term, simply meaning using an otherwise-unspecified JSON encoding, or it can
-- refer to "Protobuf over JSON" (see
-- <https://protobuf.dev/programming-guides/proto3/#json>). In this module we
-- deal with the former, and don't deal with anything Protobuf-specific at all,
-- nor do we rely on any of the infrastructure generated by the Protobuf
-- compiler (in other words, there is no need to use @protoc@). See
-- <https://grpc.io/blog/grpc-with-json/> for a Java example of using gRPC with
-- JSON without Protobuf.
--
-- In the absence of the infrastructure provided by @protoc@, you will need to
-- manually provide 'Input' and 'Output' instances for each RPC you use.
-- For example:
--
-- > type Create   = JsonRpc KeyValueService "Create"
-- > type Delete   = JsonRpc KeyValueService "Delete"
-- > ..
-- >
-- > type instance Input  Create   = ..
-- > type instance Output Create   = ..
-- > type instance Input  Retrieve = ..
-- > type instance Output Retrieve = ..
-- > ..
--
-- On the client, you will need 'ToJSON' instances for inputs and 'FromJSON'
-- instances for outputs; on the server the situation is dual. You may find it
-- convenient to use t'JsonObject' (but this is certainly not required).
--
-- TODO: <https://github.com/well-typed/grapesy/issues/166>
-- We don't currently offer explicit support for "Protobuf JSON".
data JsonRpc (serv :: Symbol) (meth :: Symbol)

instance ( KnownSymbol serv
         , KnownSymbol meth

           -- Serialization
         , NFData (Input  (JsonRpc serv meth))
         , NFData (Output (JsonRpc serv meth))

           -- Debugging constraints
         , Show (Input  (JsonRpc serv meth))
         , Show (Output (JsonRpc serv meth))
         , Show (RequestMetadata (JsonRpc serv meth))
         , Show (ResponseInitialMetadata (JsonRpc serv meth))
         , Show (ResponseTrailingMetadata (JsonRpc serv meth))
         ) => IsRPC (JsonRpc serv meth) where
  rpcContentType _ = defaultRpcContentType "json"
  rpcServiceName _ = BS.Char8.pack $ symbolVal (Proxy @serv)
  rpcMethodName  _ = BS.Char8.pack $ symbolVal (Proxy @meth)
  rpcMessageType _ = Nothing

instance ( IsRPC (JsonRpc serv meth)

           -- Serialization constraints
         , ToJSON   (Input  (JsonRpc serv meth))
         , FromJSON (Output (JsonRpc serv meth))

           -- Metadata constraints
         , BuildMetadata (RequestMetadata          (JsonRpc serv meth))
         , ParseMetadata (ResponseInitialMetadata  (JsonRpc serv meth))
         , ParseMetadata (ResponseTrailingMetadata (JsonRpc serv meth))
         ) => SupportsClientRpc (JsonRpc serv meth) where
  rpcSerializeInput    _ = Aeson.encode
  rpcDeserializeOutput _ = Aeson.eitherDecode

instance ( IsRPC (JsonRpc serv meth)

           -- Serialization constraints
         , FromJSON (Input  (JsonRpc serv meth))
         , ToJSON   (Output (JsonRpc serv meth))

           -- Metadata constraints
         , ParseMetadata (RequestMetadata           (JsonRpc serv meth))
         , BuildMetadata (ResponseInitialMetadata   (JsonRpc serv meth))
         , StaticMetadata (ResponseTrailingMetadata (JsonRpc serv meth))
         ) => SupportsServerRpc (JsonRpc serv meth) where
  rpcDeserializeInput _ = Aeson.eitherDecode
  rpcSerializeOutput  _ = Aeson.encode

-- | For JSON protocol we do not check communication protocols
instance ValidStreamingType styp
      => SupportsStreamingType (JsonRpc serv meth) styp

{-------------------------------------------------------------------------------
  Support for constructing JSON objects
-------------------------------------------------------------------------------}

-- | Convenient way to construct JSON values
--
-- Example:
--
-- > type instance Input Create =
-- >   JsonObject '[ '("key"   , Required Key)
-- >               , '("value" , Required Value)
-- >               ]
data JsonObject :: [(Symbol, Type)] -> Type where
  JsonObject :: JsonObject '[]
  (:*) :: forall f x fs. x -> JsonObject fs -> JsonObject ('(f, x) : fs)

instance Show (JsonObject '[]) where
  showsPrec _ JsonObject = showString "JsonObject"

instance (Show x, Show (JsonObject fs))
      => Show (JsonObject ('(f, x) : fs)) where
  showsPrec p (x :* xs) = showParen (p >= 6) $
        showsPrec 6 x
      . showString " :* "
      . showsPrec 6 xs

instance NFData (JsonObject '[]) where
  rnf JsonObject = ()

instance (NFData x, NFData (JsonObject fs))
      => NFData (JsonObject ('(f, x) : fs)) where
  rnf (x :* xs) = rnf (x, xs)

-- | Required field
newtype Required a = Required {
     getRequired :: a
   }
 deriving stock (Show)
 deriving newtype (NFData)

-- | Optional field
--
-- 'Maybe' will be represented by the /absence/ of the field in the object.
newtype Optional a = Optional {
      getOptional :: Maybe a
    }
 deriving stock (Show)
 deriving newtype (NFData)

infixr 5 :*

-- | Auxiliary class used for the 'ToJSON' instance for t'JsonObject'
--
-- It is not possible (nor necessary) to define additional instances.
class EncodeFields fs where
  encodeFields :: JsonObject fs -> [Aeson.Pair]
  encodeFields = undefined

instance EncodeFields '[] where
  encodeFields JsonObject = []

instance (KnownSymbol f, ToJSON x, EncodeFields fs)
      => EncodeFields ('(f, Required x) : fs) where
  encodeFields (Required x :* xs) =
        (fromString (symbolVal (Proxy @f)) .= x)
      : encodeFields xs

instance (KnownSymbol f, ToJSON x, EncodeFields fs)
      => EncodeFields ('(f, Optional x) : fs) where
  encodeFields (Optional Nothing  :* xs) = encodeFields xs
  encodeFields (Optional (Just x) :* xs) =
        (fromString (symbolVal (Proxy @f)) .= x)
      : encodeFields xs

instance EncodeFields fs => ToJSON (JsonObject fs) where
  toJSON = Aeson.object . encodeFields

-- | Auxiliary class used for the 'FromJSON' instance for t'JsonObject'
--
-- It is not possible (nor necessary) to define additional instances.
class DecodeFields fs where
  decodeFields :: Aeson.Object -> Aeson.Parser (JsonObject fs)
  decodeFields = undefined

instance DecodeFields '[] where
  decodeFields _ = return JsonObject

instance (KnownSymbol f, FromJSON x, DecodeFields fs)
      => DecodeFields ('(f, Required x) : fs) where
  decodeFields obj = do
      fs <- decodeFields obj
      x  <- obj .: fromString (symbolVal (Proxy @f))
      return (Required x :* fs)

instance (KnownSymbol f, FromJSON x, DecodeFields fs)
      => DecodeFields ('(f, Optional x) : fs) where
  decodeFields obj = do
      fs <- decodeFields obj
      x  <- obj .:? fromString (symbolVal (Proxy @f))
      return (Optional x :* fs)

instance DecodeFields fs => FromJSON (JsonObject fs) where
  parseJSON = Aeson.withObject "JsonObject" $ decodeFields