wireform-proto-0.2.0.0: src/Proto/IDL/Descriptor.hs
{- | Convert wireform's AST types to descriptor.proto types and back.
This enables bundling schema metadata with generated types:
the parsed 'ProtoFile' is converted to a 'FileDescriptorProto',
serialized, and embedded in the generated code.
The reverse direction ('fileDescriptorToAST') is used by the protoc
plugin, which receives 'FileDescriptorProto' from protoc and needs
wireform's 'ProtoFile' to drive code generation.
-}
module Proto.IDL.Descriptor (
astToFileDescriptor,
astToDescriptor,
astToFieldDescriptor,
astToEnumDescriptor,
astToServiceDescriptor,
serializeFileDescriptor,
fileDescriptorToAST,
descriptorToMessage,
fieldDescriptorToField,
enumDescriptorToEnum,
serviceDescriptorToService,
) where
import Data.ByteString (ByteString)
import Data.Int (Int32)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Vector qualified as V
import Proto.Encode (encodeMessage)
import Proto.Google.Protobuf.Descriptor
import Proto.IDL.AST
-- | Convert a parsed ProtoFile to a FileDescriptorProto.
astToFileDescriptor :: FilePath -> ProtoFile -> FileDescriptorProto
astToFileDescriptor path pf =
defaultFileDescriptorProto
{ fdpName = T.pack path
, fdpPackage = fromMaybe "" (protoPackage pf)
, fdpDependency = V.fromList (fmap importPath (protoImports pf))
, fdpMessageType = V.fromList (concatMap topMessages (protoTopLevels pf))
, fdpEnumType = V.fromList (concatMap topEnums (protoTopLevels pf))
, fdpService = V.fromList (concatMap topServices (protoTopLevels pf))
, fdpSyntax = syntaxStr (protoSyntax pf)
, fdpEdition = editionStr (protoSyntax pf)
}
where
topMessages (TLMessage msg) = [astToDescriptor msg]
topMessages _ = []
topEnums (TLEnum ed) = [astToEnumDescriptor ed]
topEnums _ = []
topServices (TLService svc) = [astToServiceDescriptor svc]
topServices _ = []
syntaxStr Proto2 = "proto2"
syntaxStr Proto3 = "proto3"
syntaxStr (Editions _) = "editions"
editionStr (Editions ed) = editionName ed
editionStr _ = ""
-- | Convert a MessageDef to a DescriptorProto.
astToDescriptor :: MessageDef -> DescriptorProto
astToDescriptor msg =
defaultDescriptorProto
{ dpName = msgName msg
, dpField = V.fromList (concatMap extractField (msgElements msg))
, dpNestedType = V.fromList (concatMap extractNested (msgElements msg))
, dpEnumType = V.fromList (concatMap extractEnum (msgElements msg))
, dpOneofDecl = V.fromList (concatMap extractOneof (msgElements msg))
}
where
extractField (MEField fd) = [astToFieldDescriptor fd]
extractField (MEMapField mf) = [mapToFieldDescriptor mf]
extractField (MEOneof od) = fmap oneofFieldToFDP (oneofFields od)
extractField _ = []
extractNested (MEMessage inner) = [astToDescriptor inner]
extractNested _ = []
extractEnum (MEEnum ed) = [astToEnumDescriptor ed]
extractEnum _ = []
extractOneof (MEOneof od) = [defaultOneofDescriptorProto {odpName = oneofName od}]
extractOneof _ = []
-- | Convert a FieldDef to a FieldDescriptorProto.
astToFieldDescriptor :: FieldDef -> FieldDescriptorProto
astToFieldDescriptor fd =
defaultFieldDescriptorProto
{ fdpFieldName = fieldName fd
, fdpFieldNumber = fromIntegral (unFieldNumber (fieldNumber fd))
, fdpFieldLabel = labelToInt (fieldLabel fd)
, fdpFieldType = fieldTypeToInt (fieldType fd)
, fdpFieldTypeName = case fieldType fd of
FTNamed n -> n
_ -> ""
}
mapToFieldDescriptor :: MapField -> FieldDescriptorProto
mapToFieldDescriptor mf =
defaultFieldDescriptorProto
{ fdpFieldName = mapFieldName mf
, fdpFieldNumber = fromIntegral (unFieldNumber (mapFieldNum mf))
, fdpFieldLabel = 3
, fdpFieldType = 11
, fdpFieldTypeName = mapFieldName mf <> "Entry"
}
oneofFieldToFDP :: OneofField -> FieldDescriptorProto
oneofFieldToFDP of' =
defaultFieldDescriptorProto
{ fdpFieldName = oneofFieldName of'
, fdpFieldNumber = fromIntegral (unFieldNumber (oneofFieldNumber of'))
, fdpFieldLabel = 1
, fdpFieldType = fieldTypeToInt (oneofFieldType of')
, fdpFieldTypeName = case oneofFieldType of' of
FTNamed n -> n
_ -> ""
}
-- | Convert an EnumDef to an EnumDescriptorProto.
astToEnumDescriptor :: EnumDef -> EnumDescriptorProto
astToEnumDescriptor ed =
defaultEnumDescriptorProto
{ edpName = enumName ed
, edpValue = V.fromList (fmap toEVDP (enumValues ed))
}
where
toEVDP ev = defaultEnumValueDescriptorProto {evdpName = evName ev, evdpNumber = fromIntegral (evNumber ev)}
-- | Convert a ServiceDef to a ServiceDescriptorProto.
astToServiceDescriptor :: ServiceDef -> ServiceDescriptorProto
astToServiceDescriptor svc =
defaultServiceDescriptorProto
{ sdpName = svcName svc
, sdpMethod = V.fromList (fmap toMDP (svcRpcs svc))
}
where
toMDP rpc =
defaultMethodDescriptorProto
{ mdpName = rpcName rpc
, mdpInputType = rpcInput rpc
, mdpOutputType = rpcOutput rpc
, mdpClientStreaming = rpcInputStr rpc == Streaming
, mdpServerStreaming = rpcOutputStr rpc == Streaming
}
-- | Serialize a ProtoFile's schema to bytes (as a FileDescriptorProto).
serializeFileDescriptor :: FilePath -> ProtoFile -> ByteString
serializeFileDescriptor path pf = encodeMessage (astToFileDescriptor path pf)
labelToInt :: Maybe FieldLabel -> Int32
labelToInt Nothing = 1
labelToInt (Just Optional) = 1
labelToInt (Just Required) = 2
labelToInt (Just Repeated) = 3
fieldTypeToInt :: FieldType -> Int32
fieldTypeToInt (FTScalar s) = scalarTypeToInt s
fieldTypeToInt (FTNamed _) = 11
scalarTypeToInt :: ScalarType -> Int32
scalarTypeToInt = \case
SDouble -> 1
SFloat -> 2
SInt64 -> 3
SUInt64 -> 4
SInt32 -> 5
SFixed64 -> 6
SFixed32 -> 7
SBool -> 8
SString -> 9
SBytes -> 12
SUInt32 -> 13
SSFixed32 -> 15
SSFixed64 -> 16
SSInt32 -> 17
SSInt64 -> 18
-- ---------------------------------------------------------------------------
-- Reverse conversion: FileDescriptorProto -> ProtoFile
-- ---------------------------------------------------------------------------
-- | Convert a 'FileDescriptorProto' (as received from protoc) to a 'ProtoFile'.
fileDescriptorToAST :: FileDescriptorProto -> ProtoFile
fileDescriptorToAST fdp =
ProtoFile
{ protoSyntax = parseSyntax (fdpSyntax fdp) (fdpEdition fdp)
, protoPackage = if T.null (fdpPackage fdp) then Nothing else Just (fdpPackage fdp)
, protoImports = fmap (ImportDef () Nothing) (V.toList (fdpDependency fdp))
, protoOptions = []
, protoTopLevels =
fmap (TLMessage . descriptorToMessage) (V.toList (fdpMessageType fdp))
<> fmap (TLEnum . enumDescriptorToEnum) (V.toList (fdpEnumType fdp))
<> fmap (TLService . serviceDescriptorToService) (V.toList (fdpService fdp))
, protoSource = Nothing
}
parseSyntax :: Text -> Text -> Syntax
parseSyntax syn ed
| syn == "proto2" = Proto2
| syn == "editions" = Editions (Edition ed)
| otherwise = Proto3
-- | Convert a 'DescriptorProto' to a 'MessageDef'.
descriptorToMessage :: DescriptorProto -> MessageDef
descriptorToMessage dp =
MessageDef
{ msgExt = ()
, msgDoc = Nothing
, msgName = dpName dp
, msgElements = fieldElems <> nestedElems <> enumElems
}
where
oneofNames = V.toList (fmap odpName (dpOneofDecl dp))
allFields = V.toList (dpField dp)
(oneofFields', regularFields) = partitionOneofFields oneofNames allFields
fieldElems = fmap (MEField . fieldDescriptorToField) regularFields
nestedElems = fmap (MEMessage . descriptorToMessage) (V.toList (dpNestedType dp))
enumElems =
fmap (MEEnum . enumDescriptorToEnum) (V.toList (dpEnumType dp))
<> buildOneofDefs oneofNames oneofFields'
partitionOneofFields :: [Text] -> [FieldDescriptorProto] -> ([[FieldDescriptorProto]], [FieldDescriptorProto])
partitionOneofFields oneofNames fields =
let regular = filter (\f -> fdpFieldOneofIdx f < 0) fields
grouped =
fmap
(\(i, _) -> filter (\f -> fdpFieldOneofIdx f == fromIntegral i) fields)
(zip [0 :: Int ..] oneofNames)
in (grouped, regular)
buildOneofDefs :: [Text] -> [[FieldDescriptorProto]] -> [MessageElement]
buildOneofDefs = zipWith mkOneof
where
mkOneof name flds =
MEOneof
( OneofDef
{ oneofExt = ()
, oneofDoc = Nothing
, oneofName = name
, oneofFields = fmap mkOneofField flds
, oneofOptions = []
}
)
mkOneofField f =
OneofField
{ oneofFieldExt = ()
, oneofFieldDoc = Nothing
, oneofFieldType = intToFieldType (fdpFieldType f) (fdpFieldTypeName f)
, oneofFieldName = fdpFieldName f
, oneofFieldNumber = FieldNumber (fromIntegral (fdpFieldNumber f))
, oneofFieldOptions = []
}
-- | Convert a 'FieldDescriptorProto' to a 'FieldDef'.
fieldDescriptorToField :: FieldDescriptorProto -> FieldDef
fieldDescriptorToField f =
FieldDef
{ fieldExt = ()
, fieldDoc = Nothing
, fieldLabel = intToLabel (fdpFieldLabel f)
, fieldType = intToFieldType (fdpFieldType f) (fdpFieldTypeName f)
, fieldName = fdpFieldName f
, fieldNumber = FieldNumber (fromIntegral (fdpFieldNumber f))
, fieldOptions = jsonNameOpt (fdpFieldJsonName f)
}
jsonNameOpt :: Text -> [OptionDef]
jsonNameOpt t
| T.null t = []
| otherwise = [OptionDef () (OptionName [SimpleOption "json_name"]) (CString t)]
-- | Convert an 'EnumDescriptorProto' to an 'EnumDef'.
enumDescriptorToEnum :: EnumDescriptorProto -> EnumDef
enumDescriptorToEnum e =
EnumDef
{ enumExt = ()
, enumDoc = Nothing
, enumName = edpName e
, enumValues =
fmap
(\v -> EnumValue () Nothing (evdpName v) (fromIntegral (evdpNumber v)) [])
(V.toList (edpValue e))
, enumOptions = []
}
-- | Convert a 'ServiceDescriptorProto' to a 'ServiceDef'.
serviceDescriptorToService :: ServiceDescriptorProto -> ServiceDef
serviceDescriptorToService s =
ServiceDef
{ svcExt = ()
, svcDoc = Nothing
, svcName = sdpName s
, svcRpcs = fmap methodToRpc (V.toList (sdpMethod s))
, svcOptions = []
}
where
methodToRpc m =
RpcDef
{ rpcExt = ()
, rpcDoc = Nothing
, rpcName = mdpName m
, rpcInput = mdpInputType m
, rpcInputStr = if mdpClientStreaming m then Streaming else NoStream
, rpcOutput = mdpOutputType m
, rpcOutputStr = if mdpServerStreaming m then Streaming else NoStream
, rpcOptions = []
}
intToLabel :: Int32 -> Maybe FieldLabel
intToLabel 1 = Just Optional
intToLabel 2 = Just Required
intToLabel 3 = Just Repeated
intToLabel _ = Nothing
intToFieldType :: Int32 -> Text -> FieldType
intToFieldType typeInt typeName = case intToScalar typeInt of
Just s -> FTScalar s
Nothing -> FTNamed (stripLeadingDot typeName)
stripLeadingDot :: Text -> Text
stripLeadingDot t = fromMaybe t (T.stripPrefix "." t)
intToScalar :: Int32 -> Maybe ScalarType
intToScalar = \case
1 -> Just SDouble
2 -> Just SFloat
3 -> Just SInt64
4 -> Just SUInt64
5 -> Just SInt32
6 -> Just SFixed64
7 -> Just SFixed32
8 -> Just SBool
9 -> Just SString
12 -> Just SBytes
13 -> Just SUInt32
15 -> Just SSFixed32
16 -> Just SSFixed64
17 -> Just SSInt32
18 -> Just SSInt64
_ -> Nothing