1# `config-ini`
2
3[![Hackage](https://img.shields.io/hackage/v/config-ini.svg)](https://hackage.haskell.org/package/config-ini) ![stability: stable](https://img.shields.io/badge/stability-stable-green.svg)
4
5The `config-ini` library is a Haskell library for doing elementary INI file parsing in a quick and painless way.
6
7## Basic Usage
8
9The `config-ini` library exports some simple monadic functions to make parsing INI-like configuration easier. INI files have a two-level structure: the top-level named chunks of configuration, and the individual key-value pairs contained within those chunks. For example, the following INI file has two sections, `NETWORK` and `LOCAL`, and each section contains its own key-value pairs separated by either `=` or `:`. Comments, which begin with `#` or `;`, are ignored:
10
11~~~.ini
12[NETWORK]
13host = example.com
14port = 7878
15
16# here is a comment
17[LOCAL]
18user = terry
19~~~
20
21The combinators provided here are designed to write quick and idiomatic parsers for basic INI files. Sections are parsed by `IniParser` computations, like `section` and its variations, while the fields within sections are parsed by `SectionParser` computations, like `field` and its variations. If we want to parse an INI file like the one above, treating the entire `LOCAL` section as optional, we can write it like this:
22
23~~~haskell
24data Config = Config
25  { cfNetwork :: NetworkConfig
26  , cfLocal :: Maybe LocalConfig
27  } deriving (Eq, Show)
28
29data NetworkConfig = NetworkConfig
30  { netHost :: String
31  , netPort :: Int
32  } deriving (Eq, Show)
33
34data LocalConfig = LocalConfig
35  { localUser :: Text
36  } deriving (Eq, Show)
37
38configParser :: IniParser Config
39configParser = do
40  netCf <- section "NETWORK" $ do
41    host <- fieldOf "host" string
42    port <- fieldOf "port" number
43    return NetworkConfig { netHost = host, netPort = port }
44  locCf <- sectionMb "LOCAL" $
45    LocalConfig <$> field "user"
46  return Config { cfNetwork = netCf, cfLocal = locCf }
47~~~
48
49We can run our computation with `parseIniFile`, which, when run on our example file above, would produce the following:
50
51~~~haskell
52>>> parseIniFile example configParser
53Right (Config {cfNetwork = NetworkConfig {netHost = "example.com", netPort = 7878}, cfLocal = Just (LocalConfig {localUser = "terry"})})
54~~~
55
56## Bidirectional Usage
57
58The above example had an INI file split into two sections (`NETWORK` and `LOCAL`) and a data type with a corresponding structure (containing a `NetworkConfig` and `Maybe LocalConfig` field), which allowed each `section`-level parser to construct a chunk of the configuration and then combine them. This works well if our configuration file has the same structure as our data type, but that might not be what we want. Let's imagine we want to construct our `Config` type as a flat record like this:
59
60~~~haskell
61data Config = Config
62  { _cfHost :: String
63  , _cfPort :: Int
64  , _cfUser :: Maybe Text
65  } deriving (Eq, Show)
66~~~
67
68In this case, we can't construct a `Config` value until we've parsed all three fields in two distinct subsections. One way of doing this is to return the intermediate values from our `section` parsers and construct the `Config` value at the end, once we have all three of its fields:
69
70~~~haskell
71configParser :: IniParser Config
72configParser = do
73  (host, port) <- section "NETWORK" $ do
74    h <- fieldOf "host" string
75    p <- fieldOf "port" number
76    return (h, p)
77  user <- section "LOCAL" $ fieldMb "user"
78  return (Config host port user)
79~~~
80
81This is unfortunately awkward and repetitive. An alternative is to flatten it out by repeating invocations of `section` like below, but this has its own problems, such as unnecessary repetition of the `"NETWORK"` string literal, unnecessarily repetitive lookups, and general verbosity:
82
83~~~haskell
84configParser :: IniParser Config
85configParser = do
86  host <- section "NETWORK" $ fieldOf "host" string
87  port <- section "NETWORK" $ fieldOf "port" number
88  user <- section "LOCAL" $ fieldMb "user"
89  return (Config host port user)
90~~~
91
92In situations like these, you can instead use the `Data.Ini.Config.Bidir` module, which provides a slightly different abstraction: the functions exported by this module assume that you start with a default configuration value, and parsing a field allows you to _update_ that configuration with the value of a field. The monads exported by this module have an extra type parameter that represents the type of the value being updated. The easiest way to use this module is by combining lenses with the `.=` and `.=?` operators, which take a lens and a description of a field, and produce a `SectionSpec` value that uses the provided lens to update the underlying type when parsing:
93
94~~~haskell
95makeLenses ''Config
96
97configParser :: IniSpec Config ()
98configParser = do
99  section "NETWORK" $ do
100    cfHost .=  field "host" string
101    cfPort .=  field "port" number
102  section "LOCAL" $ do
103    cfUser .=? field "user"
104~~~
105
106In order to use this as a parser, we will need to provide an existing value of `Config` so we can apply our updates to it. We combine the `IniSpec` defined above with a default config
107
108~~~haskell
109configIni :: Ini Config
110configIni =
111  let defConfig = Config "localhost" 8080 Nothing
112  in ini defConfig configParser
113
114myParseIni :: Text -> Either String Config
115myParseIni t = fmap getIniValue (parseIni t configIni)
116~~~
117
118This approach gives us other advantages, too. Each of the defined fields can be associated with some various pieces of metadata, marking them as optional for the purpose of parsing or associating a comment with them.
119
120~~~haskell
121
122configParser' :: IniSpec Config ()
123configParser' = do
124  section "NETWORK" $ do
125    cfHost .=  field "host" string
126      & comment ["The desired hostname"]
127      & optional
128    cfPort .=  field "port" number
129      & comment ["The port for the server"]
130  section "LOCAL" $ do
131    cfUser .=? field "user"
132      & comment ["The username"]
133~~~
134
135When we create an ini from this `IniSpec`, we can serialize it directly to get a "default" INI file, one which contains the supplied comments on each field. This is useful if our application wants to produce a default configuration from the same declarative specification as before.
136
137This approach also enables another, much more powerful feature: this enables us to perform a _diff-minimal update_. You'll notice that our `parseIni` function here doesn't give us back the value directly, but rather yet another `Ini` value from which we had to extract the value. This is because the `Ini` value also records incidental formatting choices of the input file: whitespace, comments, specifics of capitalization, and so forth. When we serialize an INI file that was returned by `parseIni`, we will get out _literally the same file_ that we put in, complete with incidental formatting choices retained.
138
139But we can also use that file and update it using the `updateIni` function: this takes a configuration value and a previous `Ini` value and builds a new `Ini` value such that as much structure as possible is retained from the original `Ini`. This means that if we parse a file, update a single field, and reserialize, that file should differ only in the field we changed _and that's it_: fields will stay in the same order (with new fields being added to the end of sections), comments will be retained, incidental whitespace will stay as it is.
140
141This is a useful tool if you're building an application that has both a human-readable configuration as well the ability to set configuration values from within the application itself. This will allow you to rewrite the configuration file while minimizing lossy changes to a possibly-hand-edited possibly-checked-into-git configuration file.
142
143## Combinators and Conventions
144
145There are several variations on the same basic functionality that appear in `config-ini`. All functions that start with `section` are for parsing section-level chunks of an INI file, while all functions that start with `field` are for parsing key-value pairs within a section. Because it's reasonably common, there are also special `fieldFlag` functions which return `Bool` values, parsed in a relatively loose way.
146
147All functions which end in `Mb` return a `Maybe` value, returning `Nothing` if the section or key was not found. All functions which end in `Def` take an additional default value, returning it if the section or key was not found. All functions which contain `Of` take a function of the type `Text -> Either String a`, which is used to attempt to decode or parse the extracted value.
148
149In total, there are three section-level parsers (`section`, `sectionMb`, and `sectionDef`) and eight field-level parsers (`field`, `fieldOf`, `fieldMb`, `fieldMbOf`, `fieldDef`, `fieldDefOf`, `fieldFlag`, `fieldFlagDef`). For the `_Of` functions, `config-ini` also provides several built-in parser functions which provide nice error messages on failure.
150