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
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. |