-
Notifications
You must be signed in to change notification settings - Fork 698
/
ParseUtils.hs
461 lines (417 loc) · 14.4 KB
/
ParseUtils.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- |
-- Module : Distribution.Client.ParseUtils
-- Maintainer : cabal-devel@haskell.org
-- Portability : portable
--
-- Parsing utilities.
module Distribution.Client.ParseUtils
( -- * Fields and field utilities
FieldDescr (..)
, liftField
, liftFields
, addFields
, aliasField
, filterFields
, mapFieldNames
, commandOptionToField
, commandOptionsToFields
-- * Sections and utilities
, SectionDescr (..)
, liftSection
-- * FieldGrammar sections
, FGSectionDescr (..)
-- * Parsing and printing flat config
, parseFields
, ppFields
, ppSection
-- * Parsing and printing config with sections and subsections
, parseFieldsAndSections
, ppFieldsAndSections
-- ** Top level of config files
, parseConfig
, showConfig
)
where
import Distribution.Client.Compat.Prelude hiding (empty, get)
import Prelude ()
import Distribution.Deprecated.ParseUtils
( Field (..)
, FieldDescr (..)
, LineNo
, PError (..)
, ParseResult (..)
, liftField
, lineNo
, readFields
, warning
)
import Distribution.Deprecated.ViewAsFieldDescr
( viewAsFieldDescr
)
import Distribution.Simple.Command
( OptionField
)
import qualified Data.ByteString as BS
import qualified Data.Map as Map
import Text.PrettyPrint (($+$))
import qualified Text.PrettyPrint as Disp
( Doc
, colon
, empty
, isEmpty
, nest
, text
, vcat
, (<>)
)
-- For new parser stuff
import Distribution.CabalSpecVersion (cabalSpecLatest)
import Distribution.FieldGrammar (parseFieldGrammar, partitionFields)
import qualified Distribution.FieldGrammar as FG
import qualified Distribution.Fields as F
import Distribution.Fields.ParseResult (runParseResult)
import Distribution.Parsec.Error (showPError)
import Distribution.Parsec.Position (Position (..))
import Distribution.Parsec.Warning (showPWarning)
import Distribution.Simple.Utils (fromUTF8BS, toUTF8BS)
-------------------------
-- FieldDescr utilities
--
liftFields
:: (b -> a)
-> (a -> b -> b)
-> [FieldDescr a]
-> [FieldDescr b]
liftFields get set = map (liftField get set)
-- | Given a collection of field descriptions, keep only a given list of them,
-- identified by name.
--
-- TODO: This makes it easy to footgun by providing a non-existent field name.
filterFields :: [String] -> [FieldDescr a] -> [FieldDescr a]
filterFields includeFields = filter ((`elem` includeFields) . fieldName)
-- | Given a collection of field descriptions, get a field with a given name.
getField :: String -> [FieldDescr a] -> Maybe (FieldDescr a)
getField name = find ((== name) . fieldName)
-- | Apply a name mangling function to the field names of all the field
-- descriptions. The typical use case is to apply some prefix.
mapFieldNames :: (String -> String) -> [FieldDescr a] -> [FieldDescr a]
mapFieldNames mangleName =
map (\descr -> descr{fieldName = mangleName (fieldName descr)})
-- | Reuse a command line 'OptionField' as a config file 'FieldDescr'.
commandOptionToField :: OptionField a -> FieldDescr a
commandOptionToField = viewAsFieldDescr
-- | Reuse a bunch of command line 'OptionField's as config file 'FieldDescr's.
commandOptionsToFields :: [OptionField a] -> [FieldDescr a]
commandOptionsToFields = map viewAsFieldDescr
-- | Add fields to a field list.
addFields
:: [FieldDescr a]
-> ([FieldDescr a] -> [FieldDescr a])
addFields = (++)
-- | Add a new field which is identical to an existing field but with a
-- different name.
aliasField
:: String
-- ^ The existing field name.
-> String
-- ^ The new field name.
-> [FieldDescr a]
-> [FieldDescr a]
aliasField oldName newName fields =
let fieldToRename = getField oldName fields
in case fieldToRename of
-- TODO: Should this throw?
Nothing -> fields
Just fieldToRename' ->
let newField = fieldToRename'{fieldName = newName}
in newField : fields
------------------------------------------
-- SectionDescr definition and utilities
--
-- | The description of a section in a config file. It can contain both
-- fields and optionally further subsections. See also 'FieldDescr'.
data SectionDescr a = forall b.
SectionDescr
{ sectionName :: String
, sectionFields :: [FieldDescr b]
, sectionSubsections :: [SectionDescr b]
, sectionGet :: a -> [(String, b)]
, sectionSet :: LineNo -> String -> b -> a -> ParseResult a
, sectionEmpty :: b
}
-- | 'FieldGrammar' section description
data FGSectionDescr g a = forall s.
FGSectionDescr
{ fgSectionName :: String
, fgSectionGrammar :: g s s
, -- todo: add subsections?
fgSectionGet :: a -> [(String, s)]
, fgSectionSet :: LineNo -> String -> s -> a -> ParseResult a
}
-- | To help construction of config file descriptions in a modular way it is
-- useful to define fields and sections on local types and then hoist them
-- into the parent types when combining them in bigger descriptions.
--
-- This is essentially a lens operation for 'SectionDescr' to help embedding
-- one inside another.
liftSection
:: (b -> a)
-> (a -> b -> b)
-> SectionDescr a
-> SectionDescr b
liftSection get' set' (SectionDescr name fields sections get set empty) =
let sectionGet' = get . get'
sectionSet' lineno param x y = do
x' <- set lineno param x (get' y)
return (set' x' y)
in SectionDescr name fields sections sectionGet' sectionSet' empty
-------------------------------------
-- Parsing and printing flat config
--
-- | Parse a bunch of semi-parsed 'Field's according to a set of field
-- descriptions. It accumulates the result on top of a given initial value.
--
-- This only covers the case of flat configuration without subsections. See
-- also 'parseFieldsAndSections'.
parseFields :: [FieldDescr a] -> a -> [Field] -> ParseResult a
parseFields fieldDescrs =
foldM setField
where
fieldMap = Map.fromList [(fieldName f, f) | f <- fieldDescrs]
setField accum (F line name value) =
case Map.lookup name fieldMap of
Just (FieldDescr _ _ set) -> set line value accum
Nothing -> do
-- the 'world-file' field was removed in 3.8, however
-- it was automatically added to many config files
-- before that, so its warning is silently ignored
unless (name == "world-file") $
warning $
"Unrecognized field " ++ name ++ " on line " ++ show line
return accum
setField accum f = do
warning $ "Unrecognized stanza on line " ++ show (lineNo f)
return accum
-- | This is a customised version of the functions from Distribution.Deprecated.ParseUtils
-- that also optionally print default values for empty fields as comments.
ppFields :: [FieldDescr a] -> (Maybe a) -> a -> Disp.Doc
ppFields fields def cur =
Disp.vcat
[ ppField name (fmap getter def) (getter cur)
| FieldDescr name getter _ <- fields
]
ppField :: String -> (Maybe Disp.Doc) -> Disp.Doc -> Disp.Doc
ppField name mdef cur
| Disp.isEmpty cur =
maybe
Disp.empty
( \def ->
Disp.text "--"
<+> Disp.text name
Disp.<> Disp.colon
<+> def
)
mdef
| otherwise = Disp.text name Disp.<> Disp.colon <+> cur
-- | Pretty print a section.
--
-- Since 'ppFields' does not cover subsections you can use this to add them.
-- Or alternatively use a 'SectionDescr' and use 'ppFieldsAndSections'.
ppSection :: String -> String -> [FieldDescr a] -> (Maybe a) -> a -> Disp.Doc
ppSection name arg fields def cur
| Disp.isEmpty fieldsDoc = Disp.empty
| otherwise =
Disp.text name
<+> argDoc
$+$ (Disp.nest 2 fieldsDoc)
where
fieldsDoc = ppFields fields def cur
argDoc
| arg == "" = Disp.empty
| otherwise = Disp.text arg
-----------------------------------------
-- Parsing and printing non-flat config
--
-- | Much like 'parseFields' but it also allows subsections. The permitted
-- subsections are given by a list of 'SectionDescr's.
parseFieldsAndSections
:: [FieldDescr a]
-- ^ field
-> [SectionDescr a]
-- ^ legacy sections
-> [FGSectionDescr FG.ParsecFieldGrammar a]
-- ^ FieldGrammar sections
-> a
-> [Field]
-> ParseResult a
parseFieldsAndSections fieldDescrs sectionDescrs fgSectionDescrs =
foldM setField
where
fieldMap = Map.fromList [(fieldName f, f) | f <- fieldDescrs]
sectionMap = Map.fromList [(sectionName s, s) | s <- sectionDescrs]
fgSectionMap = Map.fromList [(fgSectionName s, s) | s <- fgSectionDescrs]
setField a (F line name value) =
case Map.lookup name fieldMap of
Just (FieldDescr _ _ set) -> set line value a
Nothing ->
case Left <$> Map.lookup name sectionMap <|> Right <$> Map.lookup name fgSectionMap of
Just _ -> ParseFailed $ FieldShouldBeStanza name line
Nothing -> do
warning $
"Unrecognized field '"
++ name
++ "' on line "
++ show line
return a
setField a (Section line name param fields) =
case Left <$> Map.lookup name sectionMap <|> Right <$> Map.lookup name fgSectionMap of
Just (Left (SectionDescr _ fieldDescrs' sectionDescrs' _ set sectionEmpty)) -> do
b <- parseFieldsAndSections fieldDescrs' sectionDescrs' [] sectionEmpty fields
set line param b a
Just (Right (FGSectionDescr _ grammar _getter setter)) -> do
let fields1 = map convertField fields
(fields2, sections) = partitionFields fields1
-- TODO: recurse into sections
for_ (concat sections) $ \(FG.MkSection (F.Name (Position line' _) name') _ _) ->
warning $
"Unrecognized section '"
++ fromUTF8BS name'
++ "' on line "
++ show line'
case runParseResult $ parseFieldGrammar cabalSpecLatest fields2 grammar of
(warnings, Right b) -> do
for_ warnings $ \w -> warning $ showPWarning "???" w
setter line param b a
(warnings, Left (_, errs)) -> do
for_ warnings $ \w -> warning $ showPWarning "???" w
case errs of
err :| _errs -> fail $ showPError "???" err
Nothing -> do
warning $
"Unrecognized section '"
++ name
++ "' on line "
++ show line
return a
convertField :: Field -> F.Field Position
convertField (F line name str) =
F.Field (F.Name pos (toUTF8BS name)) [F.FieldLine pos $ toUTF8BS str]
where
pos = Position line 0
-- arguments omitted
convertField (Section line name _arg fields) =
F.Section (F.Name pos (toUTF8BS name)) [] (map convertField fields)
where
pos = Position line 0
-- | Much like 'ppFields' but also pretty prints any subsections. Subsection
-- are only shown if they are non-empty.
--
-- Note that unlike 'ppFields', at present it does not support printing
-- default values. If needed, adding such support would be quite reasonable.
ppFieldsAndSections :: [FieldDescr a] -> [SectionDescr a] -> [FGSectionDescr FG.PrettyFieldGrammar a] -> a -> Disp.Doc
ppFieldsAndSections fieldDescrs sectionDescrs fgSectionDescrs val =
ppFields fieldDescrs Nothing val
$+$ Disp.vcat
( [ Disp.text "" $+$ sectionDoc
| SectionDescr
{ sectionName
, sectionGet
, sectionFields
, sectionSubsections
} <-
sectionDescrs
, (param, x) <- sectionGet val
, let sectionDoc =
ppSectionAndSubsections
sectionName
param
sectionFields
sectionSubsections
[]
x
, not (Disp.isEmpty sectionDoc)
]
++ [ Disp.text "" $+$ sectionDoc
| FGSectionDescr{fgSectionName, fgSectionGrammar, fgSectionGet} <- fgSectionDescrs
, (param, x) <- fgSectionGet val
, let sectionDoc = ppFgSection fgSectionName param fgSectionGrammar x
, not (Disp.isEmpty sectionDoc)
]
)
-- | Unlike 'ppSection' which has to be called directly, this gets used via
-- 'ppFieldsAndSections' and so does not need to be exported.
ppSectionAndSubsections
:: String
-> String
-> [FieldDescr a]
-> [SectionDescr a]
-> [FGSectionDescr FG.PrettyFieldGrammar a]
-> a
-> Disp.Doc
ppSectionAndSubsections name arg fields sections fgSections cur
| Disp.isEmpty fieldsDoc = Disp.empty
| otherwise =
Disp.text name
<+> argDoc
$+$ (Disp.nest 2 fieldsDoc)
where
fieldsDoc = showConfig fields sections fgSections cur
argDoc
| arg == "" = Disp.empty
| otherwise = Disp.text arg
-- |
--
-- TODO: subsections
-- TODO: this should simply build 'PrettyField'
ppFgSection
:: String
-- ^ section name
-> String
-- ^ parameter
-> FG.PrettyFieldGrammar a a
-> a
-> Disp.Doc
ppFgSection secName arg grammar x
| null prettyFields = Disp.empty
| otherwise =
Disp.text secName
<+> argDoc
$+$ (Disp.nest 2 fieldsDoc)
where
prettyFields = FG.prettyFieldGrammar cabalSpecLatest grammar x
argDoc
| arg == "" = Disp.empty
| otherwise = Disp.text arg
fieldsDoc =
Disp.vcat
[ Disp.text fname' <<>> Disp.colon <<>> doc
| F.PrettyField _ fname doc <- prettyFields -- TODO: this skips sections
, let fname' = fromUTF8BS fname
]
-----------------------------------------------
-- Top level config file parsing and printing
--
-- | Parse a string in the config file syntax into a value, based on a
-- description of the configuration file in terms of its fields and sections.
--
-- It accumulates the result on top of a given initial (typically empty) value.
parseConfig
:: [FieldDescr a]
-> [SectionDescr a]
-> [FGSectionDescr FG.ParsecFieldGrammar a]
-> a
-> BS.ByteString
-> ParseResult a
parseConfig fieldDescrs sectionDescrs fgSectionDescrs empty str =
parseFieldsAndSections fieldDescrs sectionDescrs fgSectionDescrs empty
=<< readFields str
-- | Render a value in the config file syntax, based on a description of the
-- configuration file in terms of its fields and sections.
showConfig :: [FieldDescr a] -> [SectionDescr a] -> [FGSectionDescr FG.PrettyFieldGrammar a] -> a -> Disp.Doc
showConfig = ppFieldsAndSections