gdritter repos config-ini / 2dc3fd7
Add sections and section[s]Of combinators to old API This enables INI files that have several sections whose names match a particular schema, with the parsing step perhaps relying on the structure or exact shape of the name. Getty Ritter 6 years ago
1 changed file(s) with 68 addition(s) and 5 deletion(s). Collapse all Expand all
1212 For example, the following INI file has two sections, @NETWORK@
1313 and @LOCAL@, and each contains its own key-value pairs. Comments,
1414 which begin with @#@ or @;@, are ignored:
15 --
15
1616 > [NETWORK]
1717 > host = example.com
1818 > port = 7878
2020 > # here is a comment
2121 > [LOCAL]
2222 > user = terry
23 --
23
2424 The combinators provided here are designed to write quick and
2525 idiomatic parsers for files of this form. Sections are parsed by
2626 'IniParser' computations, like 'section' and its variations,
2828 computations, like 'field' and its variations. If we want to
2929 parse an INI file like the one above, treating the entire
3030 @LOCAL@ section as optional, we can write it like this:
31 --
31
3232 > data Config = Config
3333 > { cfNetwork :: NetworkConfig, cfLocal :: Maybe LocalConfig }
3434 > deriving (Eq, Show)
5050 > locCf <- sectionMb "LOCAL" $
5151 > LocalConfig <$> field "user"
5252 > return Config { cfNetwork = netCf, cfLocal = locCf }
53 --
53
54
5455 We can run our computation with 'parseIniFile', which,
5556 when run on our example file above, would produce the
5657 following:
57 --
58
5859 >>> parseIniFile example configParser
5960 Right (Config {cfNetwork = NetworkConfig {netHost = "example.com", netPort = 7878}, cfLocal = Just (LocalConfig {localUser = "terry"})})
6061
7374 , SectionParser
7475 -- * Section-Level Parsing
7576 , section
77 , sections
78 , sectionOf
79 , sectionsOf
7680 , sectionMb
7781 , sectionDef
7882 -- * Field-Level Parsing
149153 case lkp (normalize name) ini of
150154 Nothing -> Left ("No top-level section named " ++ show name)
151155 Just sec -> runExceptT thunk sec
156
157 -- | Find multiple named sections in the INI file and parse them all
158 -- with the provided section parser. In order to support classic INI
159 -- files with capitalized section names, section lookup is
160 -- __case-insensitive__.
161 --
162 -- >>> parseIniFile "[ONE]\nx = hello\n[ONE]\nx = goodbye\n" $ sections "ONE" (field "x")
163 -- Right (fromList ["hello","goodbye"])
164 -- >>> parseIniFile "[ONE]\nx = hello\n" $ sections "TWO" (field "x")
165 -- Right (fromList [])
166 sections :: Text -> SectionParser a -> IniParser (Seq a)
167 sections name (SectionParser thunk) = IniParser $ ExceptT $ \(RawIni ini) ->
168 let name' = normalize name
169 in mapM (runExceptT thunk . snd)
170 (Seq.filter (\ (t, _) -> t == name') ini)
171
172 -- | A call to @sectionOf f@ will apply @f@ to each section name and,
173 -- if @f@ produces a "Just" value, pass the extracted value in order
174 -- to get the "SectionParser" to use for that section. This will
175 -- find at most one section, and will produce an error if no section
176 -- exists.
177 --
178 -- >>> parseIniFile "[FOO]\nx = hello\n" $ sectionOf (T.stripSuffix "OO") (\ l -> fmap ((,) l) (field "x"))
179 -- Right ("F","hello")
180 -- >>> parseIniFile "[BAR]\nx = hello\n" $ sectionOf (T.stripSuffix "OO") (\ l -> fmap ((,) l) (field "x"))
181 -- Left "No matching top-level section"
182 sectionOf :: (Text -> Maybe b) -> (b -> SectionParser a) -> IniParser a
183 sectionOf fn sectionParser = IniParser $ ExceptT $ \(RawIni ini) ->
184 let go Seq.EmptyL = Left "No matching top-level section"
185 go ((t, sec) Seq.:< rs)
186 | Just v <- fn (actualText t) =
187 let SectionParser thunk = sectionParser v
188 in runExceptT thunk sec
189 | otherwise = go (Seq.viewl rs)
190 in go (Seq.viewl ini)
191
192
193 -- | A call to @sectionsOf f@ will apply @f@ to each section name and,
194 -- if @f@ produces a @Just@ value, pass the extracted value in order
195 -- to get the "SectionParser" to use for that section. This will
196 -- return every section for which the call to @f@ produces a "Just"
197 -- value.
198 --
199 -- >>> parseIniFile "[FOO]\nx = hello\n[BOO]\nx = goodbye\n" $ sectionsOf (T.stripSuffix "OO") (\ l -> fmap ((,) l) (field "x"))
200 -- Right (fromList [("F","hello"),("B","goodbye")])
201 -- >>> parseIniFile "[BAR]\nx = hello\n" $ sectionsOf (T.stripSuffix "OO") (\ l -> fmap ((,) l) (field "x"))
202 -- Right (fromList [])
203 sectionsOf :: (Text -> Maybe b) -> (b -> SectionParser a) -> IniParser (Seq a)
204 sectionsOf fn sectionParser = IniParser $ ExceptT $ \(RawIni ini) ->
205 let go Seq.EmptyL = return Seq.empty
206 go ((t, sec) Seq.:< rs)
207 | Just v <- fn (actualText t) =
208 let SectionParser thunk = sectionParser v
209 in do
210 x <- runExceptT thunk sec
211 xs <- go (Seq.viewl rs)
212 return (x Seq.<| xs)
213 | otherwise = go (Seq.viewl rs)
214 in go (Seq.viewl ini)
152215
153216 -- | Find a named section in the INI file and parse it with the provided
154217 -- section parser, returning 'Nothing' if the section does not exist.