9 | 9 |
(
|
10 | 10 |
-- $main
|
11 | 11 |
-- * Parsing, Serializing, and Updating Files
|
| 12 |
-- $using
|
12 | 13 |
parseIniFile
|
13 | 14 |
, emitIniFile
|
14 | 15 |
, UpdatePolicy(..)
|
|
16 | 17 |
, defaultUpdatePolicy
|
17 | 18 |
, updateIniFile
|
18 | 19 |
-- * Bidirectional Parser Types
|
| 20 |
-- $types
|
19 | 21 |
, IniSpec
|
20 | 22 |
, SectionSpec
|
21 | 23 |
-- * Section-Level Parsing
|
| 24 |
-- $sections
|
22 | 25 |
, section
|
| 26 |
, sectionOpt
|
23 | 27 |
-- * Field-Level Parsing
|
| 28 |
-- $fields
|
| 29 |
, FieldDescription
|
24 | 30 |
, (.=)
|
25 | 31 |
, (.=?)
|
26 | 32 |
, field
|
|
30 | 36 |
, placeholderValue
|
31 | 37 |
, skipIfMissing
|
32 | 38 |
-- * FieldValues
|
| 39 |
-- $fieldvalues
|
33 | 40 |
, FieldValue(..)
|
34 | 41 |
, text
|
35 | 42 |
, string
|
|
37 | 44 |
, bool
|
38 | 45 |
, readable
|
39 | 46 |
, listWithSeparator
|
| 47 |
, pairWithSeparator
|
40 | 48 |
-- * Miscellaneous Helpers
|
| 49 |
-- $misc
|
41 | 50 |
, (&)
|
42 | 51 |
, Lens
|
43 | 52 |
) where
|
|
115 | 124 |
-- INI-format file in a declarative way. The @s@ parameter represents
|
116 | 125 |
-- the type of a Haskell structure which is being serialized to or
|
117 | 126 |
-- from.
|
118 | |
newtype IniSpec s a = IniSpec (BidirM (Text, Seq (Field s)) a)
|
| 127 |
newtype IniSpec s a = IniSpec (BidirM (Section s) a)
|
119 | 128 |
deriving (Functor, Applicative, Monad)
|
120 | 129 |
|
121 | 130 |
-- | A 'SectionSpec' value represents the structure of a single
|
|
125 | 134 |
newtype SectionSpec s a = SectionSpec (BidirM (Field s) a)
|
126 | 135 |
deriving (Functor, Applicative, Monad)
|
127 | 136 |
|
128 | |
-- |
|
| 137 |
-- | Define the specification of a top-level INI section.
|
129 | 138 |
section :: Text -> SectionSpec s () -> IniSpec s ()
|
130 | 139 |
section name (SectionSpec mote) = IniSpec $ do
|
131 | 140 |
let fields = runBidirM mote
|
132 | |
modify (Seq.|> (name, fields))
|
133 | |
|
| 141 |
modify (Seq.|> Section name fields False)
|
| 142 |
|
| 143 |
-- | Define the specification of an optional top-level INI section. If
|
| 144 |
-- this section does not appear in a parsed INI file, then it will be
|
| 145 |
-- skipped.
|
| 146 |
sectionOpt :: Text -> SectionSpec s () -> IniSpec s ()
|
| 147 |
sectionOpt name (SectionSpec mote) = IniSpec $ do
|
| 148 |
let fields = runBidirM mote
|
| 149 |
modify (Seq.|> Section name fields True)
|
| 150 |
|
| 151 |
data Section s = Section Text (Seq (Field s)) Bool
|
| 152 |
|
| 153 |
-- | A "Field" is a description of
|
134 | 154 |
data Field s
|
135 | 155 |
= forall a. Eq a => Field (Lens s s a a) (FieldDescription a)
|
136 | 156 |
| forall a. Eq a => FieldMb (Lens s s (Maybe a) (Maybe a)) (FieldDescription a)
|
137 | 157 |
|
| 158 |
-- | A 'FieldDescription' is a declarative representation of the
|
| 159 |
-- structure of a field. This includes the name of the field and the
|
| 160 |
-- 'FieldValue' used to parse and serialize values of that field, as
|
| 161 |
-- well as other metadata that might be needed in the course of
|
| 162 |
-- parsing or serializing a structure.
|
138 | 163 |
data FieldDescription t = FieldDescription
|
139 | 164 |
{ fdName :: Text
|
140 | 165 |
, fdValue :: FieldValue t
|
|
214 | 239 |
|
215 | 240 |
-- | Create a description of a field by a combination of the name of
|
216 | 241 |
-- the field and a "FieldValue" describing how to parse and emit
|
217 | |
-- the
|
| 242 |
-- values associated with that field.
|
218 | 243 |
field :: Text -> FieldValue a -> FieldDescription a
|
219 | 244 |
field name value = FieldDescription
|
220 | 245 |
{ fdName = name
|
|
225 | 250 |
, fdSkipIfMissing = False
|
226 | 251 |
}
|
227 | 252 |
|
| 253 |
-- | Create a description of a 'Bool'-valued field.
|
228 | 254 |
flag :: Text -> FieldDescription Bool
|
229 | 255 |
flag name = field name bool
|
230 | 256 |
|
231 | |
-- | A "FieldValue" implementation for parsing and reading
|
232 | |
-- values according to the logic of the "Read" and "Show"
|
233 | |
-- instances for that type, providing a convenient
|
234 | |
-- human-readable error message if the parsing step fails.
|
| 257 |
-- | A "FieldValue" for parsing and serializing values according to
|
| 258 |
-- the logic of the "Read" and "Show" instances for that type,
|
| 259 |
-- providing a convenient human-readable error message if the
|
| 260 |
-- parsing step fails.
|
235 | 261 |
readable :: forall a. (Show a, Read a, Typeable a) => FieldValue a
|
236 | 262 |
readable = FieldValue { fvParse = parse, fvEmit = emit }
|
237 | 263 |
where emit = T.pack . show
|
|
243 | 269 |
prx :: Proxy a
|
244 | 270 |
prx = Proxy
|
245 | 271 |
|
246 | |
-- | A "FieldValue" implementation for parsing and reading numeric
|
247 | |
-- values according to the logic of the "Read" and "Show"
|
248 | |
-- instances for that type.
|
| 272 |
-- | Represents a numeric field whose value is parsed according to the
|
| 273 |
-- 'Read' implementation for that type, and is serialized according to
|
| 274 |
-- the 'Show' implementation for that type.
|
249 | 275 |
number :: (Show a, Read a, Num a, Typeable a) => FieldValue a
|
250 | 276 |
number = readable
|
251 | 277 |
|
252 | |
-- |
|
| 278 |
-- | Represents a field whose value is a 'Text' value
|
253 | 279 |
text :: FieldValue Text
|
254 | 280 |
text = FieldValue { fvParse = Right, fvEmit = id }
|
255 | 281 |
|
| 282 |
-- | Represents a field whose value is a 'String' value
|
256 | 283 |
string :: FieldValue String
|
257 | 284 |
string = FieldValue { fvParse = Right . T.unpack, fvEmit = T.pack }
|
258 | 285 |
|
| 286 |
-- | Represents a field whose value is a 'Bool' value. This parser is
|
| 287 |
-- case-insensitive, and matches the words @true@, @false@, @yes@, and
|
| 288 |
-- @no@, as well as single-letter abbreviations for all of the
|
| 289 |
-- above. This will serialize as @true@ for 'True' and @false@ for
|
| 290 |
-- 'False'.
|
259 | 291 |
bool :: FieldValue Bool
|
260 | 292 |
bool = FieldValue { fvParse = parse, fvEmit = emit }
|
261 | 293 |
where parse s = case T.toLower s of
|
|
271 | 303 |
emit True = "true"
|
272 | 304 |
emit False = "false"
|
273 | 305 |
|
| 306 |
-- | Represents a field whose value is a sequence of other values
|
| 307 |
-- which are delimited by a given string, and whose individual values
|
| 308 |
-- are described by another 'FieldValue' value. This uses GHC's
|
| 309 |
-- `IsList` typeclass to convert back and forth between sequence
|
| 310 |
-- types.
|
274 | 311 |
listWithSeparator :: IsList l => Text -> FieldValue (Item l) -> FieldValue l
|
275 | 312 |
listWithSeparator sep fv = FieldValue
|
276 | 313 |
{ fvParse = fmap fromList . mapM (fvParse fv . T.strip) . T.splitOn sep
|
277 | 314 |
, fvEmit = T.intercalate sep . map (fvEmit fv) . toList
|
| 315 |
}
|
| 316 |
|
| 317 |
-- | Represents a field whose value is a pair of two other values
|
| 318 |
-- separated by a given string, whose individual values are described
|
| 319 |
-- by two different 'FieldValue' values.
|
| 320 |
pairWithSeparator :: FieldValue l -> Text -> FieldValue r -> FieldValue (l, r)
|
| 321 |
pairWithSeparator left sep right = FieldValue
|
| 322 |
{ fvParse = \ t ->
|
| 323 |
let (leftChunk, rightChunk) = T.breakOn sep t
|
| 324 |
in do
|
| 325 |
x <- fvParse left leftChunk
|
| 326 |
y <- fvParse right rightChunk
|
| 327 |
return (x, y)
|
| 328 |
, fvEmit = \ (x, y) -> fvEmit left x <> sep <> fvEmit right y
|
278 | 329 |
}
|
279 | 330 |
|
280 | 331 |
-- | Provided an initial value and an 'IniSpec' describing the
|
|
292 | 343 |
-- yet. Just you wait. This is just the regular part. 'runSpec' is
|
293 | 344 |
-- easy: we walk the spec, and for each section, find the
|
294 | 345 |
-- corresponding section in the INI file and call runFields.
|
295 | |
runSpec :: s -> Seq.ViewL (Text, Seq (Field s)) -> Seq (Text, IniSection)
|
| 346 |
runSpec :: s -> Seq.ViewL (Section s) -> Seq (Text, IniSection)
|
296 | 347 |
-> Either String s
|
297 | 348 |
runSpec s Seq.EmptyL _ = Right s
|
298 | |
runSpec s ((name, fs) Seq.:< rest) ini
|
| 349 |
runSpec s (Section name fs opt Seq.:< rest) ini
|
299 | 350 |
| Just v <- lkp (T.toLower name) ini = do
|
300 | 351 |
s' <- runFields s (Seq.viewl fs) v
|
301 | 352 |
runSpec s' (Seq.viewl rest) ini
|
| 353 |
| opt = runSpec s (Seq.viewl rest) ini
|
302 | 354 |
| otherwise = Left ("Unable to find section " ++ show name)
|
303 | 355 |
|
304 | 356 |
-- These are some inline reimplementations of "lens" operators. We
|
|
328 | 380 |
| Just v <- lkp (fdName descr) (isVals sect) = do
|
329 | 381 |
value <- fvParse (fdValue descr) (T.strip (vValue v))
|
330 | 382 |
runFields (set l value s) (Seq.viewl fs) sect
|
| 383 |
| fdSkipIfMissing descr =
|
| 384 |
runFields s (Seq.viewl fs) sect
|
331 | 385 |
| Just def <- fdDefault descr =
|
332 | 386 |
runFields (set l def s) (Seq.viewl fs) sect
|
333 | 387 |
| otherwise = Left ("Unable to find field " ++ show (fdName descr))
|
|
343 | 397 |
emitIniFile :: s -> IniSpec s () -> Text
|
344 | 398 |
emitIniFile s (IniSpec mote) =
|
345 | 399 |
let spec = runBidirM mote in
|
346 | |
printIni $ Ini $ fmap (\ (name, fs) -> (name, toSection s name fs)) spec
|
| 400 |
printIni $ Ini $ fmap (\ (Section name fs _) -> (name, toSection s name fs)) spec
|
347 | 401 |
|
348 | 402 |
mkComments :: Seq Text -> Seq BlankLine
|
349 | 403 |
mkComments comments =
|
|
444 | 498 |
return (printIni (Ini ini'))
|
445 | 499 |
|
446 | 500 |
updateIniSections :: s -> Seq (Text, IniSection)
|
447 | |
-> Seq (Text, Seq (Field s))
|
| 501 |
-> Seq (Section s)
|
448 | 502 |
-> UpdatePolicy
|
449 | 503 |
-> Either String (Seq (Text, IniSection))
|
450 | 504 |
updateIniSections s sections fields pol =
|
451 | 505 |
F.for sections $ \ (name, sec) -> do
|
452 | 506 |
let err = (Left ("Unexpected top-level section: " ++ show name))
|
453 | |
spec <- maybe err Right (lkp name fields)
|
| 507 |
Section _ spec _ <- maybe err Right
|
| 508 |
(F.find (\ (Section n _ _) -> n == name) fields)
|
454 | 509 |
newVals <- updateIniSection s (isVals sec) spec pol
|
455 | 510 |
return (name, sec { isVals = newVals })
|
456 | 511 |
|
|
569 | 624 |
|
570 | 625 |
|
571 | 626 |
|
572 | |
{- $main This module is an alternate API used for parsing INI files.
|
573 | |
Unlike the standard API, it is bidirectional: the same declarative
|
574 | |
structure can be also used to emit an INI file, or even to produce an
|
575 | |
updated INI file with minimal modification to the textual file
|
576 | |
provided.
|
577 | |
|
578 | |
This module makes some extra assumptions about your configuration type
|
579 | |
and the way you interact with it: in particular, it assumes that you
|
580 | |
have lenses for all the fields you're parsing, and that you have some
|
581 | |
kind of sensible default value of that configuration. Instead of
|
582 | |
providing combinators which can extract and parse a field of an INI
|
583 | |
file into a value, the bidirectional API allows you to declaratively
|
584 | |
map lenses into your structure to descriptions of corresponding fields
|
585 | |
in INI files.
|
| 627 |
{- $main
|
| 628 |
|
| 629 |
This module presents an alternate API for parsing INI files. Unlike
|
| 630 |
the standard API, it is bidirectional: the same declarative structure
|
| 631 |
can be used to parse an INI file to a value, serialize an INI file
|
| 632 |
from a value, or even /update/ an INI file by comparing it against a
|
| 633 |
value and serializing in a way that minimizes the differences between
|
| 634 |
revisions of the file.
|
| 635 |
|
| 636 |
This API does make some extra assumptions about your configuration
|
| 637 |
type and the way you interact with it: in particular, it assumes that
|
| 638 |
you have lenses for all the fields you're parsing, and that you have
|
| 639 |
some kind of sensible default value of that configuration
|
| 640 |
type. Instead of providing combinators which can extract and parse a
|
| 641 |
field of an INI file into a value, the bidirectional API allows you to
|
| 642 |
declaratively associate a lens into your structure with a field of the
|
| 643 |
INI file.
|
586 | 644 |
|
587 | 645 |
Consider the following example INI file:
|
588 | 646 |
|
|
605 | 663 |
>
|
606 | 664 |
> ''makeLenses Config
|
607 | 665 |
|
608 | |
We can now define a basic specification of the type @IniSpec Config
|
| 666 |
We can now define a basic specification of the type @'IniSpec' Config
|
609 | 667 |
()@ by using the provided operations to declare our top-level
|
610 | 668 |
sections, and then within those sections associate fields with lenses
|
611 | 669 |
into our @Config@ structure.
|
612 | 670 |
|
613 | |
> configSpec :: IniSpec Config ()
|
614 | |
> configSpec = do
|
615 | |
> section "NETWORK" $ do
|
616 | |
> cfHost .= field "host" string
|
617 | |
> cfPost .= field "port" number
|
618 | |
> section "LOCAL" $ do
|
619 | |
> cfUser .=? field "user" text
|
620 | |
|
621 | |
The '.=' operator associates a field with a lens directly, and the
|
622 | |
'.=?' operator associates a field with a lens to a 'Maybe' value,
|
623 | |
setting that value to 'Nothing' if the field does not appear in the
|
624 | |
configuration. Each 'field' invocation must include the name of the
|
625 | |
field and a representation of the type of that field: 'string',
|
626 | |
'number', and 'text' in the above snippet are all values of type
|
627 | |
'FieldValue', which bundle together a parser and serializer so that
|
628 | |
they can be used bidirectionally.
|
| 671 |
@
|
| 672 |
'configSpec' :: 'IniSpec' Config ()
|
| 673 |
'configSpec' = do
|
| 674 |
'section' \"NETWORK\" $ do
|
| 675 |
cfHost '.=' 'field' \"host\" 'string'
|
| 676 |
cfPost '.=' 'field' \"port\" 'number'
|
| 677 |
'section' \"LOCAL\" $ do
|
| 678 |
cfUser '.=?' 'field' \"user\" 'text'
|
| 679 |
@
|
| 680 |
|
| 681 |
There are two operators used to associate lenses with fields:
|
| 682 |
|
| 683 |
['.='] Associates a lens of type @Lens' s a@ with a field description
|
| 684 |
of type @FieldDescription a@
|
| 685 |
|
| 686 |
['.=?'] Associates a lens of type @Lens' s (Maybe a)@ with a field
|
| 687 |
description of type @FieldDescription a@. If the value does
|
| 688 |
not appear in an INI file, then the lens will be set to
|
| 689 |
'Nothing'; similarly, if the value is 'Nothing', then the
|
| 690 |
field will not be serialized in the file.
|
| 691 |
|
| 692 |
Each field must include the field's name as well as a 'FieldValue',
|
| 693 |
which describes how to both parse and serialize a value of a given
|
| 694 |
type. Several built-in 'FieldValue' descriptions are provided, but you
|
| 695 |
can always build your own by providing parsing and serialization
|
| 696 |
functions for individual fields.
|
629 | 697 |
|
630 | 698 |
We can also provide extra metadata about a field, allowing it to be
|
631 | 699 |
skipped in parsing, or to provide an explicit default value, or to
|
|
633 | 701 |
serialize an INI file. These are conventionally applied to the field
|
634 | 702 |
using the '&' operator:
|
635 | 703 |
|
636 | |
> configSpec :: IniSpec Config ()
|
637 | |
> configSpec = do
|
638 | |
> section "NETWORK" $ do
|
639 | |
> cfHost .= field "host" string
|
640 | |
> & comment ["The desired hostname (optional)"]
|
641 | |
> & skipIfMissing
|
642 | |
> cfPost .= field "port" number
|
643 | |
> & comment ["The port number"]
|
644 | |
> & defaultValue 9999
|
645 | |
> section "LOCAL" $ do
|
646 | |
> cfUser .=? field "user" text
|
| 704 |
@
|
| 705 |
configSpec :: 'IniSpec' Config ()
|
| 706 |
configSpec = do
|
| 707 |
'section' \"NETWORK\" $ do
|
| 708 |
cfHost '.=' 'field' \"host\" 'string'
|
| 709 |
& 'comment' [\"The desired hostname (optional)\"]
|
| 710 |
& 'skipIfMissing'
|
| 711 |
cfPost '.=' 'field' \"port\" 'number'
|
| 712 |
& 'comment' [\"The port number\"]
|
| 713 |
& 'defaultValue' 9999
|
| 714 |
'section' \"LOCAL\" $ do
|
| 715 |
cfUser '.=?' 'field' \"user\" 'text'
|
| 716 |
@
|
647 | 717 |
|
648 | 718 |
In order to parse an INI file, we need to provide a default value of
|
649 | 719 |
our underlying @Config@ type on which we can perform our 'Lens'-based
|
|
658 | 728 |
spacing and comments.
|
659 | 729 |
|
660 | 730 |
-}
|
| 731 |
|
| 732 |
-- $using
|
| 733 |
-- Functions for parsing, serializing, and updating INI files.
|
| 734 |
|
| 735 |
-- $types
|
| 736 |
-- Types which represent declarative specifications for INI
|
| 737 |
-- file structure.
|
| 738 |
|
| 739 |
-- $sections
|
| 740 |
-- Declaring sections of an INI file specification
|
| 741 |
|
| 742 |
-- $fields
|
| 743 |
-- Declaring individual fields of an INI file specification.
|
| 744 |
|
| 745 |
-- $fieldvalues
|
| 746 |
-- Values of type 'FieldValue' represent both a parser and a
|
| 747 |
-- serializer for a value of a given type. It's possible to manually
|
| 748 |
-- create 'FieldValue' descriptions, but for simple configurations,
|
| 749 |
-- but for the sake of convenience, several commonly-needed
|
| 750 |
-- varieties of 'FieldValue' are defined here.
|
| 751 |
|
| 752 |
-- $misc
|
| 753 |
-- These values and types are exported for compatibility.
|