On Mon, Sep 26, 2011 at 11:57 AM, Iustin Pop <[email protected]> wrote:
> 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]
> +
I take that the first argument is not a Name for consistency with the
second argument?

> +-- | 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
The second 'let' seems redundant, I think? (just let, not ddecl ;) )

> +
> +
> +-- | 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)]
Hmm no newline before return, I think

> --
> 1.7.3.1
>
>

Rest LGTM

Reply via email to