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
7 years ago
| 12 | 12 | For example, the following INI file has two sections, @NETWORK@ |
| 13 | 13 | and @LOCAL@, and each contains its own key-value pairs. Comments, |
| 14 | 14 | which begin with @#@ or @;@, are ignored: |
| 15 | -- | |
| 15 | ||
| 16 | 16 | > [NETWORK] |
| 17 | 17 | > host = example.com |
| 18 | 18 | > port = 7878 |
| 20 | 20 | > # here is a comment |
| 21 | 21 | > [LOCAL] |
| 22 | 22 | > user = terry |
| 23 | -- | |
| 23 | ||
| 24 | 24 | The combinators provided here are designed to write quick and |
| 25 | 25 | idiomatic parsers for files of this form. Sections are parsed by |
| 26 | 26 | 'IniParser' computations, like 'section' and its variations, |
| 28 | 28 | computations, like 'field' and its variations. If we want to |
| 29 | 29 | parse an INI file like the one above, treating the entire |
| 30 | 30 | @LOCAL@ section as optional, we can write it like this: |
| 31 | -- | |
| 31 | ||
| 32 | 32 | > data Config = Config |
| 33 | 33 | > { cfNetwork :: NetworkConfig, cfLocal :: Maybe LocalConfig } |
| 34 | 34 | > deriving (Eq, Show) |
| 50 | 50 | > locCf <- sectionMb "LOCAL" $ |
| 51 | 51 | > LocalConfig <$> field "user" |
| 52 | 52 | > return Config { cfNetwork = netCf, cfLocal = locCf } |
| 53 | -- | |
| 53 | ||
| 54 | ||
| 54 | 55 | We can run our computation with 'parseIniFile', which, |
| 55 | 56 | when run on our example file above, would produce the |
| 56 | 57 | following: |
| 57 | -- | |
| 58 | ||
| 58 | 59 | >>> parseIniFile example configParser |
| 59 | 60 | Right (Config {cfNetwork = NetworkConfig {netHost = "example.com", netPort = 7878}, cfLocal = Just (LocalConfig {localUser = "terry"})}) |
| 60 | 61 | |
| 73 | 74 | , SectionParser |
| 74 | 75 | -- * Section-Level Parsing |
| 75 | 76 | , section |
| 77 | , sections | |
| 78 | , sectionOf | |
| 79 | , sectionsOf | |
| 76 | 80 | , sectionMb |
| 77 | 81 | , sectionDef |
| 78 | 82 | -- * Field-Level Parsing |
| 149 | 153 | case lkp (normalize name) ini of |
| 150 | 154 | Nothing -> Left ("No top-level section named " ++ show name) |
| 151 | 155 | 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) | |
| 152 | 215 | |
| 153 | 216 | -- | Find a named section in the INI file and parse it with the provided |
| 154 | 217 | -- section parser, returning 'Nothing' if the section does not exist. |