Skip to content

Commit

Permalink
Add sections and section[s]Of combinators to old API
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
aisamanra committed Jan 11, 2018
1 parent 1cb76ba commit 2dc3fd7
Showing 1 changed file with 68 additions and 5 deletions.
73 changes: 68 additions & 5 deletions src/Data/Ini/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@ and the individual key-value pairs contained within those chunks.
For example, the following INI file has two sections, @NETWORK@
and @LOCAL@, and each contains its own key-value pairs. Comments,
which begin with @#@ or @;@, are ignored:
--
> [NETWORK]
> host = example.com
> port = 7878
>
> # here is a comment
> [LOCAL]
> user = terry
--
The combinators provided here are designed to write quick and
idiomatic parsers for files of this form. 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:
--
> data Config = Config
> { cfNetwork :: NetworkConfig, cfLocal :: Maybe LocalConfig }
> deriving (Eq, Show)
Expand All @@ -50,11 +50,12 @@ parse an INI file like the one above, treating the entire
> locCf <- sectionMb "LOCAL" $
> LocalConfig <$> field "user"
> return Config { cfNetwork = netCf, cfLocal = locCf }
--
We can run our computation with 'parseIniFile', which,
when run on our example file above, would produce the
following:
--
>>> parseIniFile example configParser
Right (Config {cfNetwork = NetworkConfig {netHost = "example.com", netPort = 7878}, cfLocal = Just (LocalConfig {localUser = "terry"})})
Expand All @@ -73,6 +74,9 @@ module Data.Ini.Config
, SectionParser
-- * Section-Level Parsing
, section
, sections
, sectionOf
, sectionsOf
, sectionMb
, sectionDef
-- * Field-Level Parsing
Expand Down Expand Up @@ -150,6 +154,65 @@ section name (SectionParser thunk) = IniParser $ ExceptT $ \(RawIni ini) ->
Nothing -> Left ("No top-level section named " ++ show name)
Just sec -> runExceptT thunk sec

-- | Find multiple named sections in the INI file and parse them all
-- with the provided section parser. In order to support classic INI
-- files with capitalized section names, section lookup is
-- __case-insensitive__.
--
-- >>> parseIniFile "[ONE]\nx = hello\n[ONE]\nx = goodbye\n" $ sections "ONE" (field "x")
-- Right (fromList ["hello","goodbye"])
-- >>> parseIniFile "[ONE]\nx = hello\n" $ sections "TWO" (field "x")
-- Right (fromList [])
sections :: Text -> SectionParser a -> IniParser (Seq a)
sections name (SectionParser thunk) = IniParser $ ExceptT $ \(RawIni ini) ->
let name' = normalize name
in mapM (runExceptT thunk . snd)
(Seq.filter (\ (t, _) -> t == name') ini)

-- | A call to @sectionOf f@ will apply @f@ to each section name and,
-- if @f@ produces a "Just" value, pass the extracted value in order
-- to get the "SectionParser" to use for that section. This will
-- find at most one section, and will produce an error if no section
-- exists.
--
-- >>> parseIniFile "[FOO]\nx = hello\n" $ sectionOf (T.stripSuffix "OO") (\ l -> fmap ((,) l) (field "x"))
-- Right ("F","hello")
-- >>> parseIniFile "[BAR]\nx = hello\n" $ sectionOf (T.stripSuffix "OO") (\ l -> fmap ((,) l) (field "x"))
-- Left "No matching top-level section"
sectionOf :: (Text -> Maybe b) -> (b -> SectionParser a) -> IniParser a
sectionOf fn sectionParser = IniParser $ ExceptT $ \(RawIni ini) ->
let go Seq.EmptyL = Left "No matching top-level section"
go ((t, sec) Seq.:< rs)
| Just v <- fn (actualText t) =
let SectionParser thunk = sectionParser v
in runExceptT thunk sec
| otherwise = go (Seq.viewl rs)
in go (Seq.viewl ini)


-- | A call to @sectionsOf f@ will apply @f@ to each section name and,
-- if @f@ produces a @Just@ value, pass the extracted value in order
-- to get the "SectionParser" to use for that section. This will
-- return every section for which the call to @f@ produces a "Just"
-- value.
--
-- >>> parseIniFile "[FOO]\nx = hello\n[BOO]\nx = goodbye\n" $ sectionsOf (T.stripSuffix "OO") (\ l -> fmap ((,) l) (field "x"))
-- Right (fromList [("F","hello"),("B","goodbye")])
-- >>> parseIniFile "[BAR]\nx = hello\n" $ sectionsOf (T.stripSuffix "OO") (\ l -> fmap ((,) l) (field "x"))
-- Right (fromList [])
sectionsOf :: (Text -> Maybe b) -> (b -> SectionParser a) -> IniParser (Seq a)
sectionsOf fn sectionParser = IniParser $ ExceptT $ \(RawIni ini) ->
let go Seq.EmptyL = return Seq.empty
go ((t, sec) Seq.:< rs)
| Just v <- fn (actualText t) =
let SectionParser thunk = sectionParser v
in do
x <- runExceptT thunk sec
xs <- go (Seq.viewl rs)
return (x Seq.<| xs)
| otherwise = go (Seq.viewl rs)
in go (Seq.viewl ini)

-- | Find a named section in the INI file and parse it with the provided
-- section parser, returning 'Nothing' if the section does not exist.
-- In order to
Expand Down

0 comments on commit 2dc3fd7

Please sign in to comment.