This patch replaces the current hard-coded JSON instances (all alike,
just manual conversion to/from string) with auto-generated code based
on Template Haskell
(http://www.haskell.org/haskellwiki/Template_Haskell).
The reduction in code line is not big, as the helper module is well
documented and thus overall we gain about 70 code lines; however, if
we ignore comments we're in good shape, and any future addition of
such data types will be much simpler and less error-prone.
---
Makefile.am | 3 +-
htools/Ganeti/HTools/Types.hs | 107 +++++------------------
htools/Ganeti/Jobs.hs | 81 +++++-------------
htools/Ganeti/OpCodes.hs | 30 +++----
htools/Ganeti/THH.hs | 187 +++++++++++++++++++++++++++++++++++++++++
5 files changed, 248 insertions(+), 160 deletions(-)
create mode 100644 htools/Ganeti/THH.hs
diff --git a/Makefile.am b/Makefile.am
index 5d4f608..b3edd40 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -361,7 +361,8 @@ HS_LIB_SRCS = \
htools/Ganeti/HTools/Program/Hspace.hs \
htools/Ganeti/Jobs.hs \
htools/Ganeti/Luxi.hs \
- htools/Ganeti/OpCodes.hs
+ htools/Ganeti/OpCodes.hs \
+ htools/Ganeti/THH.hs
HS_BUILT_SRCS = htools/Ganeti/HTools/Version.hs htools/Ganeti/Constants.hs
HS_BUILT_SRCS_IN = $(patsubst %,%.in,$(HS_BUILT_SRCS))
diff --git a/htools/Ganeti/HTools/Types.hs b/htools/Ganeti/HTools/Types.hs
index 75dbce5..8e6b6a4 100644
--- a/htools/Ganeti/HTools/Types.hs
+++ b/htools/Ganeti/HTools/Types.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE TemplateHaskell #-}
+
{-| Some common types.
-}
@@ -72,6 +74,7 @@ import qualified Data.Map as M
import qualified Text.JSON as JSON
import qualified Ganeti.Constants as C
+import qualified Ganeti.THH as THH
-- | The instance index type.
type Idx = Int
@@ -100,36 +103,12 @@ type GroupID = String
-- Ord instance will order them in the order they are defined, so when
-- changing this data type be careful about the interaction with the
-- desired sorting order.
-data AllocPolicy
- = AllocPreferred -- ^ This is the normal status, the group
- -- should be used normally during allocations
- | AllocLastResort -- ^ This group should be used only as
- -- last-resort, after the preferred groups
- | AllocUnallocable -- ^ This group must not be used for new
- -- allocations
- deriving (Show, Read, Eq, Ord, Enum, Bounded)
-
--- | Convert a string to an alloc policy.
-allocPolicyFromString :: (Monad m) => String -> m AllocPolicy
-allocPolicyFromString s =
- case () of
- _ | s == C.allocPolicyPreferred -> return AllocPreferred
- | s == C.allocPolicyLastResort -> return AllocLastResort
- | s == C.allocPolicyUnallocable -> return AllocUnallocable
- | otherwise -> fail $ "Invalid alloc policy mode: " ++ s
-
--- | Convert an alloc policy to the Ganeti string equivalent.
-allocPolicyToString :: AllocPolicy -> String
-allocPolicyToString AllocPreferred = C.allocPolicyPreferred
-allocPolicyToString AllocLastResort = C.allocPolicyLastResort
-allocPolicyToString AllocUnallocable = C.allocPolicyUnallocable
-
-instance JSON.JSON AllocPolicy where
- showJSON = JSON.showJSON . allocPolicyToString
- readJSON s = case JSON.readJSON s of
- JSON.Ok s' -> allocPolicyFromString s'
- JSON.Error e -> JSON.Error $
- "Can't parse alloc_policy: " ++ e
+$(THH.declareSADT "AllocPolicy"
+ [ ("AllocPreferred", 'C.allocPolicyPreferred)
+ , ("AllocLastResort", 'C.allocPolicyLastResort)
+ , ("AllocUnallocable", 'C.allocPolicyUnallocable)
+ ])
+$(THH.makeJSONInstance ''AllocPolicy)
-- | The resource spec type.
data RSpec = RSpec
@@ -182,41 +161,15 @@ data IMove = Failover -- ^ Failover the
instance (f)
deriving (Show, Read)
-- | Instance disk template type.
-data DiskTemplate = DTDiskless
- | DTFile
- | DTSharedFile
- | DTPlain
- | DTBlock
- | DTDrbd8
- deriving (Show, Read, Eq, Enum, Bounded)
-
--- | Converts a DiskTemplate to String.
-diskTemplateToString :: DiskTemplate -> String
-diskTemplateToString DTDiskless = C.dtDiskless
-diskTemplateToString DTFile = C.dtFile
-diskTemplateToString DTSharedFile = C.dtSharedFile
-diskTemplateToString DTPlain = C.dtPlain
-diskTemplateToString DTBlock = C.dtBlock
-diskTemplateToString DTDrbd8 = C.dtDrbd8
-
--- | Converts a DiskTemplate from String.
-diskTemplateFromString :: (Monad m) => String -> m DiskTemplate
-diskTemplateFromString s =
- case () of
- _ | s == C.dtDiskless -> return DTDiskless
- | s == C.dtFile -> return DTFile
- | s == C.dtSharedFile -> return DTSharedFile
- | s == C.dtPlain -> return DTPlain
- | s == C.dtBlock -> return DTBlock
- | s == C.dtDrbd8 -> return DTDrbd8
- | otherwise -> fail $ "Invalid disk template: " ++ s
-
-instance JSON.JSON DiskTemplate where
- showJSON = JSON.showJSON . diskTemplateToString
- readJSON s = case JSON.readJSON s of
- JSON.Ok s' -> diskTemplateFromString s'
- JSON.Error e -> JSON.Error $
- "Can't parse disk_template as string: " ++ e
+$(THH.declareSADT "DiskTemplate"
+ [ ("DTDiskless", 'C.dtDiskless)
+ , ("DTFile", 'C.dtFile)
+ , ("DTSharedFile", 'C.dtSharedFile)
+ , ("DTPlain", 'C.dtPlain)
+ , ("DTBlock", 'C.dtBlock)
+ , ("DTDrbd8", 'C.dtDrbd8)
+ ])
+$(THH.makeJSONInstance ''DiskTemplate)
-- | Formatted solution output for one move (involved nodes and
-- commands.
@@ -347,21 +300,9 @@ class Element a where
setIdx :: a -> Int -> a
-- | The iallocator node-evacuate evac_mode type.
-data EvacMode = ChangePrimary
- | ChangeSecondary
- | ChangeAll
- deriving (Show, Read)
-
-instance JSON.JSON EvacMode where
- showJSON mode = case mode of
- ChangeAll -> JSON.showJSON C.iallocatorNevacAll
- ChangePrimary -> JSON.showJSON C.iallocatorNevacPri
- ChangeSecondary -> JSON.showJSON C.iallocatorNevacSec
- readJSON v =
- case JSON.readJSON v of
- JSON.Ok s | s == C.iallocatorNevacAll -> return ChangeAll
- | s == C.iallocatorNevacPri -> return ChangePrimary
- | s == C.iallocatorNevacSec -> return ChangeSecondary
- | otherwise -> fail $ "Invalid evacuate mode " ++ s
- JSON.Error e -> JSON.Error $
- "Can't parse evacuate mode as string: " ++ e
+$(THH.declareSADT "EvacMode"
+ [ ("ChangePrimary", 'C.iallocatorNevacPri)
+ , ("ChangeSecondary", 'C.iallocatorNevacSec)
+ , ("ChangeAll", 'C.iallocatorNevacAll)
+ ])
+$(THH.makeJSONInstance ''EvacMode)
diff --git a/htools/Ganeti/Jobs.hs b/htools/Ganeti/Jobs.hs
index ed7bc7d..1e2cebe 100644
--- a/htools/Ganeti/Jobs.hs
+++ b/htools/Ganeti/Jobs.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE TemplateHaskell #-}
+
{-| Implementation of the job information.
-}
@@ -32,67 +34,30 @@ import Text.JSON (readJSON, showJSON, JSON)
import qualified Text.JSON as J
import qualified Ganeti.Constants as C
+import qualified Ganeti.THH as THH
-- | Our ADT for the OpCode status at runtime (while in a job).
-data OpStatus = OP_STATUS_QUEUED
- | OP_STATUS_WAITING
- | OP_STATUS_CANCELING
- | OP_STATUS_RUNNING
- | OP_STATUS_CANCELED
- | OP_STATUS_SUCCESS
- | OP_STATUS_ERROR
- deriving (Eq, Enum, Bounded, Show, Read)
-
-instance JSON OpStatus where
- showJSON os = showJSON w
- where w = case os of
- OP_STATUS_QUEUED -> C.opStatusQueued
- OP_STATUS_WAITING -> C.opStatusWaiting
- OP_STATUS_CANCELING -> C.opStatusCanceling
- OP_STATUS_RUNNING -> C.opStatusRunning
- OP_STATUS_CANCELED -> C.opStatusCanceled
- OP_STATUS_SUCCESS -> C.opStatusSuccess
- OP_STATUS_ERROR -> C.opStatusError
- readJSON s = case readJSON s of
- J.Ok v | v == C.opStatusQueued -> J.Ok OP_STATUS_QUEUED
- | v == C.opStatusWaiting -> J.Ok OP_STATUS_WAITING
- | v == C.opStatusCanceling -> J.Ok OP_STATUS_CANCELING
- | v == C.opStatusRunning -> J.Ok OP_STATUS_RUNNING
- | v == C.opStatusCanceled -> J.Ok OP_STATUS_CANCELED
- | v == C.opStatusSuccess -> J.Ok OP_STATUS_SUCCESS
- | v == C.opStatusError -> J.Ok OP_STATUS_ERROR
- | otherwise -> J.Error ("Unknown opcode status " ++ v)
- _ -> J.Error ("Cannot parse opcode status " ++ show s)
+$(THH.declareSADT "OpStatus"
+ [ ("OP_STATUS_QUEUED", 'C.opStatusQueued)
+ , ("OP_STATUS_WAITING", 'C.opStatusWaiting)
+ , ("OP_STATUS_CANCELING", 'C.opStatusCanceling)
+ , ("OP_STATUS_RUNNING", 'C.opStatusRunning)
+ , ("OP_STATUS_CANCELED", 'C.opStatusCanceled)
+ , ("OP_STATUS_SUCCESS", 'C.opStatusSuccess)
+ , ("OP_STATUS_ERROR", 'C.opStatusError)
+ ])
+$(THH.makeJSONInstance ''OpStatus)
-- | The JobStatus data type. Note that this is ordered especially
-- such that greater\/lesser comparison on values of this type makes
-- sense.
-data JobStatus = JOB_STATUS_QUEUED
- | JOB_STATUS_WAITING
- | JOB_STATUS_RUNNING
- | JOB_STATUS_SUCCESS
- | JOB_STATUS_CANCELING
- | JOB_STATUS_CANCELED
- | JOB_STATUS_ERROR
- deriving (Eq, Enum, Ord, Bounded, Show, Read)
-
-instance JSON JobStatus where
- showJSON js = showJSON w
- where w = case js of
- JOB_STATUS_QUEUED -> C.jobStatusQueued
- JOB_STATUS_WAITING -> C.jobStatusWaiting
- JOB_STATUS_CANCELING -> C.jobStatusCanceling
- JOB_STATUS_RUNNING -> C.jobStatusRunning
- JOB_STATUS_CANCELED -> C.jobStatusCanceled
- JOB_STATUS_SUCCESS -> C.jobStatusSuccess
- JOB_STATUS_ERROR -> C.jobStatusError
- readJSON s = case readJSON s of
- J.Ok v | v == C.jobStatusQueued -> J.Ok JOB_STATUS_QUEUED
- | v == C.jobStatusWaiting -> J.Ok JOB_STATUS_WAITING
- | v == C.jobStatusCanceling -> J.Ok JOB_STATUS_CANCELING
- | v == C.jobStatusRunning -> J.Ok JOB_STATUS_RUNNING
- | v == C.jobStatusSuccess -> J.Ok JOB_STATUS_SUCCESS
- | v == C.jobStatusCanceled -> J.Ok JOB_STATUS_CANCELED
- | v == C.jobStatusError -> J.Ok JOB_STATUS_ERROR
- | otherwise -> J.Error ("Unknown job status " ++ v)
- _ -> J.Error ("Unknown job status " ++ show s)
+$(THH.declareSADT "JobStatus"
+ [ ("JOB_STATUS_QUEUED", 'C.jobStatusQueued)
+ , ("JOB_STATUS_WAITING", 'C.jobStatusWaiting)
+ , ("JOB_STATUS_CANCELING", 'C.jobStatusCanceling)
+ , ("JOB_STATUS_RUNNING", 'C.jobStatusRunning)
+ , ("JOB_STATUS_CANCELED", 'C.jobStatusCanceled)
+ , ("JOB_STATUS_SUCCESS", 'C.jobStatusSuccess)
+ , ("JOB_STATUS_ERROR", 'C.jobStatusError)
+ ])
+$(THH.makeJSONInstance ''JobStatus)
diff --git a/htools/Ganeti/OpCodes.hs b/htools/Ganeti/OpCodes.hs
index 0930598..b3fc1f3 100644
--- a/htools/Ganeti/OpCodes.hs
+++ b/htools/Ganeti/OpCodes.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE TemplateHaskell #-}
+
{-| Implementation of the opcodes.
-}
@@ -34,27 +36,19 @@ import Text.JSON (readJSON, showJSON, makeObj, JSON)
import qualified Text.JSON as J
import Text.JSON.Types
+import qualified Ganeti.Constants as C
+import qualified Ganeti.THH as THH
+
import Ganeti.HTools.Utils
-- | Replace disks type.
-data ReplaceDisksMode = ReplaceOnPrimary
- | ReplaceOnSecondary
- | ReplaceNewSecondary
- | ReplaceAuto
- deriving (Show, Read, Eq)
-
-instance JSON ReplaceDisksMode where
- showJSON m = case m of
- ReplaceOnPrimary -> showJSON "replace_on_primary"
- ReplaceOnSecondary -> showJSON "replace_on_secondary"
- ReplaceNewSecondary -> showJSON "replace_new_secondary"
- ReplaceAuto -> showJSON "replace_auto"
- readJSON s = case readJSON s of
- J.Ok "replace_on_primary" -> J.Ok ReplaceOnPrimary
- J.Ok "replace_on_secondary" -> J.Ok ReplaceOnSecondary
- J.Ok "replace_new_secondary" -> J.Ok ReplaceNewSecondary
- J.Ok "replace_auto" -> J.Ok ReplaceAuto
- _ -> J.Error "Can't parse a valid ReplaceDisksMode"
+$(THH.declareSADT "ReplaceDisksMode"
+ [ ("ReplaceOnPrimary", 'C.replaceDiskPri)
+ , ("ReplaceOnSecondary", 'C.replaceDiskSec)
+ , ("ReplaceNewSecondary", 'C.replaceDiskChg)
+ , ("ReplaceAuto", 'C.replaceDiskAuto)
+ ])
+$(THH.makeJSONInstance ''ReplaceDisksMode)
-- | OpCode representation.
--
diff --git a/htools/Ganeti/THH.hs b/htools/Ganeti/THH.hs
new file mode 100644
index 0000000..fe5e4ae
--- /dev/null
+++ b/htools/Ganeti/THH.hs
@@ -0,0 +1,187 @@
+{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
+
+{-| TemplateHaskell helper for HTools.
+
+As TemplateHaskell require that splices be defined in a separate
+module, we combine all the TemplateHaskell functionality that HTools
+needs in this module (except the one for unittests).
+
+-}
+
+{-
+
+Copyright (C) 2011 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
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.THH ( declareSADT
+ , makeJSONInstance
+ ) where
+
+import Data.Char
+import Language.Haskell.TH
+
+import qualified Text.JSON as JSON
+
+-- | Ensure first letter is lowercase.
+--
+-- Used to convert type name to function prefix, e.g. in @data Aa ->
+-- aaToString@.
+ensureLower :: String -> String
+ensureLower [] = []
+ensureLower (x:xs) = toLower x:xs
+
+-- | ToString function name.
+toStrName :: String -> Name
+toStrName = mkName . (++ "ToString") . ensureLower
+
+-- | FromString function name.
+fromStrName :: String -> Name
+fromStrName = mkName . (++ "FromString") . ensureLower
+
+-- | Generates a data type declaration.
+--
+-- The type will have a fixed list of instances
+strADTDecl :: String -> [String] -> Dec
+strADTDecl name constructors =
+ DataD [] (mkName name) []
+ (map (flip NormalC [] . mkName) constructors)
+ [''Show, ''Read, ''Eq, ''Enum, ''Bounded, ''Ord]
+
+-- | Generates a toString function.
+--
+-- This generates a simple function of the form:
+--
+-- @
+-- nameToString :: Name -> String
+-- nameToString Cons1 = var1
+-- nameToString Cons2 = \"value2\"
+-- @
+genToString :: Name -> Name -> [(String, Name)] -> Q [Dec]
+genToString fname tname constructors = do
+ sigt <- [t| $(conT tname) -> String |]
+ -- the body clauses, matching on the constructor and returning the
+ -- string value
+ clauses <- mapM (\(c, v) -> clause [recP (mkName c) []]
+ (normalB (varE v)) []) constructors
+ return [SigD fname sigt, FunD fname clauses]
+
+-- | Generates a fromString function.
+--
+-- The function generated is monadic and can fail parsing the
+-- string. It is of the form:
+--
+-- @
+-- nameFromString :: (Monad m) => String -> m Name
+-- nameFromString s | s == var1 = Cons1
+-- | s == \"value2\" = Cons2
+-- | otherwise = fail /.../
+-- @
+genFromString :: Name -> Name -> [(String, Name)] -> Q [Dec]
+genFromString fname tname constructors = do
+ -- signature of form (Monad m) => String -> m $name
+ sigt <- [t| (Monad m) => String -> m $(conT tname) |]
+ -- clauses for a guarded pattern
+ let varp = mkName "s"
+ varpe = varE varp
+ clauses <- mapM (\(c, v) -> do
+ -- the clause match condition
+ g <- normalG [| $varpe == $(varE v) |]
+ -- the clause result
+ r <- [| return $(conE (mkName c)) |]
+ return (g, r)) constructors
+ -- the otherwise clause (fallback)
+ oth_clause <- do
+ g <- normalG [| otherwise |]
+ r <- [|fail ("Invalid string value for type " ++
+ $(litE (stringL (nameBase tname))) ++ ": " ++ $varpe) |]
+ return (g, r)
+ let fun = FunD fname [Clause [VarP varp]
+ (GuardedB (clauses++[oth_clause])) []]
+ return [SigD fname sigt, fun]
+
+-- | Generates a data type from a given string format.
+--
+-- The format is expected to multiline. The first line contains the
+-- type name, and the rest of the lines must contain two words: the
+-- constructor name and then the string representation of the
+-- respective constructor.
+--
+-- The function will generate the data type declaration, and then two
+-- functions:
+--
+-- * /name/ToString, which converts the type to a string
+--
+-- * /name/FromString, which (monadically) converts from a string to the type
+--
+-- Note that this is basically just a custom show/read instance,
+-- nothing else.
+declareSADT :: String -> [(String, Name)] -> Q [Dec]
+declareSADT sname cons = do
+ let name = mkName sname
+ let ddecl = strADTDecl sname (map fst cons)
+ tostr <- genToString (toStrName sname) name cons
+ fromstr <- genFromString (fromStrName sname) name cons
+ return $ ddecl:tostr ++ fromstr
+
+
+-- | Creates the showJSON member of a JSON instance declaration.
+--
+-- This will create what is the equivalent of:
+--
+-- @
+-- showJSON = showJSON . /name/ToString
+-- @
+--
+-- in an instance JSON /name/ declaration
+genShowJSON :: String -> Q [Dec]
+genShowJSON name = [d| showJSON = JSON.showJSON . $(varE (toStrName name)) |]
+
+-- | Creates the readJSON member of a JSON instance declaration.
+--
+-- This will create what is the equivalent of:
+--
+-- @
+-- readJSON s = case readJSON s of
+-- Ok s' -> /name/FromString s'
+-- Error e -> Error /description/
+-- @
+--
+-- in an instance JSON /name/ declaration
+genReadJSON :: String -> Q Dec
+genReadJSON name = do
+ let s = mkName "s"
+ body <- [| case JSON.readJSON $(varE s) of
+ JSON.Ok s' -> $(varE (fromStrName name)) s'
+ JSON.Error e ->
+ JSON.Error $ "Can't parse string value for type " ++
+ $(litE (StringL name)) ++ ": " ++ e
+ |]
+ return $ FunD (mkName "readJSON") [Clause [VarP s] (NormalB body) []]
+
+-- | Generates a JSON instance for a given type.
+--
+-- This assumes that the /name/ToString and /name/FromString functions
+-- have been defined as by the 'declareSADT' function.
+makeJSONInstance :: Name -> Q [Dec]
+makeJSONInstance name = do
+ let base = nameBase name
+ showJ <- genShowJSON base
+ readJ <- genReadJSON base
+
+ return [InstanceD [] (AppT (ConT ''JSON.JSON) (ConT name)) (readJ:showJ)]
--
1.7.3.1