I am trying to get a Config data record out of the following code:
data Connections = Connections { cfgProperty :: !Object
, connectionName :: String
} deriving (Show, Generic)
data Config = Config {connections :: [Connections]} deriving (Show, Generic)
data Cfg = Cfg { config :: Config } deriving (Show, Generic)
instance FromJSON Cfg
instance FromJSON Config
instance FromJSON Connections
instance ToJSON Cfg
instance ToJSON Config
instance ToJSON Connections
jsonFile :: FilePath
jsonFile = "config/config.json"
getCfg :: IO B.ByteString
getCfg = B.readFile jsonFile
parseCfg = do
j <- (A.eitherDecode <$> getCfg) :: IO (Either String Cfg)
case j of
Left err -> liftIO $ putStrLn err
Right j -> config j
I'm getting the following error:
/apps/workspace/hade/src/Actor/MasterActor.hs: 56, 20
• Couldn't match expected type ‘IO ()’ with actual type ‘Config’
• In the expression: config j
In a case alternative: Right j -> config j
In a stmt of a 'do' block:
case j of {
Left err -> liftIO $ putStrLn err
Right j -> config j }
Here is config.json
{
"config":
{
"connections": [
{
"cfgProperty":
{
"Env": "local",
"Host": "localhost",
"Port": "8001",
"Directory": "/apps/workspace/hade/maps/src01_cvs"
},
"connectionName": "src01_cvs"
},
{
"cfgProperty":
{
"Env": "local",
"Host": "localhost",
"Port": "8001",
"Directory": "/apps/workspace/hade/maps/trg01_cvs"
},
"connectionName": "trg01_cvs"
}
]
}
}{
"config":
{
"connections": [
{
"cfgProperty":
{
"Env": "local",
"Host": "localhost",
"Port": "8001",
"Directory": "/apps/workspace/hade/maps/src01_cvs"
},
"connectionName": "src01_cvs"
},
{
"cfgProperty":
{
"Env": "local",
"Host": "localhost",
"Port": "8001",
"Directory": "/apps/workspace/hade/maps/trg01_cvs"
},
"connectionName": "trg01_cvs"
}
]
}
}
I have tried many different configurations using both eitherDecode and decode, but I have run into roadblocks each time. I can get the code to print out the Config record if I change the case to be:
case j of
Left err -> putStrLn err
Right j -> print $ config j
(along with some other changes), but I cannot get it to simply return the Config record itself. Any assistance would be appreciated.
The root of the problem that you are running into is the call to getCfg
in parseCfg
. The type of getCfg
is IO ByteString
which is an IO
monad and the only things that you can do with an IO
monad are to call the functions that must be defined for all monads (bind
, fmap
, ap
, ...) all of which return another IO
monad. This means that if parseCfg
is going to call getCfg
then it must return an IO
monad.
To quote the haskell wiki : "Because you can't escape from the IO monad, it is impossible to write a function that does a computation in the IO monad but whose result type does not include the IO
type constructor."
One way around this is to call getCfg
outside of parseCfg
and pass the result to parseCfg
. Then parseCfg
could return a Config
but it makes more sense to return Either String Config
so that any parsing error from eitherDecode
is preserved. This lets you define parseCfg
as nothing more than an fmap
of config
over the eitherDecode
return value.
The resulting function looks like this:
parseCfg :: ByteString -> Either String Config
parseCfg json = config <$> eitherDecode json
Here is your program, modified to run with the above parseCfg
.
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TemplateHaskell #-}
import Data.ByteString.Lazy as B
import Data.Aeson
import Data.Aeson.TH
import GHC.Generics
import Control.Monad.IO.Class
data CfgProperty = CfgProperty {
env :: String,
host :: String,
port :: String,
directory :: String
} deriving (Show, Generic)
-- Instead of FromJSON, use deriveJSON for CfgProperty
-- which allows changing of field labels from lower
-- case to upper case.
$(deriveJSON
defaultOptions { fieldLabelModifier = let f "env" = "Env"
f "host" = "Host"
f "port" = "Port"
f "directory" = "Directory"
f other = other
in f
}
''CfgProperty
)
data Connections = Connections { cfgProperty :: CfgProperty
, connectionName :: String
} deriving (Show, Generic)
data Config = Config {connections :: [Connections]} deriving (Show, Generic)
data Cfg = Cfg { config :: Config } deriving (Show, Generic)
instance FromJSON Cfg
instance FromJSON Config
instance FromJSON Connections
jsonFile :: FilePath
jsonFile = "config/config.json"
getCfg :: IO ByteString
getCfg = B.readFile jsonFile
parseCfg :: ByteString -> Either String Config
parseCfg json = config <$> eitherDecode json
main :: IO()
main = do
json <- getCfg
case parseCfg json of
Left err -> Prelude.putStrLn err
Right cfg -> print cfg -- do whatever you want with cfg here.
Here's a modified config.json
. The original contained two config
JSON
records with nothing between them which was not valid JSON
, and was not pertinent to the question.
{
"config":
{
"connections": [
{
"cfgProperty":
{
"Env": "local",
"Host": "localhost",
"Port": "8001",
"Directory": "/apps/workspace/hade/maps/src01_cvs"
},
"connectionName": "src01_cvs"
},
{
"cfgProperty":
{
"Env": "local",
"Host": "localhost",
"Port": "8001",
"Directory": "/apps/workspace/hade/maps/trg01_cvs"
},
"connectionName": "trg01_cvs"
}
]
}
}