packages feed

thrift-compiler-0.1.0.1: Thrift/Compiler/GenUnion.hs

{-
  Copyright (c) Meta Platforms, Inc. and affiliates.
  All rights reserved.

  This source code is licensed under the BSD-style license found in the
  LICENSE file in the root directory of this source tree.
-}

module Thrift.Compiler.GenUnion
  ( genUnionDecl
  , genUnionImports
  ) where

import Prelude hiding (exp)
import Data.Maybe
import Data.Text (Text)
import Language.Haskell.Exts.Syntax hiding
  (Name, Type, Annotation, Decl)
import qualified Data.Set as Set
import qualified Language.Haskell.Exts.Syntax as HS

import Thrift.Compiler.GenStruct
import Thrift.Compiler.GenUtils
import Thrift.Compiler.Plugins.Haskell
import Thrift.Compiler.Types

-- Generate Datatype -----------------------------------------------------------

genUnionImports :: HS Union -> Set.Set Import
genUnionImports Union{..} =
  foldr (Set.union . getImports) baseImports unionAlts
  where
    getImports :: HS UnionAlt -> Set.Set Import
    getImports UnionAlt{..} = typeToImport altResolvedType
    baseImports = Set.fromList
                  [ QImport "Prelude" "Prelude"
                  , QImport "Control.DeepSeq" "DeepSeq"
                  , QImport "Control.Exception" "Exception"
                  , QImport "Data.Aeson" "Aeson"
                  , QImport "Data.Aeson.Types" "Aeson"
                  , QImport "Data.Default" "Default"
                  , QImport "Data.Hashable" "Hashable"
                  , QImport "Data.HashMap.Strict" "HashMap"
                  , QImport "Data.List" "List"
                  , QImport "Data.Ord" "Ord"
                  , SymImport "Prelude" [ ".", "<$>", "<*>", ">>=", "==", "++" ]
                  , SymImport "Data.Aeson" [ ".:", ".=" ]
                  , SymImport "Data.Monoid" [ "<>" ]
                  ]

genUnionDecl :: HS Union -> [HS.Decl ()]
genUnionDecl union@Union{..} =
  -- Union Declaration
  [ DataDecl () (DataType ()) Nothing
    (DHead () $ textToName unionResolvedName)
    -- Data Constructors
    (map genAltDecl unionAlts ++
     case unionHasEmpty of
       NonEmpty -> []
       HasEmpty ->
         [ QualConDecl () Nothing Nothing $
           ConDecl () (textToName unionEmptyName) []
         ])

    -- Deriving
    (pure $ deriving_ $ map (IRule () Nothing Nothing . IHCon ()) $
           [ qualSym "Prelude" "Eq"
           , qualSym "Prelude" "Show"
           ] ++
           [ qualSym "Prelude" "Ord" | deriveOrd ])
  -- Aeson Instances
  , genToJSONInst union
  -- ThriftStruct Instance
  , genThriftStruct union
  -- Other Instances
  , genNFData union
  , genDefault union
  , genHashable union
  ] ++
  [ genOrd union | not deriveOrd ]
  where
    deriveOrd =
      all (\UnionAlt{..} -> isNothing $ mkOrd altResolvedType) unionAlts

genAltDecl :: HS UnionAlt -> QualConDecl ()
genAltDecl UnionAlt{..} =
  QualConDecl () Nothing Nothing $
  ConDecl () (textToName altResolvedName) [ genType altResolvedType ]

-- Generate Aeson Instances ----------------------------------------------------

genToJSONInst :: HS Union -> HS.Decl ()
genToJSONInst Union{..} =
  InstDecl () Nothing
    (IRule () Nothing Nothing $
     IHApp ()
       (IHCon () $ qualSym "Aeson" "ToJSON")
       (TyCon () $ unqualSym unionResolvedName))
    (Just [ InsDecl () $ FunBind () $
            map genToJSONAlt unionAlts ++
            case unionHasEmpty of
              NonEmpty -> []
              HasEmpty -> [ genToJSONEmpty unionEmptyName ]
          ])

genToJSONAlt :: HS UnionAlt -> Match ()
genToJSONAlt UnionAlt{..} =
  Match () (textToName "toJSON")
  [ PApp () (unqualSym altResolvedName) [ pvar arg ] ]
  (UnGuardedRhs () $
     qvar "Aeson" "object" `app`
     HS.List ()
     [ infixApp ".=" (stringLit altName) $
       case fixToJSONValue altResolvedType of
         Nothing -> var arg
         Just f  -> f `app` var arg
     ])
  Nothing
  where
    arg = "__" <> altName

genToJSONEmpty :: Text -> Match ()
genToJSONEmpty uname =
  Match () (textToName "toJSON")
  [ PApp () (unqualSym uname) [] ]
  (UnGuardedRhs () $
     qvar "Aeson" "object" `app` HS.List () [])
  Nothing

-- Generate ThriftStruct Instance ----------------------------------------------

genThriftStruct :: HS Union -> HS.Decl ()
genThriftStruct union@Union{..} =
  InstDecl () Nothing
    (IRule () Nothing Nothing $
     IHApp ()
       (IHCon () $ qualSym "Thrift" "ThriftStruct")
       (TyCon () $ unqualSym unionResolvedName))
    (Just $ map (InsDecl ())
     [ genBuilder union
     , genParser union
     ])

genBuilder :: HS Union -> HS.Decl ()
genBuilder Union{..} =
  FunBind () $
  (flip map unionAlts $ \UnionAlt{..} ->
    let arg = "__" <> altName in
    mkMatch (PApp () (unqualSym altResolvedName) [ pvar arg ])
    [ genFieldBase altResolvedType altName altId lastId (var arg) ]) ++
  case unionHasEmpty of
    NonEmpty -> []
    HasEmpty -> [ mkMatch (PApp () (unqualSym unionEmptyName) []) [] ]
  where
    lastId = intLit (0 :: Int)
    mkMatch pat fields =
      Match () (textToName "buildStruct")
      [ pvar "_proxy", pat ]
      (UnGuardedRhs () $
       protocolFun "genStruct" `app`
       HS.List () fields)
      Nothing

genParser :: HS Union -> HS.Decl ()
genParser Union{..} =
  FunBind ()
  [ Match ()
    (textToName "parseStruct")
    [ pvar "_proxy" ]
    (UnGuardedRhs () $ Do ()
     [ Generator () (pvar "_fieldBegin") $
       protocolFun "parseFieldBegin" `app` lastId `app` var "_idMap"
     , Qualifier () $
       Case () (var "_fieldBegin")
       [ Alt ()
         (PApp () (qualSym "Thrift" "FieldBegin")
          [ pvar "_type", pvar "_id", pvar "_bool" ])
         (UnGuardedRhs () $ Do ()
          [ Qualifier () $ Case () (var "_id") $
            map genParseValue unionAlts ++
            [ Alt () (PWildCard ())
              (UnGuardedRhs () $ case unionHasEmpty of
                NonEmpty ->
                  qvar "Prelude" "fail" `app`
                  (infixApp "++"
                   (stringLit
                    ("unrecognized alternative for union '" <>
                     unionName <>
                     "': "))
                   (qvar "Prelude" "show" `app` var "_id"))
                HasEmpty -> Do ()
                  [ Qualifier () $
                      protocolFun "parseSkip" `app`
                      var "_type" `app`
                      qcon "Prelude" "Nothing"
                  , Qualifier () $ protocolFun "parseStop"
                  , Qualifier () $
                      qvar "Prelude" "return" `app` con unionEmptyName
                  ])
              Nothing
            ]
          ])
         Nothing
       , Alt () (PApp () (qualSym "Thrift" "FieldEnd") [])
         (UnGuardedRhs () $
          case unionHasEmpty of
            NonEmpty -> qvar "Prelude" "fail" `app`
                        stringLit ("union '" <> unionName <> "' is empty")
            HasEmpty -> qvar "Prelude" "return" `app` con unionEmptyName)
         Nothing
       ]
     ])
    (Just $ BDecls ()
     [ PatBind () (pvar "_idMap")
       (UnGuardedRhs () $
        qvar "HashMap" "fromList" `app`
        HS.List ()
        (map (\UnionAlt{..} ->
         Tuple () Boxed [ stringLit altName, intLit altId ])
         unionAlts))
       Nothing
     ])
  ]
  where
    lastId = intLit (0 :: Int)

genParseValue :: HS UnionAlt -> Alt ()
genParseValue UnionAlt{..} =
  Alt ()
  (PLit ()
   (if altId < 0 then Negative () else Signless ())
   (Int () (abs $ fromIntegral altId) (show altId)))
  (GuardedRhss ()
   -- check that the parsed type is correct
   [ GuardedRhs ()
     [ Qualifier () $
       infixApp "==" (var "_type") (genThriftType altResolvedType)
     ] $ Do ()
     [ Generator () (pvar "_val") $ genParseType P_FieldMode altResolvedType
     , Qualifier () $ protocolFun "parseStop"
     , Qualifier () $
       qvar "Prelude" "return" `app`
       (con altResolvedName `app` var "_val")
     ]
   ])
  Nothing

-- Generate NFData Instance ----------------------------------------------------

genNFData :: HS Union -> HS.Decl ()
genNFData Union{..} =
  InstDecl () Nothing
    (IRule () Nothing Nothing $
     IHApp ()
       (IHCon () $ qualSym "DeepSeq" "NFData")
       (TyCon () $ unqualSym unionResolvedName))
    (Just
     [ InsDecl () $ FunBind () $
       (flip map unionAlts $ \UnionAlt{..} ->
         Match () (textToName "rnf")
         [ PApp () (unqualSym altResolvedName) [ pvar ("__" <> altName) ] ]
         (UnGuardedRhs () $ qvar "DeepSeq" "rnf" `app` var ("__" <> altName))
         Nothing) ++
       case unionHasEmpty of
         NonEmpty -> []
         HasEmpty ->
           [ Match () (textToName "rnf")
             [ PApp () (unqualSym unionEmptyName) [] ]
             (UnGuardedRhs () $ unit_con ())
             Nothing
           ]
     ])

-- Generate Default Instance ---------------------------------------------------

genDefault :: HS Union -> HS.Decl ()
genDefault Union{..} =
  InstDecl () Nothing
    (IRule () Nothing Nothing $
     IHApp ()
       (IHCon () $ qualSym "Default" "Default")
       (TyCon () $ unqualSym unionResolvedName))
    (Just
     [ InsDecl () $ FunBind ()
       [ Match () (textToName "def") []
         (UnGuardedRhs () $
          case (unionHasEmpty, unionAlts) of
            (HasEmpty, _) -> con unionEmptyName
            (NonEmpty, UnionAlt{..} : _) ->
              con altResolvedName `app` typeToDefault altResolvedType
            (NonEmpty, []) ->
              qvar "Exception" "throw" `app`
              (qcon "Thrift" "ProtocolException" `app`
               stringLit
               ("def: no default value for empty union '" <> unionResolvedName
                <> "'")))
         Nothing
       ]
     ])

-- Generate Default Instance ---------------------------------------------------

genHashable :: HS Union -> HS.Decl ()
genHashable Union{..} =
  InstDecl () Nothing
  (IRule () Nothing Nothing $
   IHApp ()
   (IHCon () $ qualSym "Hashable" "Hashable")
   (TyCon () $ unqualSym unionResolvedName))
  (Just
   [ InsDecl () $ FunBind () $
     map genHashWithSalt unionAlts ++
     case unionHasEmpty of
       NonEmpty -> []
       HasEmpty -> [ genHashWithSaltEmpty unionEmptyName ]
   ])

genHashWithSalt :: HS UnionAlt -> Match ()
genHashWithSalt UnionAlt{..} =
  Match () (textToName "hashWithSalt")
  [ pvar "__salt"
  , PApp () (unqualSym altResolvedName)
    [ pvar $ "_" <> altName ]
  ]
  (UnGuardedRhs () $
   qvar "Hashable" "hashWithSalt" `app`
   var "__salt" `app`
   (qvar "Hashable" "hashWithSalt" `app`
    intLit altId `app`
    transformValue mkHashable Default altResolvedType (var $ "_" <> altName)))
  Nothing

genHashWithSaltEmpty :: Text -> Match ()
genHashWithSaltEmpty uname =
  Match () (textToName "hashWithSalt")
  [ pvar "__salt"
  , PApp () (unqualSym uname) []
  ]
  (UnGuardedRhs () $
   qvar "Hashable" "hashWithSalt" `app`
   var "__salt" `app`
   ExpTypeSig () (intLit (0 ::Int)) (qualType "Prelude" "Int"))
  Nothing

-- Generate Default Instance ---------------------------------------------------

genOrd :: HS Union -> HS.Decl ()
genOrd Union{..} =
  InstDecl () Nothing
  (IRule () Nothing Nothing $
   IHApp ()
   (IHCon () $ qualSym "Ord" "Ord")
   (TyCon () $ unqualSym unionName))
  (Just
   [ InsDecl () $ FunBind () $
     concatMap genCompare unionAlts ++
     case unionHasEmpty of
       NonEmpty -> []
       HasEmpty -> genCompareEmpty unionEmptyName
   ])

genCompare :: HS UnionAlt -> [Match ()]
genCompare UnionAlt{..} =
  [ Match () (textToName "compare")
    [ PApp () (unqualSym altResolvedName) [pvar "__a"]
    , PApp () (unqualSym altResolvedName) [pvar "__b"]
    ]
    (UnGuardedRhs () $
     qvar "Ord" "compare" `app`
     transform (var "__a") `app`
     transform (var "__b"))
    Nothing
  , Match () (textToName "compare")
    [ PApp () (unqualSym altResolvedName) [PWildCard ()]
    , PWildCard ()
    ]
    (UnGuardedRhs () $ qcon "Ord" "LT")
    Nothing
  ]
  where
    transform = transformValue mkOrd Default altResolvedType

genCompareEmpty :: Text -> [Match ()]
genCompareEmpty uname =
  [ Match () (textToName "compare")
    [ PApp () (unqualSym uname) []
    , PApp () (unqualSym uname) []
    ]
    (UnGuardedRhs () $ qcon "Ord" "EQ")
    Nothing
  , Match () (textToName "compare")
    [ PApp () (unqualSym uname) []
    , PWildCard ()
    ]
    (UnGuardedRhs () $ qcon "Ord" "GT")
    Nothing
  ]