From c48711d5c17aa1cc7ab0a5974245068c981d6aa2 Mon Sep 17 00:00:00 2001
From: Iustin Pop <iustin@google.com>
Date: Tue, 27 Nov 2012 16:45:39 +0100
Subject: [PATCH] Add a 'real' type for JobIds

Currently, the job ID is a simple type alias. This is suboptimal, as
it means we can't use a custom JSON (or Arbitrary) instance for it.

The patch changes it into a newtype, and then a) simplifies some
deserialisation code and b) changes some more fields to this new type
(rather than plain 'Int').

We also move the JobId to types, since it will be needed in opcodes as
well.

Signed-off-by: Iustin Pop <iustin@google.com>
Reviewed-by: Guido Trotter <ultrotter@google.com>
---
 htest/Test/Ganeti/Types.hs | 15 +++++++++++
 htools/Ganeti/Luxi.hs      | 55 +++++++++++++-------------------------
 htools/Ganeti/Types.hs     | 29 ++++++++++++++++++++
 3 files changed, 63 insertions(+), 36 deletions(-)

diff --git a/htest/Test/Ganeti/Types.hs b/htest/Test/Ganeti/Types.hs
index 790f94819..79abcef63 100644
--- a/htest/Test/Ganeti/Types.hs
+++ b/htest/Test/Ganeti/Types.hs
@@ -33,11 +33,13 @@ module Test.Ganeti.Types
   , InstanceStatus(..)
   , NonEmpty(..)
   , Hypervisor(..)
+  , JobId(..)
   ) where
 
 import Data.List (sort)
 import Test.QuickCheck as QuickCheck hiding (Result)
 import Test.HUnit
+import qualified Text.JSON as J
 
 import Test.Ganeti.TestHelper
 import Test.Ganeti.TestCommon
@@ -45,6 +47,7 @@ import Test.Ganeti.TestCommon
 import Ganeti.BasicTypes
 import qualified Ganeti.Constants as C
 import Ganeti.Types as Types
+import Ganeti.Luxi as Luxi
 
 {-# ANN module "HLint: ignore Use camelCase" #-}
 
@@ -109,6 +112,11 @@ $(genArbitrary ''NICMode)
 
 $(genArbitrary ''FinalizedJobStatus)
 
+instance Arbitrary Luxi.JobId where
+  arbitrary = do
+    (Positive i) <- arbitrary
+    Luxi.makeJobId i
+
 -- * Properties
 
 prop_AllocPolicy_serialisation :: AllocPolicy -> Property
@@ -271,6 +279,12 @@ case_FinalizedJobStatus_pyequiv = do
                             [minBound..maxBound]
   assertEqual "for FinalizedJobStatus equivalence" all_py_codes all_hs_codes
 
+-- | Tests JobId serialisation (both from string and ints).
+prop_JobId_serialisation :: JobId -> Property
+prop_JobId_serialisation jid =
+  testSerialisation jid .&&.
+  (J.readJSON . J.showJSON . show $ fromJobId jid) ==? J.Ok jid
+
 testSuite "Types"
   [ 'prop_AllocPolicy_serialisation
   , 'prop_DiskTemplate_serialisation
@@ -304,4 +318,5 @@ testSuite "Types"
   , 'case_NICMode_pyequiv
   , 'prop_FinalizedJobStatus_serialisation
   , 'case_FinalizedJobStatus_pyequiv
+  , 'prop_JobId_serialisation
   ]
diff --git a/htools/Ganeti/Luxi.hs b/htools/Ganeti/Luxi.hs
index 073f0e0e5..b7d99b944 100644
--- a/htools/Ganeti/Luxi.hs
+++ b/htools/Ganeti/Luxi.hs
@@ -30,6 +30,8 @@ module Ganeti.Luxi
   , LuxiReq(..)
   , Client
   , JobId
+  , fromJobId
+  , makeJobId
   , RecvResult(..)
   , strOfOp
   , getClient
@@ -52,7 +54,6 @@ module Ganeti.Luxi
 
 import Control.Exception (catch)
 import Data.IORef
-import Data.Ratio (numerator, denominator)
 import qualified Data.ByteString as B
 import qualified Data.ByteString.UTF8 as UTF8
 import Data.Word (Word8)
@@ -74,9 +75,9 @@ import Ganeti.JSON
 import Ganeti.Jobs (JobStatus)
 import Ganeti.OpParams (pTagsObject)
 import Ganeti.OpCodes
-import Ganeti.Utils
 import qualified Ganeti.Query.Language as Qlang
 import Ganeti.THH
+import Ganeti.Types
 
 -- * Utility functions
 
@@ -96,9 +97,6 @@ data RecvResult = RecvConnClosed    -- ^ Connection closed
                 | RecvOk String     -- ^ Successfull receive
                   deriving (Show, Eq)
 
--- | The Ganeti job type.
-type JobId = Int
-
 -- | Currently supported Luxi operations and JSON serialization.
 $(genLuxiOp "LuxiOp"
   [ (luxiReqQuery,
@@ -126,7 +124,7 @@ $(genLuxiOp "LuxiOp"
      , simpleField "lock"   [t| Bool     |]
      ])
   , (luxiReqQueryJobs,
-     [ simpleField "ids"    [t| [Int]    |]
+     [ simpleField "ids"    [t| [JobId]  |]
      , simpleField "fields" [t| [String] |]
      ])
   , (luxiReqQueryExports,
@@ -146,24 +144,24 @@ $(genLuxiOp "LuxiOp"
      [ simpleField "ops" [t| [[OpCode]] |] ]
     )
   , (luxiReqWaitForJobChange,
-     [ simpleField "job"      [t| Int     |]
+     [ simpleField "job"      [t| JobId   |]
      , simpleField "fields"   [t| [String]|]
      , simpleField "prev_job" [t| JSValue |]
      , simpleField "prev_log" [t| JSValue |]
      , simpleField "tmout"    [t| Int     |]
      ])
   , (luxiReqArchiveJob,
-     [ simpleField "job" [t| Int |] ]
+     [ simpleField "job" [t| JobId |] ]
     )
   , (luxiReqAutoArchiveJobs,
      [ simpleField "age"   [t| Int |]
      , simpleField "tmout" [t| Int |]
      ])
   , (luxiReqCancelJob,
-     [ simpleField "job" [t| Int |] ]
+     [ simpleField "job" [t| JobId |] ]
     )
   , (luxiReqChangeJobPriority,
-     [ simpleField "job" [t| Int |]
+     [ simpleField "job"      [t| JobId |]
      , simpleField "priority" [t| Int |] ]
     )
   , (luxiReqSetDrainFlag,
@@ -326,10 +324,9 @@ decodeCall :: LuxiCall -> Result LuxiOp
 decodeCall (LuxiCall call args) =
   case call of
     ReqQueryJobs -> do
-              (jid, jargs) <- fromJVal args
-              rid <- mapM parseJobId jid
+              (jids, jargs) <- fromJVal args
               let rargs = map fromJSString jargs
-              return $ QueryJobs rid rargs
+              return $ QueryJobs jids rargs
     ReqQueryInstances -> do
               (names, fields, locking) <- fromJVal args
               return $ QueryInstances names fields locking
@@ -372,12 +369,10 @@ decodeCall (LuxiCall call args) =
                     J.readJSON d `ap`
                     J.readJSON e
                   _ -> J.Error "Not enough values"
-              rid <- parseJobId jid
-              return $ WaitForJobChange rid fields pinfo pidx wtmout
+              return $ WaitForJobChange jid fields pinfo pidx wtmout
     ReqArchiveJob -> do
               [jid] <- fromJVal args
-              rid <- parseJobId jid
-              return $ ArchiveJob rid
+              return $ ArchiveJob jid
     ReqAutoArchiveJobs -> do
               (age, tmout) <- fromJVal args
               return $ AutoArchiveJobs age tmout
@@ -392,13 +387,11 @@ decodeCall (LuxiCall call args) =
               item <- tagObjectFrom kind name
               return $ QueryTags item
     ReqCancelJob -> do
-              [job] <- fromJVal args
-              rid <- parseJobId job
-              return $ CancelJob rid
+              [jid] <- fromJVal args
+              return $ CancelJob jid
     ReqChangeJobPriority -> do
-              (job, priority) <- fromJVal args
-              rid <- parseJobId job
-              return $ ChangeJobPriority rid priority
+              (jid, priority) <- fromJVal args
+              return $ ChangeJobPriority jid priority
     ReqSetDrainFlag -> do
               [flag] <- fromJVal args
               return $ SetDrainFlag flag
@@ -437,22 +430,12 @@ callMethod method s = do
   let rval = validateResult result
   return rval
 
--- | Parses a job ID.
-parseJobId :: JSValue -> Result JobId
-parseJobId (JSString x) = tryRead "parsing job id" . fromJSString $ x
-parseJobId (JSRational _ x) =
-  if denominator x /= 1
-    then Bad $ "Got fractional job ID from master daemon?! Value:" ++ show x
-    -- FIXME: potential integer overflow here on 32-bit platforms
-    else Ok . fromIntegral . numerator $ x
-parseJobId x = Bad $ "Wrong type/value for job id: " ++ show x
-
 -- | Parse job submission result.
 parseSubmitJobResult :: JSValue -> ErrorResult JobId
 parseSubmitJobResult (JSArray [JSBool True, v]) =
-  case parseJobId v of
-    Bad msg -> Bad $ LuxiError msg
-    Ok v' -> Ok v'
+  case J.readJSON v of
+    J.Error msg -> Bad $ LuxiError msg
+    J.Ok v' -> Ok v'
 parseSubmitJobResult (JSArray [JSBool False, JSString x]) =
   Bad . LuxiError $ fromJSString x
 parseSubmitJobResult v =
diff --git a/htools/Ganeti/Types.hs b/htools/Ganeti/Types.hs
index 90df5656f..b70d70413 100644
--- a/htools/Ganeti/Types.hs
+++ b/htools/Ganeti/Types.hs
@@ -73,13 +73,18 @@ module Ganeti.Types
   , nICModeToRaw
   , FinalizedJobStatus(..)
   , finalizedJobStatusToRaw
+  , JobId
+  , fromJobId
+  , makeJobId
   ) where
 
 import qualified Text.JSON as JSON
+import Data.Ratio (numerator, denominator)
 
 import qualified Ganeti.Constants as C
 import qualified Ganeti.THH as THH
 import Ganeti.JSON
+import Ganeti.Utils
 
 -- * Generic types
 
@@ -342,3 +347,27 @@ $(THH.declareSADT "FinalizedJobStatus"
   , ("JobStatusFailed",     'C.jobStatusError)
   ])
 $(THH.makeJSONInstance ''FinalizedJobStatus)
+
+-- | The Ganeti job type.
+newtype JobId = JobId { fromJobId :: Int }
+  deriving (Show, Eq)
+
+-- | Builds a job ID.
+makeJobId :: (Monad m) => Int -> m JobId
+makeJobId i | i >= 0 = return $ JobId i
+            | otherwise = fail $ "Invalid value for job ID ' " ++ show i ++ "'"
+
+-- | Parses a job ID.
+parseJobId :: (Monad m) => JSON.JSValue -> m JobId
+parseJobId (JSON.JSString x) =
+  tryRead "parsing job id" (JSON.fromJSString x) >>= makeJobId
+parseJobId (JSON.JSRational _ x) =
+  if denominator x /= 1
+    then fail $ "Got fractional job ID from master daemon?! Value:" ++ show x
+    -- FIXME: potential integer overflow here on 32-bit platforms
+    else makeJobId . fromIntegral . numerator $ x
+parseJobId x = fail $ "Wrong type/value for job id: " ++ show x
+
+instance JSON.JSON JobId where
+  showJSON = JSON.showJSON . fromJobId
+  readJSON = parseJobId
-- 
GitLab