Datatypes for haskell RPC calls
[ganeti-local] / htools / Ganeti / THH.hs
index 6457115..929da6f 100644 (file)
@@ -1,4 +1,4 @@
-{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
 
 {-| TemplateHaskell helper for HTools.
 
@@ -10,7 +10,7 @@ needs in this module (except the one for unittests).
 
 {-
 
-Copyright (C) 2011 Google Inc.
+Copyright (C) 2011, 2012 Google Inc.
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
@@ -34,7 +34,6 @@ module Ganeti.THH ( declareSADT
                   , makeJSONInstance
                   , genOpID
                   , genOpCode
-                  , noDefault
                   , genStrOfOp
                   , genStrOfKey
                   , genLuxiOp
@@ -63,6 +62,8 @@ import Language.Haskell.TH
 
 import qualified Text.JSON as JSON
 
+import Ganeti.HTools.JSON
+
 -- * Exported types
 
 type Container = M.Map String
@@ -109,16 +110,26 @@ containerField :: Field -> Field
 containerField field = field { fieldIsContainer = True }
 
 -- | Sets custom functions on a field.
-customField :: Q Exp -> Q Exp -> Field -> Field
+customField :: Name    -- ^ The name of the read function
+            -> Name    -- ^ The name of the show function
+            -> Field   -- ^ The original field
+            -> Field   -- ^ Updated field
 customField readfn showfn field =
-  field { fieldRead = Just readfn, fieldShow = Just showfn }
+  field { fieldRead = Just (varE readfn), fieldShow = Just (varE showfn) }
 
 fieldRecordName :: Field -> String
 fieldRecordName (Field { fieldName = name, fieldConstr = alias }) =
   maybe (camelCase name) id alias
 
+-- | Computes the preferred variable name to use for the value of this
+-- field. If the field has a specific constructor name, then we use a
+-- first-letter-lowercased version of that; otherwise, we simply use
+-- the field name. See also 'fieldRecordName'.
 fieldVariable :: Field -> String
-fieldVariable = map toLower . fieldRecordName
+fieldVariable f =
+  case (fieldConstr f) of
+    Just name -> ensureLower name
+    _ -> fieldName f
 
 actualFieldType :: Field -> Q Type
 actualFieldType f | fieldIsContainer f = [t| Container $t |]
@@ -133,15 +144,18 @@ checkNonOptDef (Field { fieldDefault = (Just _), fieldName = name }) =
   fail $ "Default field " ++ name ++ " used in parameter declaration"
 checkNonOptDef _ = return ()
 
-loadFn :: Field -> Q Exp -> Q Exp
-loadFn (Field { fieldIsContainer = True }) expr = [| $expr >>= readContainer |]
-loadFn (Field { fieldRead = Just readfn }) expr = [| $expr >>= $readfn |]
-loadFn _ expr = expr
-
-saveFn :: Field -> Q Exp -> Q Exp
-saveFn (Field { fieldIsContainer = True }) expr = [| showContainer $expr |]
-saveFn (Field { fieldRead = Just readfn }) expr = [| $readfn $expr |]
-saveFn _ expr = expr
+-- | Produces the expression that will de-serialise a given
+-- field. Since some custom parsing functions might need to use the
+-- entire object, we do take and pass the object to any custom read
+-- functions.
+loadFn :: Field   -- ^ The field definition
+       -> Q Exp   -- ^ The value of the field as existing in the JSON message
+       -> Q Exp   -- ^ The entire object in JSON object format
+       -> Q Exp   -- ^ Resulting expression
+loadFn (Field { fieldIsContainer = True }) expr _ =
+  [| $expr >>= readContainer |]
+loadFn (Field { fieldRead = Just readfn }) expr o = [| $expr >>= $readfn $o |]
+loadFn _ expr _ = expr
 
 -- * Common field declarations
 
@@ -205,8 +219,12 @@ appFn f x | f == VarE 'id = x
           | otherwise = AppE f x
 
 -- | Container loader
-readContainer :: (Monad m) => JSON.JSObject a -> m (Container a)
-readContainer = return . M.fromList . JSON.fromJSObject
+readContainer :: (Monad m, JSON.JSON a) =>
+                 JSON.JSObject JSON.JSValue -> m (Container a)
+readContainer obj = do
+  let kjvlist = JSON.fromJSObject obj
+  kalist <- mapM (\(k, v) -> fromKeyValue k v >>= \a -> return (k, a)) kjvlist
+  return $ M.fromList kalist
 
 -- | Container dumper
 showContainer :: (JSON.JSON a) => Container a -> JSON.JSValue
@@ -234,7 +252,7 @@ strADTDecl name constructors =
 -- @
 genToRaw :: Name -> Name -> Name -> [(String, Either String Name)] -> Q [Dec]
 genToRaw traw fname tname constructors = do
-  sigt <- [t| $(conT tname) -> $(conT traw) |]
+  let sigt = AppT (AppT ArrowT (ConT tname)) (ConT traw)
   -- the body clauses, matching on the constructor and returning the
   -- raw value
   clauses <- mapM  (\(c, v) -> clause [recP (mkName c) []]
@@ -316,8 +334,10 @@ declareSADT = declareADT ''String
 -- @
 --
 -- in an instance JSON /name/ declaration
-genShowJSON :: String -> Q [Dec]
-genShowJSON name = [d| showJSON = JSON.showJSON . $(varE (toRawName name)) |]
+genShowJSON :: String -> Q Dec
+genShowJSON name = do
+  body <- [| JSON.showJSON . $(varE (toRawName name)) |]
+  return $ FunD (mkName "showJSON") [Clause [] (NormalB body) []]
 
 -- | Creates the readJSON member of a JSON instance declaration.
 --
@@ -337,7 +357,8 @@ genReadJSON name = do
                JSON.Ok s' -> $(varE (fromRawName name)) s'
                JSON.Error e ->
                    JSON.Error $ "Can't parse raw value for type " ++
-                           $(stringE name) ++ ": " ++ e
+                           $(stringE name) ++ ": " ++ e ++ " from " ++
+                           show $(varE s)
            |]
   return $ FunD (mkName "readJSON") [Clause [VarP s] (NormalB body) []]
 
@@ -350,7 +371,7 @@ makeJSONInstance name = do
   let base = nameBase name
   showJ <- genShowJSON base
   readJ <- genReadJSON base
-  return [InstanceD [] (AppT (ConT ''JSON.JSON) (ConT name)) (readJ:showJ)]
+  return [InstanceD [] (AppT (ConT ''JSON.JSON) (ConT name)) [readJ,showJ]]
 
 -- * Template code for opcodes
 
@@ -401,27 +422,15 @@ type OpParam = (String, Q Type, Q Exp)
 -- datatype and the JSON serialisation out of it. We can't use a
 -- generic serialisation since we need to be compatible with Ganeti's
 -- own, so we have a few quirks to work around.
---
--- There are three things to be defined for each parameter:
---
--- * name
---
--- * type; if this is 'Maybe', will only be serialised if it's a
---   'Just' value
---
--- * default; if missing, won't raise an exception, but will instead
---   use the default
---
 genOpCode :: String                -- ^ Type name to use
-          -> [(String, [OpParam])] -- ^ Constructor name and parameters
+          -> [(String, [Field])]   -- ^ Constructor name and parameters
           -> Q [Dec]
 genOpCode name cons = do
   decl_d <- mapM (\(cname, fields) -> do
                     -- we only need the type of the field, without Q
-                    fields' <- mapM (\(_, qt, _) ->
-                                         qt >>= \t -> return (NotStrict, t))
-                               fields
-                    return $ NormalC (mkName cname) fields')
+                    fields' <- mapM actualFieldType fields
+                    let fields'' = zip (repeat NotStrict) fields'
+                    return $ NormalC (mkName cname) fields'')
             cons
   let declD = DataD [] (mkName name) [] decl_d [''Show, ''Read, ''Eq]
 
@@ -436,42 +445,23 @@ isOptional :: Type -> Bool
 isOptional (AppT (ConT dt) _) | dt == ''Maybe = True
 isOptional _ = False
 
--- | Generates the \"save\" expression for a single opcode parameter.
---
--- There is only one special handling mode: if the parameter is of
--- 'Maybe' type, then we only save it if it's a 'Just' value,
--- otherwise we skip it.
-saveField :: Name    -- ^ The name of variable that contains the value
-          -> OpParam -- ^ Parameter definition
-          -> Q Exp
-saveField fvar (fname, qt, _) = do
-  t <- qt
-  let fnexp = stringE fname
-      fvare = varE fvar
-  (if isOptional t
-   then [| case $fvare of
-             Just v' -> [( $fnexp, $showJSONE v')]
-             Nothing -> []
-         |]
-   else [| [( $fnexp, $showJSONE $fvare )] |])
-
 -- | Generates the \"save\" clause for an entire opcode constructor.
 --
 -- This matches the opcode with variables named the same as the
 -- constructor fields (just so that the spliced in code looks nicer),
--- and passes those name plus the parameter definition to 'saveField'.
+-- and passes those name plus the parameter definition to 'saveObjectField'.
 saveConstructor :: String    -- ^ The constructor name
-                -> [OpParam] -- ^ The parameter definitions for this
+                -> [Field]   -- ^ The parameter definitions for this
                              -- constructor
                 -> Q Clause  -- ^ Resulting clause
 saveConstructor sname fields = do
   let cname = mkName sname
-  let fnames = map (\(n, _, _) -> mkName n) fields
+  let fnames = map (mkName . fieldVariable) fields
   let pat = conP cname (map varP fnames)
-  let felems = map (uncurry saveField) (zip fnames fields)
+  let felems = map (uncurry saveObjectField) (zip fnames fields)
       -- now build the OP_ID serialisation
       opid = [| [( $(stringE "OP_ID"),
-                   $showJSONE $(stringE . deCamelCase $ sname) )] |]
+                   JSON.showJSON $(stringE . deCamelCase $ sname) )] |]
       flist = listE (opid:felems)
       -- and finally convert all this to a json object
       flist' = [| $(varNameE "makeObj") (concat $flist) |]
@@ -481,51 +471,23 @@ saveConstructor sname fields = do
 --
 -- This builds a per-constructor match clause that contains the
 -- respective constructor-serialisation code.
-genSaveOpCode :: [(String, [OpParam])] -> Q (Dec, Dec)
+genSaveOpCode :: [(String, [Field])] -> Q (Dec, Dec)
 genSaveOpCode opdefs = do
   cclauses <- mapM (uncurry saveConstructor) opdefs
   let fname = mkName "saveOpCode"
   sigt <- [t| $(conT (mkName "OpCode")) -> JSON.JSValue |]
   return $ (SigD fname sigt, FunD fname cclauses)
 
--- | Generates the \"load\" field for a single parameter.
---
--- There is custom handling, depending on how the parameter is
--- specified. For a 'Maybe' type parameter, we allow that it is not
--- present (via 'Utils.maybeFromObj'). Otherwise, if there is a
--- default value, we allow the parameter to be abset, and finally if
--- there is no default value, we require its presence.
-loadField :: OpParam -> Q (Name, Stmt)
-loadField (fname, qt, qdefa) = do
-  let fvar = mkName fname
-  t <- qt
-  defa <- qdefa
-  -- these are used in all patterns below
-  let objvar = varNameE "o"
-      objfield = stringE fname
-  bexp <- if isOptional t
-          then [| $((varNameE "maybeFromObj")) $objvar $objfield |]
-          else case defa of
-                 AppE (ConE dt) defval | dt == 'Just ->
-                   -- but has a default value
-                   [| $(varNameE "fromObjWithDefault")
-                      $objvar $objfield $(return defval) |]
-                 ConE dt | dt == 'Nothing ->
-                     [| $(varNameE "fromObj") $objvar $objfield |]
-                 s -> fail $ "Invalid default value " ++ show s ++
-                      ", expecting either 'Nothing' or a 'Just defval'"
-  return (fvar, BindS (VarP fvar) bexp)
-
-loadConstructor :: String -> [OpParam] -> Q Exp
+loadConstructor :: String -> [Field] -> Q Exp
 loadConstructor sname fields = do
   let name = mkName sname
-  fbinds <- mapM loadField fields
+  fbinds <- mapM loadObjectField fields
   let (fnames, fstmts) = unzip fbinds
   let cval = foldl (\accu fn -> AppE accu (VarE fn)) (ConE name) fnames
       fstmts' = fstmts ++ [NoBindS (AppE (VarE 'return) cval)]
   return $ DoE fstmts'
 
-genLoadOpCode :: [(String, [OpParam])] -> Q (Dec, Dec)
+genLoadOpCode :: [(String, [Field])] -> Q (Dec, Dec)
 genLoadOpCode opdefs = do
   let fname = mkName "loadOpCode"
       arg1 = mkName "v"
@@ -548,10 +510,6 @@ genLoadOpCode opdefs = do
   sigt <- [t| JSON.JSValue -> JSON.Result $(conT (mkName "OpCode")) |]
   return $ (SigD fname sigt, FunD fname [Clause [VarP arg1] (NormalB body) []])
 
--- | No default type.
-noDefault :: Q Exp
-noDefault = conE 'Nothing
-
 -- * Template code for luxi
 
 -- | Constructor-to-string for LuxiOp.
@@ -589,9 +547,12 @@ genLuxiOp name cons = do
                                fields
                     return $ NormalC (mkName cname) fields')
             cons
-  let declD = DataD [] (mkName name) [] decl_d [''Show, ''Read]
+  let declD = DataD [] (mkName name) [] decl_d [''Show, ''Read, ''Eq]
   (savesig, savefn) <- genSaveLuxiOp cons
-  return [declD, savesig, savefn]
+  req_defs <- declareSADT "LuxiReq" .
+              map (\(str, _) -> ("Req" ++ str, mkName ("luxiReq" ++ str))) $
+                  cons
+  return $ [declD, savesig, savefn] ++ req_defs
 
 -- | Generates the \"save\" expression for a single luxi parameter.
 saveLuxiField :: Name -> LuxiParam -> Q Exp
@@ -633,7 +594,7 @@ buildObject sname field_pfx fields = do
   let name = mkName sname
   fields_d <- mapM (fieldTypeInfo field_pfx) fields
   let decl_d = RecC name fields_d
-  let declD = DataD [] name [] [decl_d] [''Show, ''Read]
+  let declD = DataD [] name [] [decl_d] [''Show, ''Read, ''Eq]
   ser_decls <- buildObjectSerialisation sname fields
   return $ declD:ser_decls
 
@@ -645,7 +606,7 @@ buildObjectSerialisation sname fields = do
   shjson <- objectShowJSON sname
   rdjson <- objectReadJSON sname
   let instdecl = InstanceD [] (AppT (ConT ''JSON.JSON) (ConT name))
-                 (rdjson:shjson)
+                 [rdjson, shjson]
   return $ savedecls ++ [loadsig, loadfn, instdecl]
 
 genSaveObject :: (Name -> Field -> Q Exp)
@@ -671,22 +632,25 @@ genSaveObject save_fn sname fields = do
 
 saveObjectField :: Name -> Field -> Q Exp
 saveObjectField fvar field
-  | isContainer = [| [( $nameE , $showJSONE . showContainer $ $fvarE)] |]
+  | isContainer = [| [( $nameE , JSON.showJSON . showContainer $ $fvarE)] |]
   | fisOptional = [| case $(varE fvar) of
                       Nothing -> []
-                      Just v -> [( $nameE, $showJSONE v)]
+                      Just v -> [( $nameE, JSON.showJSON v)]
                   |]
   | otherwise = case fieldShow field of
-      Nothing -> [| [( $nameE, $showJSONE $fvarE)] |]
-      Just fn -> [| [( $nameE, $showJSONE . $fn $ $fvarE)] |]
+      Nothing -> [| [( $nameE, JSON.showJSON $fvarE)] |]
+      Just fn -> [| let (actual, extra) = $fn $fvarE
+                    in extra ++ [( $nameE, JSON.showJSON actual)]
+                  |]
   where isContainer = fieldIsContainer field
         fisOptional  = fieldIsOptional field
         nameE = stringE (fieldName field)
         fvarE = varE fvar
 
-objectShowJSON :: String -> Q [Dec]
-objectShowJSON name =
-  [d| showJSON = JSON.showJSON . $(varE . mkName $ "save" ++ name) |]
+objectShowJSON :: String -> Q Dec
+objectShowJSON name = do
+  body <- [| JSON.showJSON . $(varE . mkName $ "save" ++ name) |]
+  return $ FunD (mkName "showJSON") [Clause [] (NormalB body) []]
 
 genLoadObject :: (Field -> Q (Name, Stmt))
               -> String -> [Field] -> Q (Dec, Dec)
@@ -721,7 +685,7 @@ loadObjectField field = do
                    [| $(varNameE "fromObjWithDefault") $objvar
                       $objfield $defv |]
                  Nothing -> [| $(varNameE "fromObj") $objvar $objfield |]
-  bexp <- loadFn field loadexp
+  bexp <- loadFn field loadexp objvar
 
   return (fvar, BindS (VarP fvar) bexp)
 
@@ -767,8 +731,8 @@ buildParam sname field_pfx fields = do
   fields_p <- mapM (paramFieldTypeInfo field_pfx) fields
   let decl_f = RecC name_f fields_f
       decl_p = RecC name_p fields_p
-  let declF = DataD [] name_f [] [decl_f] [''Show, ''Read]
-      declP = DataD [] name_p [] [decl_p] [''Show, ''Read]
+  let declF = DataD [] name_f [] [decl_f] [''Show, ''Read, ''Eq]
+      declP = DataD [] name_p [] [decl_p] [''Show, ''Read, ''Eq]
   ser_decls_f <- buildObjectSerialisation sname_f fields
   ser_decls_p <- buildPParamSerialisation sname_p fields
   fill_decls <- fillParam sname field_pfx fields
@@ -782,7 +746,7 @@ buildPParamSerialisation sname fields = do
   shjson <- objectShowJSON sname
   rdjson <- objectReadJSON sname
   let instdecl = InstanceD [] (AppT (ConT ''JSON.JSON) (ConT name))
-                 (rdjson:shjson)
+                 [rdjson, shjson]
   return $ savedecls ++ [loadsig, loadfn, instdecl]
 
 savePParamField :: Name -> Field -> Q Exp
@@ -806,7 +770,7 @@ loadPParamField field = do
   let objvar = varNameE "o"
       objfield = stringE name
       loadexp = [| $(varNameE "maybeFromObj") $objvar $objfield |]
-  bexp <- loadFn field loadexp
+  bexp <- loadFn field loadexp objvar
   return (fvar, BindS (VarP fvar) bexp)
 
 -- | Builds a simple declaration of type @n_x = fromMaybe f_x p_x@.