infinite negative utility

config-ini

The config-ini library is a Haskell library for doing elementary INI file parsing in a quick and painless way. You can find the source code on Github and the full documentation for the library on Hackage.

There are two ways of using the library: one of them involves a traditional monadic DSL which parses field-by-field, and the other is a powerful bidirectional DSL which allows you to use the same declarative specification to parse, serialize, and diff-minimally update INI files.

Consider the following basic INI file:

[NETWORK]
host = example.com
port = 7878

# here is a comment
[LOCAL]
user = terry

We want to parse this into a Haskell data structure defined using this type, with its associated lenses:


data Config = Config
  { _cfHost :: String
  , _cfPort :: Int
  , _cfUser :: Maybe Text
  } deriving (Eq, Show)

makeLenses ''Config

Using config-ini's basic API, we can extract the fields using basic functions like fieldOf and section, which lookup and deserialize values from the INI file, and construct a Config value from those:

configParser :: IniParser Config
configParser = do
  (host, port) <- section "NETWORK" $ do
    host <- fieldOf "host" string
    port <- fieldOf "port" number
    pure (host, port)
  user <- sectionMb "LOCAL" $ field "user"
  return Config
    { _cfHost = host
    , _cfPort = port
    , _cfUser = user
    }

In order to use config-ini's bidirectional API, we instead have to use the generated lenses and associate them with descriptions of how to look up those individual fields, like this:

configSpec :: IniSpec Config ()
configSpec = do
  section "NETWORK" $ do
    cfHost .=  field "host" string
    cfPort .=  field "port" number
  section "LOCAL" $ do
    cfUser .=? field "user"

This defines a specification that we can use to create a parser, but in order to actually construct a value, we need a default Config value to supply to it, which we pass along with the IniSpec to the ini function, which gives us a value of type Ini, from which we can derive a parser:

configIni :: Ini Config
configIni =
  let defConfig = Config "localhost" 8080 Nothing
  in ini defConfig configSpec

myParseIni :: Text -> Either String Config
myParseIni t = fmap getIniValue (parseIni t configIni)

However, we can also serialize the Ini value, which takes the default value and turns it into a textual INI file. We can also use the Ini value to parse a file into a new Ini value, update it, and then re-serialize: the Ini value will contain all of the "structural" information about the file, like whitespace and comments and ordering, which means that the update is diff-minimal: all unchanged values will remain in the same order, changed value will be modified in-place with retained comments, and new values will appear grouped together at the end of their sections.