language-thrift-0.7.0.0: examples/generateHaskellTypes.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Main (main) where
-- This is a fairly simple code generator that uses language-thrift to parse
-- IDL files and produces pretty printed Haskell code for all the types.
--
-- For services, it just generates a simple GADT that defines the inputs and
-- outputs for that service. Service inheritance is not supported and service
-- method exceptions are ignored.
#if __GLASGOW_HASKELL__ >= 709
import Prelude hiding ((<$>))
#endif
import Data.Char (toLower, toUpper)
import Data.Maybe (isNothing)
import Data.Text (Text, unpack)
import System.Exit (exitFailure)
import System.IO (stderr, stdout)
import qualified Data.Text as Text
import qualified Text.PrettyPrint.ANSI.Leijen as AnsiPP
import Text.PrettyPrint.Leijen hiding (list, tupled)
import Text.Trifecta (Result (..), parseString)
import Text.Trifecta.Delta (Delta (Directed))
import Language.Thrift.Parser.Trifecta (thriftIDL)
import qualified Language.Thrift.Types as T
die :: String -> IO a
die s = putStrLn s >> exitFailure
-- | '<$>' with the arguments flipped.
(<&>) :: Functor f => f a -> (a -> b) -> f b
(<&>) = flip fmap
infixl 1 <&>
($$) :: T.Docstring -> Doc -> Doc
($$) Nothing y = y
($$) (Just t) y = case Text.lines t of
[] -> y
l:ls -> let docstring = align . vsep
$ (text "-- |" <+> text (unpack l))
: map ((text "--" <+>) . text . unpack) ls
in align (docstring <$> y)
infixr 1 $$
list :: [Doc] -> Doc
list = encloseSep lbracket rbracket (text ", ")
tupled :: [Doc] -> Doc
tupled = encloseSep lparen rparen (text ", ")
renderConstValue :: T.ConstValue a -> Doc
renderConstValue (T.ConstInt i _) = integer i
renderConstValue (T.ConstFloat f _) = double f
renderConstValue (T.ConstLiteral l _) = dquotes $ text (unpack l) -- TODO escaping
renderConstValue (T.ConstIdentifier i _) = text (unpack i)
renderConstValue (T.ConstList l _) = list (map renderConstValue l)
renderConstValue (T.ConstMap m _) = text "Map.fromList" <+> list (map renderConstTuple m)
where
renderConstTuple (a, b) = tupled [renderConstValue a, renderConstValue b]
renderTypeReference :: Show a => T.TypeReference a -> Doc
renderTypeReference (T.DefinedType t _) = text (unpack t)
renderTypeReference (T.StringType _ _) = text "Text"
renderTypeReference (T.BinaryType _ _) = text "ByteString"
renderTypeReference (T.BoolType _ _) = text "Bool"
renderTypeReference (T.ByteType _ _) = text "Word8"
renderTypeReference (T.I16Type _ _) = text "Word16"
renderTypeReference (T.I32Type _ _) = text "Word32"
renderTypeReference (T.I64Type _ _) = text "Word64"
renderTypeReference (T.DoubleType _ _) = text "Double"
renderTypeReference (T.MapType k v _ _) =
parens $ hsep [text "Map", renderTypeReference k, renderTypeReference v]
renderTypeReference (T.SetType i _ _) = parens $ text "Set" <+> renderTypeReference i
renderTypeReference (T.ListType i _ _) = brackets $ renderTypeReference i
renderTypeReference t = error $ "Unsupported field type: " ++ show t
renderStructField :: Show a => Text -> T.Field a -> Doc
renderStructField structName T.Field{..} = hang 4 $
fieldDocstring $$ name </> hsep
[ text "::"
, (if isOptional
then text "Maybe" <> space
else empty)
<> renderTypeReference fieldValueType
]
where
isOptional
| isNothing fieldRequiredness = False
| otherwise = r == T.Optional && isNothing fieldDefaultValue
where (Just r) = fieldRequiredness
name = text . unpack $ Text.concat [
structName
, underscoresToCamelCase False fieldName
]
renderTypedef :: Show a => T.Typedef a -> Doc
renderTypedef T.Typedef{..} = typedefDocstring $$ hsep
[ text "type"
, typeName typedefName
, equals
, renderTypeReference typedefTargetType
]
renderEnum :: T.Enum a -> Doc
renderEnum T.Enum{..} = enumDocstring $$
text "data" <+> typeName enumName <>
encloseSep (text " = ") empty (text " | ") (map renderDef enumValues)
<$$> indent 4 derivingClause
where
renderDef T.EnumDef{..} = enumDefDocstring $$ typeName enumDefName
renderStruct :: Show a => T.Struct a -> Doc
renderStruct T.Struct{..} = structDocstring $$
text "data" <+> typeName structName </>
equals <+> typeName structName <$$>
(if null structFields
then empty
else indent 2 renderFields) </> derivingClause
-- TODO prefix should be configurable using annotations
where
renderFields = encloseSep (text "{ ") (line <> text "}") (text ", ") $
map (renderStructField $ underscoresToCamelCase True structName)
structFields
renderException :: Show a => T.Exception a -> Doc
renderException T.Exception{..} = renderStruct T.Struct
{ T.structName = exceptionName
, T.structFields = exceptionFields
, T.structAnnotations = exceptionAnnotations
, T.structDocstring = exceptionDocstring
, T.structSrcAnnot = exceptionSrcAnnot
}
renderUnion :: Show a => T.Union a -> Doc
renderUnion T.Union{..} =
hang 4
(unionDocstring $$
text "data" <+> typeName unionName <$>
encloseSep (text "= ") empty (text " | ")
(map renderField unionFields))
<$$> indent 4 derivingClause
where
renderField (T.Field _ _ ftype fname _ _ docstring _) =
docstring $$ fieldName </> renderTypeReference ftype
where
fieldName = text . unpack $ Text.concat
[ underscoresToCamelCase False unionName
, underscoresToCamelCase False fname
]
derivingClause :: Doc
derivingClause =
text "deriving" <+> tupled (map text ["Show", "Ord", "Eq"])
renderType :: Show a => T.Type a -> Doc
renderType (T.TypedefType t) = renderTypedef t
renderType (T.EnumType t) = renderEnum t
renderType (T.ExceptionType t) = renderException t
renderType (T.StructType t) = renderStruct t
renderType (T.UnionType t) = renderUnion t
renderType t = error $ "Unsupported type: " ++ show t
typeName :: Text -> Doc
typeName = mkName False
renderConst :: Show a => T.Const a -> Doc
renderConst T.Const{..} = constDocstring $$
sep [name, text "::", renderTypeReference constValueType] <$>
sep [name, text "=", renderConstValue constValue]
where
name = mkName True constName
renderFunction :: Show a => Text -> T.Function a -> Doc
renderFunction serviceName T.Function{functionOneWay = False, ..} =
functionDocstring $$
typeName functionName <+> text "::" <>
(if null functionParameters
then space
else linebreak <> renderParams) <>
typeName serviceName <+> returnType <> linebreak
where
returnType = case functionReturnType of
Nothing -> text "()"
Just t -> renderTypeReference t
renderParams = indent 2 $
encloseSep (text "{ ") (line <> text "} -> ") (text ", ") $
map (renderStructField structName) functionParameters
where
structName = underscoresToCamelCase True functionName
renderFunction _ f = error $ "Unsupported function: " ++ show f
renderService :: Show a => T.Service a -> Doc
renderService T.Service{serviceExtends = Nothing, ..} = serviceDocstring $$
text "data" <+> typeName serviceName <+> text "a where" <$$>
indent 2 (vcat (map (renderFunction serviceName) serviceFunctions))
renderService s = error $ "Unsupported service: " ++ show s
renderDefinition :: Show a => T.Definition a -> Doc
renderDefinition (T.ConstDefinition c) = renderConst c
renderDefinition (T.TypeDefinition t) = renderType t
renderDefinition (T.ServiceDefinition s) = renderService s
generateOutput :: Show a => T.Program a -> IO ()
generateOutput (T.Program _ definitions) = do
let doc = headers <$> empty <$>
vcat (map ((<$> empty) . renderDefinition) definitions)
displayIO stdout $ renderPretty 0.8 80 doc
where
import_ m items = sep [
text "import"
, text m
, maybe empty (tupled . map string) items
]
importQualified m s = sep [
text "import qualified"
, text m
, text "as"
, text s
]
headers = vcat [
import_ "Data.Map" (Just ["Map"])
, import_ "Data.Set" (Just ["Set"])
, import_ "Data.Text" (Just ["Text"])
, import_ "Data.ByteString" (Just ["ByteString"])
, import_ "Data.Word" (Just ["Word8", "Word16", "Word32", "Word64"])
, text ""
, importQualified "Data.Map" "Map"
]
mkName :: Bool -> Text -> Doc
mkName lowerFirst = text . unpack . underscoresToCamelCase lowerFirst
underscoresToCamelCase :: Bool -> Text -> Text
underscoresToCamelCase lowerFirst =
camelCase lowerFirst . Text.split (== '_')
camelCase :: Bool -> [Text] -> Text
camelCase lowerFirst =
maybeLower . Text.concat . map (transformIndex toUpper 0)
where
maybeLower = if lowerFirst then transformIndex toLower 0 else id
transformIndex :: (Char -> Char) -> Int -> Text -> Text
transformIndex f i s = Text.concat [
Text.take i s
, Text.singleton $ f (s `Text.index` i)
, Text.drop (i + 1) s
]
main :: IO ()
main = do
result <- getContents <&> parseString thriftIDL (Directed "stdin" 0 0 0 0)
case result of
Success p -> generateOutput p
Failure doc -> do
AnsiPP.displayIO stderr $ AnsiPP.renderPretty 0.8 80 doc
die "Parse Failed"