{-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -fno-warn-warnings-deprecations #-} {-| Combines the construction of RPC server components and their Python stubs. -} {- Copyright (C) 2013 Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -} module Ganeti.THH.PyRPC ( genPyUDSRpcStub , genPyUDSRpcStubStr ) where import Control.Monad import Data.Char (toLower, toUpper) import Data.Functor import Data.Maybe (fromMaybe) import Language.Haskell.TH import Language.Haskell.TH.Syntax (liftString) import Text.PrettyPrint import Ganeti.THH.Types -- | The indentation step in generated Python files. pythonIndentStep :: Int pythonIndentStep = 2 -- | A helper function that nests a block of generated output by the default -- step (see 'pythonIndentStep'). nest' :: Doc -> Doc nest' = nest pythonIndentStep -- | The name of an abstract function to which all method in a Python stub -- are forwarded to. genericInvokeName :: String genericInvokeName = "_GenericInvoke" -- | The name of a function that returns the socket path for reaching the -- appropriate RPC client. socketPathName :: String socketPathName = "_GetSocketPath" -- | Create a Python expression that applies a given function to a list of -- given expressions apply :: String -> [Doc] -> Doc apply name as = text name <> parens (hcat $ punctuate (text ", ") as) -- | An empty line block. emptyLine :: Doc emptyLine = text "" -- apparently using 'empty' doesn't work lowerFirst :: String -> String lowerFirst (x:xs) = toLower x : xs lowerFirst [] = [] upperFirst :: String -> String upperFirst (x:xs) = toUpper x : xs upperFirst [] = [] -- | Creates a method declaration given a function name and a list of -- Haskell types corresponding to its arguments. toFunc :: String -> [Type] -> Q Doc toFunc fname as = do args <- zipWithM varName [1..] as let args' = text "self" : args callName = lowerFirst fname return $ (text "def" <+> apply fname args') <> colon $+$ nest' (text "return" <+> text "self." <> apply genericInvokeName (text (show callName) : args) ) where -- | Create a name for a method argument, given its index position -- and Haskell type. varName :: Int -> Type -> Q Doc varName _ (VarT n) = lowerFirstNameQ n varName _ (ConT n) = lowerFirstNameQ n varName idx (AppT ListT t) = listOf idx t varName idx (AppT (ConT n) t) | n == ''[] = listOf idx t | otherwise = kind1Of idx n t varName idx (AppT (AppT (TupleT 2) t) t') = pairOf idx t t' varName idx (AppT (AppT (ConT n) t) t') | n == ''(,) = pairOf idx t t' varName idx t = do report False $ "Don't know how to make a Python variable name from " ++ show t ++ "; using a numbered one." return $ text ('_' : show idx) -- | Create a name for a method argument, knowing that its a list of -- a given type. listOf :: Int -> Type -> Q Doc listOf idx t = (<> text "List") <$> varName idx t -- | Create a name for a method argument, knowing that its wrapped in -- a type of kind @* -> *@. kind1Of :: Int -> Name -> Type -> Q Doc kind1Of idx name t = (<> text (nameBase name)) <$> varName idx t -- | Create a name for a method argument, knowing that its a pair of -- the given types. pairOf :: Int -> Type -> Type -> Q Doc pairOf idx t t' = do tn <- varName idx t tn' <- varName idx t' return $ tn <> text "_" <> tn' <> text "_Pair" lowerFirstNameQ :: Name -> Q Doc lowerFirstNameQ = return . text . lowerFirst . nameBase -- | Creates a method declaration by inspecting (reifying) Haskell's function -- name. nameToFunc :: Name -> Q Doc nameToFunc name = do (as, _) <- funArgs `liftM` typeOfFun name -- If the function has just one argument, try if it isn't a tuple; -- if not, use the arguments as they are. let as' = fromMaybe as $ case as of [t] -> tupleArgs t -- TODO CHECK! _ -> Nothing toFunc (upperFirst $ nameBase name) as' -- | Generates a Python class stub, given a class name, the list of Haskell -- functions to expose as methods, and a optionally a piece of code to -- include. namesToClass :: String -- ^ the class name -> Doc -- ^ Python code to include in the class -> [Name] -- ^ the list of functions to include -> Q Doc namesToClass cname pycode fns = do fnsCode <- mapM (liftM ($+$ emptyLine) . nameToFunc) fns return $ vcat [ text "class" <+> apply cname [text "object"] <> colon , nest' ( pycode $+$ vcat fnsCode ) ] -- | Takes a list of function names and creates a RPC handler that delegates -- calls to them, as well as writes out the corresponding Python stub. -- -- See 'mkRpcM' for the requirements on the passed functions and the returned -- expression. genPyUDSRpcStub :: String -- ^ the name of the class to be generated -> String -- ^ the name of the constant from @constants.py@ holding -- the path to a UDS socket -> [Name] -- ^ names of functions to include -> Q Doc genPyUDSRpcStub className constName = liftM (header $+$) . namesToClass className stubCode where header = text "# This file is automatically generated, do not edit!" $+$ text "# pylint: disable-all" stubCode = abstrMethod genericInvokeName [ text "method", text "*args"] $+$ method socketPathName [] ( text "from ganeti import pathutils" $+$ text "return" <+> text "pathutils." <> text constName) method name args body = text "def" <+> apply name (text "self" : args) <> colon $+$ nest' body $+$ emptyLine abstrMethod name args = method name args $ text "raise" <+> apply "NotImplementedError" [] -- The same as 'genPyUDSRpcStub', but returns the result as a @String@ -- expression. genPyUDSRpcStubStr :: String -- ^ the name of the class to be generated -> String -- ^ the constant in @pathutils.py@ holding the socket path -> [Name] -- ^ functions to include -> Q Exp genPyUDSRpcStubStr className constName names = liftString . render =<< genPyUDSRpcStub className constName names