TH simplification for Luxi
[ganeti-local] / htools / Ganeti / THH.hs
index 7584a2b..82e44a5 100644 (file)
@@ -34,15 +34,20 @@ module Ganeti.THH ( declareSADT
                   , genOpID
                   , genOpCode
                   , noDefault
+                  , genStrOfOp
+                  , genStrOfKey
+                  , genLuxiOp
                   ) where
 
-import Control.Monad (liftM)
+import Control.Monad (liftM, liftM2)
 import Data.Char
 import Data.List
 import Language.Haskell.TH
 
 import qualified Text.JSON as JSON
 
+-- * Helper functions
+
 -- | Ensure first letter is lowercase.
 --
 -- Used to convert type name to function prefix, e.g. in @data Aa ->
@@ -51,6 +56,14 @@ ensureLower :: String -> String
 ensureLower [] = []
 ensureLower (x:xs) = toLower x:xs
 
+-- | Helper for quoted expressions.
+varNameE :: String -> Q Exp
+varNameE = varE . mkName
+
+-- | showJSON as an expression, for reuse.
+showJSONE :: Q Exp
+showJSONE = varNameE "showJSON"
+
 -- | ToString function name.
 toStrName :: String -> Name
 toStrName = mkName . (++ "ToString") . ensureLower
@@ -62,8 +75,17 @@ fromStrName = mkName . (++ "FromString") . ensureLower
 -- | Converts a name to it's varE/litE representations.
 --
 reprE :: Either String Name -> Q Exp
-reprE (Left name) = litE (StringL name)
-reprE (Right name) = varE name
+reprE = either stringE varE
+
+-- | Smarter function application.
+--
+-- This does simply f x, except that if is 'id', it will skip it, in
+-- order to generate more readable code when using -ddump-splices.
+appFn :: Exp -> Exp -> Exp
+appFn f x | f == VarE 'id = x
+          | otherwise = AppE f x
+
+-- * Template code for simple string-equivalent ADTs
 
 -- | Generates a data type declaration.
 --
@@ -183,7 +205,7 @@ genReadJSON name = do
                JSON.Ok s' -> $(varE (fromStrName name)) s'
                JSON.Error e ->
                    JSON.Error $ "Can't parse string value for type " ++
-                           $(litE (StringL name)) ++ ": " ++ e
+                           $(stringE name) ++ ": " ++ e
            |]
   return $ FunD (mkName "readJSON") [Clause [VarP s] (NormalB body) []]
 
@@ -198,37 +220,42 @@ makeJSONInstance name = do
   readJ <- genReadJSON base
   return [InstanceD [] (AppT (ConT ''JSON.JSON) (ConT name)) (readJ:showJ)]
 
+-- * Template code for opcodes
+
 -- | Transforms a CamelCase string into an_underscore_based_one.
 deCamelCase :: String -> String
 deCamelCase =
     intercalate "_" . map (map toUpper) . groupBy (\_ b -> not $ isUpper b)
 
--- | Computes the name of a given constructor
+-- | Computes the name of a given constructor.
 constructorName :: Con -> Q Name
 constructorName (NormalC name _) = return name
 constructorName (RecC name _)    = return name
 constructorName x                = fail $ "Unhandled constructor " ++ show x
 
--- | Builds the constructor-to-string function.
+-- | Builds the generic constructor-to-string function.
 --
 -- This generates a simple function of the following form:
 --
 -- @
--- fname (ConStructorOne {}) = "CON_STRUCTOR_ONE"
--- fname (ConStructorTwo {}) = "CON_STRUCTOR_TWO"
+-- fname (ConStructorOne {}) = trans_fun("ConStructorOne")
+-- fname (ConStructorTwo {}) = trans_fun("ConStructorTwo")
 -- @
 --
 -- This builds a custom list of name/string pairs and then uses
 -- 'genToString' to actually generate the function
-genOpID :: Name -> String -> Q [Dec]
-genOpID name fname = do
+genConstrToStr :: (String -> String) -> Name -> String -> Q [Dec]
+genConstrToStr trans_fun name fname = do
   TyConI (DataD _ _ _ cons _) <- reify name
   cnames <- mapM (liftM nameBase . constructorName) cons
-  let svalues = map (Left . deCamelCase) cnames
+  let svalues = map (Left . trans_fun) cnames
   genToString (mkName fname) name $ zip cnames svalues
 
+-- | Constructor-to-string for OpCode.
+genOpID :: Name -> String -> Q [Dec]
+genOpID = genConstrToStr deCamelCase
 
--- | OpCode parameter (field) type
+-- | OpCode parameter (field) type.
 type OpParam = (String, Q Type, Q Exp)
 
 -- | Generates the OpCode data type.
@@ -265,7 +292,7 @@ genOpCode name cons = do
   (loadsig, loadfn) <- genLoadOpCode cons
   return [declD, loadsig, loadfn, savesig, savefn]
 
--- | Checks whether a given parameter is options
+-- | Checks whether a given parameter is options.
 --
 -- This requires that it's a 'Maybe'.
 isOptional :: Type -> Bool
@@ -282,15 +309,14 @@ saveField :: Name    -- ^ The name of variable that contains the value
           -> Q Exp
 saveField fvar (fname, qt, _) = do
   t <- qt
-  let showJ = varE (mkName "showJSON")
-      fnexp = litE (stringL fname)
+  let fnexp = stringE fname
       fvare = varE fvar
   (if isOptional t
    then [| case $fvare of
-             Just v' -> [( $fnexp, $showJ v')]
+             Just v' -> [( $fnexp, $showJSONE v')]
              Nothing -> []
          |]
-   else [| [( $fnexp, $showJ $fvare )] |])
+   else [| [( $fnexp, $showJSONE $fvare )] |])
 
 -- | Generates the \"save\" clause for an entire opcode constructor.
 --
@@ -307,12 +333,11 @@ saveConstructor sname fields = do
   let pat = conP cname (map varP fnames)
   let felems = map (uncurry saveField) (zip fnames fields)
       -- now build the OP_ID serialisation
-      opid = [| [( $(litE (stringL "OP_ID")),
-                   $(varE (mkName "showJSON"))
-                        $(litE . stringL . deCamelCase $ sname) )] |]
+      opid = [| [( $(stringE "OP_ID"),
+                   $showJSONE $(stringE . deCamelCase $ sname) )] |]
       flist = listE (opid:felems)
       -- and finally convert all this to a json object
-      flist' = [| $(varE (mkName "makeObj")) (concat $flist) |]
+      flist' = [| $(varNameE "makeObj") (concat $flist) |]
   clause [pat] (normalB flist') []
 
 -- | Generates the main save opcode function.
@@ -339,17 +364,17 @@ loadField (fname, qt, qdefa) = do
   t <- qt
   defa <- qdefa
   -- these are used in all patterns below
-  let objvar = varE (mkName "o")
-      objfield = litE (StringL fname)
+  let objvar = varNameE "o"
+      objfield = stringE fname
   bexp <- if isOptional t
-          then [| $((varE (mkName "maybeFromObj"))) $objvar $objfield |]
+          then [| $((varNameE "maybeFromObj")) $objvar $objfield |]
           else case defa of
                  AppE (ConE dt) defval | dt == 'Just ->
                    -- but has a default value
-                   [| $(varE (mkName "fromObjWithDefault"))
+                   [| $(varNameE "fromObjWithDefault")
                       $objvar $objfield $(return defval) |]
                  ConE dt | dt == 'Nothing ->
-                     [| $(varE (mkName "fromObj")) $objvar $objfield |]
+                     [| $(varNameE "fromObj") $objvar $objfield |]
                  s -> fail $ "Invalid default value " ++ show s ++
                       ", expecting either 'Nothing' or a 'Just defval'"
   return (fvar, BindS (VarP fvar) bexp)
@@ -371,8 +396,8 @@ genLoadOpCode opdefs = do
       opid = mkName "op_id"
   st1 <- bindS (varP objname) [| liftM JSON.fromJSObject
                                  (JSON.readJSON $(varE arg1)) |]
-  st2 <- bindS (varP opid) [| $(varE (mkName "fromObj"))
-                              $(varE objname) $(litE (stringL "OP_ID")) |]
+  st2 <- bindS (varP opid) [| $(varNameE "fromObj")
+                              $(varE objname) $(stringE "OP_ID") |]
   -- the match results (per-constructor blocks)
   mexps <- mapM (uncurry loadConstructor) opdefs
   fails <- [| fail $ "Unknown opcode " ++ $(varE opid) |]
@@ -389,3 +414,65 @@ genLoadOpCode opdefs = do
 -- | No default type.
 noDefault :: Q Exp
 noDefault = conE 'Nothing
+
+-- * Template code for luxi
+
+-- | Constructor-to-string for LuxiOp.
+genStrOfOp :: Name -> String -> Q [Dec]
+genStrOfOp = genConstrToStr id
+
+-- | Constructor-to-string for MsgKeys.
+genStrOfKey :: Name -> String -> Q [Dec]
+genStrOfKey = genConstrToStr ensureLower
+
+-- | LuxiOp parameter type.
+type LuxiParam = (String, Q Type, Q Exp)
+
+-- | Generates the LuxiOp data type.
+--
+-- This takes a Luxi operation definition and builds both the
+-- datatype and the function trnasforming the arguments to JSON.
+-- We can't use anything less generic, because the way different
+-- operations are serialized differs on both parameter- and top-level.
+--
+-- There are three things to be defined for each parameter:
+--
+-- * name
+--
+-- * type
+--
+-- * operation; this is the operation performed on the parameter before
+--   serialization
+--
+genLuxiOp :: String -> [(String, [LuxiParam])] -> Q [Dec]
+genLuxiOp name cons = do
+  decl_d <- mapM (\(cname, fields) -> do
+                    fields' <- mapM (\(_, qt, _) ->
+                                         qt >>= \t -> return (NotStrict, t))
+                               fields
+                    return $ NormalC (mkName cname) fields')
+            cons
+  let declD = DataD [] (mkName name) [] decl_d [''Show, ''Read]
+  (savesig, savefn) <- genSaveLuxiOp cons
+  return [declD, savesig, savefn]
+
+-- | Generates the \"save\" clause for entire LuxiOp constructor.
+saveLuxiConstructor :: (String, [LuxiParam]) -> Q Clause
+saveLuxiConstructor (sname, fields) =
+  let cname = mkName sname
+      fnames = map (\(nm, _, _) -> mkName nm) fields
+      pat = conP cname (map varP fnames)
+      flist = map (\(nm, _, fn) -> liftM2 appFn fn $ (varNameE nm)) fields
+      showlist = map (\x -> [| JSON.showJSON $x |]) flist
+      finval = case showlist of
+                 [] -> [| JSON.showJSON () |]
+                 _ -> [| JSON.showJSON $(listE showlist) |]
+  in clause [pat] (normalB finval) []
+
+-- | Generates the main save LuxiOp function.
+genSaveLuxiOp :: [(String, [LuxiParam])] -> Q (Dec, Dec)
+genSaveLuxiOp opdefs = do
+  sigt <- [t| $(conT (mkName "LuxiOp")) -> JSON.JSValue |]
+  let fname = mkName "opToArgs"
+  cclauses <- mapM saveLuxiConstructor opdefs
+  return $ (SigD fname sigt, FunD fname cclauses)