gdritter repos config-ini / 903d6da
Update README bidir details Getty Ritter 7 years ago
1 changed file(s) with 37 addition(s) and 20 deletion(s). Collapse all Expand all
5353 Right (Config {cfNetwork = NetworkConfig {netHost = "example.com", netPort = 7878}, cfLocal = Just (LocalConfig {localUser = "terry"})})
5454 ~~~
5555
56 ## Setter- and Lens-Based Usage
56 ## Bidirectional Usage
5757
5858 The 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:
5959
7878 return (Config host port user)
7979 ~~~
8080
81 This 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 table lookups, and general verbosity:
81 This 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:
8282
8383 ~~~.haskell
8484 configParser :: IniParser Config
8989 return (Config host port user)
9090 ~~~
9191
92 In situations like these, you can instead use the `Data.Ini.Config.St` 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 normal `SectionParser` value, and produce a `SectionStParser` value that uses the lens to update the underlying type:
92 In 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:
9393
9494 ~~~.haskell
9595 makeLenses ''Config
9696
97 configParser :: IniStParser Config ()
97 configParser :: IniSpec Config ()
9898 configParser = do
99 sectionSt "NETWORK" $ do
100 cfHost .= fieldOf "host" string
101 cfPort .= fieldOf "port" number
102 sectionSt "LOCAL" $ do
103 cfUser .= fieldMb "user"
99 section "NETWORK" $ do
100 cfHost .= field "host" string
101 cfPort .= field "port" number
102 section "LOCAL" $ do
103 cfUser .=? field "user"
104104 ~~~
105105
106 In order to use this parser, we will need to provide an existing value of `Config` so we can apply our updates to it. This is the biggest downside to this approach: in this case, even though the `host` and `port` fields are obligatory and will be overwritten by the parser, we still need to provide dummy values for them.
106 In 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
107107
108108 ~~~.haskell
109 configIni :: Ini Config
110 configIni =
111 let defConfig = Config "localhost" 8080 Nothing
112 in ini defConfig configParser
113
109114 myParseIni :: Text -> Either String Config
110 myParseIni t = parseIniFileSt t defaultConfig configParser
111 where defaultConfig = Config "unset" 0 Nothing
115 myParseIni t = fmap getIniValue (parseIni t configIni)
112116 ~~~
113117
114 The `IniStParser` implementation isn't tied to lenses, and many of the functions exported by `Data.Ini.Config.St` expected any generic function of the type `a -> s -> s`, and not a lens specifically. If we didn't want to use lenses, we can still take advantage of this library, albeit in a more verbose way:
118 This 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.
115119
116120 ~~~.haskell
117 configParser :: IniStParser Config ()
118 configParser = do
119 sectionSt "NETWORK" $ do
120 fieldOfSt "host" string (\ h s -> s { _cfHost = h })
121 fieldOfSt "port" number (\ p s -> s { _cfPort = p })
122 sectionSt "LOCAL" $ do
123 fieldMbSt "user" (\ u s -> s { _cfUser = u })
121
122 configParser' :: IniSpec Config ()
123 configParser' = 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"]
124133 ~~~
134
135 When 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
137 This 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
139 But 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
141 This 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.
125142
126143 ## Combinators and Conventions
127144