From edc1acde4621c2a9d1dc5220c09e3503a83f0ec1 Mon Sep 17 00:00:00 2001
From: Iustin Pop <iustin@google.com>
Date: Fri, 16 Nov 2012 14:32:53 +0100
Subject: [PATCH] Generalise the JSON "Container" type
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Currently, we have some types that we kept as dictionaries in the
objects representation (due to inconsistent behaviour, or other
technical reasons). This should be improved in the future, but in the
meantime we can improve the Container type by allowing its keys to be
non-strings; this is needed, for example, for better safe in
DiskParams type, where we don't want arbitrary strings as keys, but
only the actually defined types.

To implement this change, we generalise the type (Container β†’
GenericContainer), and in the process we introduce a type class for
"things that can be represented as strings". This is very similar to a
combination of Read and Show, but with custom string
representation. The new type class is needed because in JSON
representation, object keys must be strings, so we need to be able to
serialised/deserialise the generic keys to/from plain strings.

We also add the instance for DiskTemplate at the same time.

Signed-off-by: Iustin Pop <iustin@google.com>
Reviewed-by: Adeodato Simo <dato@google.com>
---
 htest/Test/Ganeti/Objects.hs | 25 ++++++++++----------
 htools/Ganeti/Config.hs      |  2 +-
 htools/Ganeti/JSON.hs        | 46 ++++++++++++++++++++++++++++--------
 htools/Ganeti/Types.hs       |  5 ++++
 4 files changed, 55 insertions(+), 23 deletions(-)

diff --git a/htest/Test/Ganeti/Objects.hs b/htest/Test/Ganeti/Objects.hs
index 6dc0af93c..de21d0c15 100644
--- a/htest/Test/Ganeti/Objects.hs
+++ b/htest/Test/Ganeti/Objects.hs
@@ -102,9 +102,9 @@ instance Arbitrary Instance where
       <$> getFQDN <*> getFQDN <*> getFQDN -- OS name, but...
       <*> arbitrary
       -- FIXME: add non-empty hvparams when they're a proper type
-      <*> pure (Container Map.empty) <*> arbitrary
+      <*> pure (GenericContainer Map.empty) <*> arbitrary
       -- ... and for OSParams
-      <*> pure (Container Map.empty) <*> arbitrary <*> arbitrary
+      <*> pure (GenericContainer Map.empty) <*> arbitrary <*> arbitrary
       <*> arbitrary <*> arbitrary <*> arbitrary
       -- ts
       <*> arbitrary <*> arbitrary
@@ -127,7 +127,7 @@ $(genArbitrary ''PartialIPolicy)
 -- validation rules.
 instance Arbitrary NodeGroup where
   arbitrary = NodeGroup <$> getFQDN <*> pure [] <*> arbitrary <*> arbitrary
-                        <*> arbitrary <*> pure (Container Map.empty)
+                        <*> arbitrary <*> pure (GenericContainer Map.empty)
                         -- ts
                         <*> arbitrary <*> arbitrary
                         -- uuid
@@ -146,23 +146,23 @@ $(genArbitrary ''FilledBeParams)
 
 -- | No real arbitrary instance for 'ClusterHvParams' yet.
 instance Arbitrary ClusterHvParams where
-  arbitrary = return $ Container Map.empty
+  arbitrary = return $ GenericContainer Map.empty
 
 -- | No real arbitrary instance for 'OsHvParams' yet.
 instance Arbitrary OsHvParams where
-  arbitrary = return $ Container Map.empty
+  arbitrary = return $ GenericContainer Map.empty
 
 instance Arbitrary ClusterNicParams where
-  arbitrary = (Container . Map.singleton C.ppDefault) <$> arbitrary
+  arbitrary = (GenericContainer . Map.singleton C.ppDefault) <$> arbitrary
 
 instance Arbitrary OsParams where
-  arbitrary = (Container . Map.fromList) <$> arbitrary
+  arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
 
 instance Arbitrary ClusterOsParams where
-  arbitrary = (Container . Map.fromList) <$> arbitrary
+  arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
 
 instance Arbitrary ClusterBeParams where
-  arbitrary = (Container . Map.fromList) <$> arbitrary
+  arbitrary = (GenericContainer . Map.fromList) <$> arbitrary
 
 instance Arbitrary TagSet where
   arbitrary = Set.fromList <$> genTags
@@ -179,10 +179,11 @@ genEmptyCluster ncount = do
       nodes' = zipWith (\n idx -> n { nodeGroup = guuid,
                                       nodeName = nodeName n ++ show idx })
                nodes [(1::Int)..]
-      contnodes = Container . Map.fromList $ map (\n -> (nodeName n, n)) nodes'
-      continsts = Container Map.empty
+      contnodes = GenericContainer . Map.fromList $
+                  map (\n -> (nodeName n, n)) nodes'
+      continsts = GenericContainer Map.empty
   grp <- arbitrary
-  let contgroups = Container $ Map.singleton guuid grp
+  let contgroups = GenericContainer $ Map.singleton guuid grp
   serial <- arbitrary
   cluster <- resize 8 arbitrary
   let c = ConfigData version cluster contnodes contgroups continsts serial
diff --git a/htools/Ganeti/Config.hs b/htools/Ganeti/Config.hs
index 62582735c..c2c692905 100644
--- a/htools/Ganeti/Config.hs
+++ b/htools/Ganeti/Config.hs
@@ -192,7 +192,7 @@ getGroupIpolicy cfg ng =
 -- | Computes a group\'s (merged) disk params.
 getGroupDiskParams :: ConfigData -> NodeGroup -> DiskParams
 getGroupDiskParams cfg ng =
-  Container $
+  GenericContainer $
   fillDict (fromContainer . clusterDiskparams $ configCluster cfg)
            (fromContainer $ groupDiskparams ng) []
 
diff --git a/htools/Ganeti/JSON.hs b/htools/Ganeti/JSON.hs
index 1480140e8..113889834 100644
--- a/htools/Ganeti/JSON.hs
+++ b/htools/Ganeti/JSON.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
 {-| JSON utility functions. -}
 
 {-
@@ -37,11 +38,12 @@ module Ganeti.JSON
   , asObjectList
   , tryFromObj
   , toArray
-  , Container(..)
+  , HasStringRepr(..)
+  , GenericContainer(..)
+  , Container
   )
   where
 
-import Control.Arrow (second)
 import Control.Monad (liftM)
 import Data.Maybe (fromMaybe)
 import qualified Data.Map as Map
@@ -167,24 +169,48 @@ toArray o =
 
 -- * Container type (special type for JSON serialisation)
 
+-- | Class of types that can be converted from Strings. This is
+-- similar to the 'Read' class, but it's using a different
+-- serialisation format, so we have to define a separate class. Mostly
+-- useful for custom key types in JSON dictionaries, which have to be
+-- backed by strings.
+class HasStringRepr a where
+  fromStringRepr :: (Monad m) => String -> m a
+  toStringRepr :: a -> String
+
+-- | Trivial instance 'HasStringRepr' for 'String'.
+instance HasStringRepr String where
+  fromStringRepr = return
+  toStringRepr = id
+
 -- | The container type, a wrapper over Data.Map
-newtype Container a = Container { fromContainer :: Map.Map String a }
+newtype GenericContainer a b =
+  GenericContainer { fromContainer :: Map.Map a b }
   deriving (Show, Read, Eq)
 
+-- | Type alias for string keys.
+type Container = GenericContainer String
+
 -- | Container loader.
-readContainer :: (Monad m, J.JSON a) =>
-                 J.JSObject J.JSValue -> m (Container a)
+readContainer :: (Monad m, HasStringRepr a, Ord a, J.JSON b) =>
+                 J.JSObject J.JSValue -> m (GenericContainer a b)
 readContainer obj = do
   let kjvlist = J.fromJSObject obj
-  kalist <- mapM (\(k, v) -> fromKeyValue k v >>= \a -> return (k, a)) kjvlist
-  return $ Container (Map.fromList kalist)
+  kalist <- mapM (\(k, v) -> do
+                    k' <- fromStringRepr k
+                    v' <- fromKeyValue k v
+                    return (k', v')) kjvlist
+  return $ GenericContainer (Map.fromList kalist)
 
 -- | Container dumper.
-showContainer :: (J.JSON a) => Container a -> J.JSValue
+showContainer :: (HasStringRepr a, J.JSON b) =>
+                 GenericContainer a b -> J.JSValue
 showContainer =
-  J.makeObj . map (second J.showJSON) . Map.toList . fromContainer
+  J.makeObj . map (\(k, v) -> (toStringRepr k, J.showJSON v)) .
+  Map.toList . fromContainer
 
-instance (J.JSON a) => J.JSON (Container a) where
+instance (HasStringRepr a, Ord a, J.JSON b) =>
+         J.JSON (GenericContainer a b) where
   showJSON = showContainer
   readJSON (J.JSObject o) = readContainer o
   readJSON v = fail $ "Failed to load container, expected object but got "
diff --git a/htools/Ganeti/Types.hs b/htools/Ganeti/Types.hs
index a31cf62ad..99eceae14 100644
--- a/htools/Ganeti/Types.hs
+++ b/htools/Ganeti/Types.hs
@@ -60,6 +60,7 @@ import qualified Text.JSON as JSON
 
 import qualified Ganeti.Constants as C
 import qualified Ganeti.THH as THH
+import Ganeti.JSON
 
 -- * Generic types
 
@@ -118,6 +119,10 @@ $(THH.declareSADT "DiskTemplate"
        ])
 $(THH.makeJSONInstance ''DiskTemplate)
 
+instance HasStringRepr DiskTemplate where
+  fromStringRepr = diskTemplateFromRaw
+  toStringRepr = diskTemplateToRaw
+
 -- | The Group allocation policy type.
 --
 -- Note that the order of constructors is important as the automatic
-- 
GitLab