packages feed

wireform-proto-0.2.0.0: src/Proto/Schema.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

{- | Schema metadata system.

Retains full proto schema information at the type level and term level.

Each generated message carries:

* Its fully-qualified proto name
* Its package name
* A list of field descriptors (name, number, type, label)
* The raw file descriptor bytes (for interop with other proto tools)
* A default value

Each field is accessible via the 'HasField' typeclass, which provides
a getter, setter, and field descriptor.
-}
module Proto.Schema (
  -- * Message metadata
  ProtoMessage (..),

  -- * Field access
  HasField (..),

  -- * Field descriptors
  FieldDescriptor (..),
  FieldTypeDescriptor (..),
  ScalarFieldType (..),
  FieldLabel' (..),
  FieldAccessor (..),
  SomeFieldDescriptor (..),

  -- * Enum metadata
  ProtoEnum (..),

  -- * Service metadata
  ProtoService (..),
  MethodDescriptor (..),

  -- * Querying
  lookupFieldDescriptor,
  fieldDescriptorByNumber,
  messageFieldNames,
  messageFieldNumbers,
) where

import Data.ByteString (ByteString)
import Data.Kind (Type)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import GHC.TypeLits (Symbol)


{- | Typeclass carrying full proto schema metadata for a message type.

Generated code provides instances that make all schema information
available at runtime without parsing .proto files.
-}
class ProtoMessage a where
  -- | Fully-qualified proto message name (e.g. @"example.Person"@).
  protoMessageName :: Proxy a -> Text


  -- | The proto package this message belongs to.
  protoPackageName :: Proxy a -> Text
  protoPackageName _ = ""


  -- | All field descriptors, keyed by field number.
  protoFieldDescriptors :: Proxy a -> Map Int (SomeFieldDescriptor a)


  {- | The raw serialized FileDescriptorProto bytes.
  Can be fed to other proto tools for interop.
  -}
  protoFileDescriptorBytes :: Proxy a -> ByteString
  protoFileDescriptorBytes _ = ""


  -- | Default value with all fields at their proto default.
  protoDefaultValue :: a


{- | Type-safe field access via plain get\/set functions.

@HasField msg "fieldName" fieldType@ means the message @msg@ has a
field named @"fieldName"@ with Haskell type @fieldType@. For lens-style
access, see "Proto.Lens".
-}
class HasField (msg :: Type) (name :: Symbol) (a :: Type) | msg name -> a where
  -- | Get the field value.
  getField :: msg -> a


  -- | Set the field value, returning a new message.
  setField :: a -> msg -> msg


  -- | The field descriptor for this field.
  fieldDescriptor :: Proxy msg -> Proxy name -> FieldDescriptor msg a


-- | A field descriptor carrying the proto metadata for one field.
data FieldDescriptor msg a = FieldDescriptor
  { fdName :: !Text
  -- ^ Proto field name (snake_case)
  , fdNumber :: !Int
  -- ^ Proto field number
  , fdTypeDesc :: !FieldTypeDescriptor
  , fdLabel :: !FieldLabel'
  , fdGet :: msg -> a
  -- ^ Accessor function
  , fdSet :: a -> msg -> msg
  -- ^ Setter function
  }


-- | Existentially-wrapped field descriptor for heterogeneous collections.
data SomeFieldDescriptor msg where
  SomeField :: FieldDescriptor msg a -> SomeFieldDescriptor msg


-- | Proto field type descriptor.
data FieldTypeDescriptor
  = ScalarType !ScalarFieldType
  | -- | Fully-qualified message type name
    MessageType !Text
  | -- | Fully-qualified enum type name
    EnumType !Text
  | -- | Map<key, value>
    MapType !ScalarFieldType !FieldTypeDescriptor
  deriving stock (Show, Eq)


-- | Scalar field types matching the proto spec.
data ScalarFieldType
  = DoubleField
  | FloatField
  | Int32Field
  | Int64Field
  | UInt32Field
  | UInt64Field
  | SInt32Field
  | SInt64Field
  | Fixed32Field
  | Fixed64Field
  | SFixed32Field
  | SFixed64Field
  | BoolField
  | StringField
  | BytesField
  deriving stock (Show, Eq, Ord, Enum, Bounded)


-- | Field label (cardinality).
data FieldLabel'
  = LabelOptional
  | LabelRequired
  | LabelRepeated
  deriving stock (Show, Eq, Ord)


-- | How a field is accessed in the record.
data FieldAccessor
  = -- | Direct record field
    PlainField
  | -- | Wrapped in Maybe
    OptionalField
  | -- | Wrapped in Vector
    RepeatedField
  | -- | Wrapped in Map
    MapField'
  deriving stock (Show, Eq, Ord)


-- | Full metadata for a proto enum type.
class ProtoEnum a where
  -- | Fully-qualified proto enum name.
  protoEnumName :: Proxy a -> Text


  -- | All enum value names and their numeric values.
  protoEnumValues :: Proxy a -> [(Text, Int)]


  -- | Convert from proto numeric value.
  fromProtoEnumValue :: Int -> Maybe a


  -- | Convert to proto numeric value.
  toProtoEnumValue :: a -> Int


-- | Metadata for a proto service.
class ProtoService a where
  protoServiceName :: Proxy a -> Text
  protoServiceMethods :: Proxy a -> [MethodDescriptor]


-- | Metadata for a single RPC method.
data MethodDescriptor = MethodDescriptor
  { mdName :: !Text
  , mdInputType :: !Text
  , mdOutputType :: !Text
  , mdClientStreaming :: !Bool
  , mdServerStreaming :: !Bool
  }
  deriving stock (Show, Eq)


-- | Look up a field descriptor by name.
lookupFieldDescriptor :: ProtoMessage a => Text -> Proxy a -> Maybe (SomeFieldDescriptor a)
lookupFieldDescriptor name p =
  let descs = Map.elems (protoFieldDescriptors p)
  in case filter (\(SomeField fd) -> fdName fd == name) descs of
       (d : _) -> Just d
       [] -> Nothing


-- | Look up a field descriptor by field number.
fieldDescriptorByNumber :: ProtoMessage a => Int -> Proxy a -> Maybe (SomeFieldDescriptor a)
fieldDescriptorByNumber num p = Map.lookup num (protoFieldDescriptors p)


-- | All field names in a message.
messageFieldNames :: ProtoMessage a => Proxy a -> [Text]
messageFieldNames p = fmap (\(SomeField fd) -> fdName fd) (Map.elems (protoFieldDescriptors p))


-- | All field numbers in a message.
messageFieldNumbers :: ProtoMessage a => Proxy a -> [Int]
messageFieldNumbers p = Map.keys (protoFieldDescriptors p)