| 1 |
{-|
|
| 2 |
Module : Data.Ini.Config.Bidir
|
| 3 |
Copyright : (c) Getty Ritter, 2017
|
| 4 |
License : BSD
|
| 5 |
Maintainer : Getty Ritter <config-ini@infinitenegativeutility.com>
|
| 6 |
Stability : experimental
|
| 7 |
|
| 8 |
This module presents an alternate API for parsing INI files. Unlike
|
| 9 |
the standard API, it is bidirectional: the same declarative structure
|
| 10 |
can be used to parse an INI file to a value, serialize an INI file
|
| 11 |
from a value, or even /update/ an INI file by comparing it against a
|
| 12 |
value and serializing in a way that minimizes the differences between
|
| 13 |
revisions of the file.
|
| 14 |
|
| 15 |
This API does make some extra assumptions about your configuration
|
| 16 |
type and the way you interact with it: in particular, it assumes that
|
| 17 |
you have lenses for all the fields you're parsing and that you have
|
| 18 |
some kind of sensible default value of that configuration
|
| 19 |
type. Instead of providing combinators which can extract and parse a
|
| 20 |
field of an INI file into a value, the bidirectional API allows you to
|
| 21 |
declaratively associate a lens into your structure with a field of the
|
| 22 |
INI file.
|
| 23 |
|
| 24 |
Consider the following example INI file:
|
| 25 |
|
| 26 |
> [NETWORK]
|
| 27 |
> host = example.com
|
| 28 |
> port = 7878
|
| 29 |
>
|
| 30 |
> [LOCAL]
|
| 31 |
> user = terry
|
| 32 |
|
| 33 |
We'd like to parse this INI file into a @Config@ type which we've
|
| 34 |
defined like this, using
|
| 35 |
<https://hackage.haskell.org/package/lens lens> or a similar library
|
| 36 |
to provide lenses:
|
| 37 |
|
| 38 |
> data Config = Config
|
| 39 |
> { _cfHost :: String
|
| 40 |
> , _cfPort :: Int
|
| 41 |
> , _cfUser :: Maybe Text
|
| 42 |
> } deriving (Eq, Show)
|
| 43 |
>
|
| 44 |
> ''makeLenses Config
|
| 45 |
|
| 46 |
We can now define a basic specification of the type @'IniSpec' Config
|
| 47 |
()@ by using the provided operations to declare our top-level
|
| 48 |
sections, and then within those sections we can associate fields with
|
| 49 |
@Config@ lenses.
|
| 50 |
|
| 51 |
@
|
| 52 |
'configSpec' :: 'IniSpec' Config ()
|
| 53 |
'configSpec' = do
|
| 54 |
'section' \"NETWORK\" $ do
|
| 55 |
cfHost '.=' 'field' \"host\" 'string'
|
| 56 |
cfPost '.=' 'field' \"port\" 'number'
|
| 57 |
'sectionOpt' \"LOCAL\" $ do
|
| 58 |
cfUser '.=?' 'field' \"user\" 'text'
|
| 59 |
@
|
| 60 |
|
| 61 |
There are two operators used to associate lenses with fields:
|
| 62 |
|
| 63 |
['.='] Associates a lens of type @Lens' s a@ with a field description
|
| 64 |
of type @FieldDescription a@. By default, this will raise an
|
| 65 |
error when parsing if the field described is missing, but we
|
| 66 |
can mark it as optional, as we'll see.
|
| 67 |
|
| 68 |
['.=?'] Associates a lens of type @Lens' s (Maybe a)@ with a field
|
| 69 |
description of type @FieldDescription a@. During parsing, if
|
| 70 |
the value does not appear in an INI file, then the lens will
|
| 71 |
be set to 'Nothing'; similarly, during serializing, if the
|
| 72 |
value is 'Nothing', then the field will not be serialized in
|
| 73 |
the file.
|
| 74 |
|
| 75 |
Each field must include the field's name as well as a 'FieldValue',
|
| 76 |
which describes how to both parse and serialize a value of a given
|
| 77 |
type. Several built-in 'FieldValue' descriptions are provided, but you
|
| 78 |
can always build your own by providing parsing and serialization
|
| 79 |
functions for individual fields.
|
| 80 |
|
| 81 |
We can also provide extra metadata about a field, allowing it to be
|
| 82 |
skipped durin parsing, or to provide an explicit default value, or to
|
| 83 |
include an explanatory comment for that value to be used when we
|
| 84 |
serialize an INI file. These are conventionally applied to the field
|
| 85 |
using the '&' operator:
|
| 86 |
|
| 87 |
@
|
| 88 |
configSpec :: 'IniSpec' Config ()
|
| 89 |
configSpec = do
|
| 90 |
'section' \"NETWORK\" $ do
|
| 91 |
cfHost '.=' 'field' \"host\" 'string'
|
| 92 |
& 'comment' [\"The desired hostname (optional)\"]
|
| 93 |
& 'skipIfMissing'
|
| 94 |
cfPost '.=' 'field' \"port\" 'number'
|
| 95 |
& 'comment' [\"The port number\"]
|
| 96 |
'sectionOpt' \"LOCAL\" $ do
|
| 97 |
cfUser '.=?' 'field' \"user\" 'text'
|
| 98 |
@
|
| 99 |
|
| 100 |
When we want to use this specification, we need to create a value of
|
| 101 |
type 'Ini', which is an abstract representation of an INI
|
| 102 |
specification. To create an 'Ini' value, we need to use the 'ini'
|
| 103 |
function, which combines the spec with the default version of our
|
| 104 |
configuration value.
|
| 105 |
|
| 106 |
Once we have a value of type 'Ini', we can use it for three basic
|
| 107 |
operations:
|
| 108 |
|
| 109 |
* We can parse a textual INI file with 'parseIni', which will
|
| 110 |
systematically walk the spec and use the provided lens/field
|
| 111 |
associations to create a parsed configuration file. This will give
|
| 112 |
us a new value of type 'Ini' that represents the parsed
|
| 113 |
configuration, and we can extract the actual configuration value
|
| 114 |
with 'getIniValue'.
|
| 115 |
|
| 116 |
* We can update the value contained in an 'Ini' value. If the 'Ini'
|
| 117 |
value is the result of a previous call to 'parseIni', then this
|
| 118 |
update will attempt to retain as much of the incidental structure of
|
| 119 |
the parsed file as it can: for example, it will attempt to retain
|
| 120 |
comments, whitespace, and ordering. The general strategy is to make
|
| 121 |
the resulting INI file "diff-minimal": the diff between the older
|
| 122 |
INI file and the updated INI file should contain as little noise as
|
| 123 |
possible. Small cosmetic choices such as how to treat generated
|
| 124 |
comments are controlled by a configurable 'UpdatePolicy' value.
|
| 125 |
|
| 126 |
* We can serialize an 'Ini' value to a textual INI file. This will
|
| 127 |
produce the specified INI file (either a default fresh INI, or a
|
| 128 |
modified existing INI) as a textual value.
|
| 129 |
|
| 130 |
-}
|
| 131 |
|
1 | 132 |
{-# LANGUAGE CPP #-}
|
2 | 133 |
{-# LANGUAGE RankNTypes #-}
|
3 | 134 |
{-# LANGUAGE OverloadedStrings #-}
|
|
8 | 139 |
|
9 | 140 |
module Data.Ini.Config.Bidir
|
10 | 141 |
(
|
11 | |
-- $main
|
12 | |
|
13 | 142 |
-- * Parsing, Serializing, and Updating Files
|
14 | 143 |
-- $using
|
15 | 144 |
Ini
|
|
195 | 324 |
Left err -> error err
|
196 | 325 |
Right i' -> i'
|
197 | 326 |
|
| 327 |
-- | Use the provided 'UpdatePolicy' as a guide when creating future
|
| 328 |
-- updated versions of the given 'Ini' value.
|
198 | 329 |
setIniUpdatePolicy :: UpdatePolicy -> Ini s -> Ini s
|
199 | 330 |
setIniUpdatePolicy pol i = i { iniPol = pol }
|
200 | 331 |
|
|
251 | 382 |
where isOptional (Field _ fd) = fdSkipIfMissing fd
|
252 | 383 |
isOptional (FieldMb _ _) = True
|
253 | 384 |
|
| 385 |
-- | Treat an entire section as containing entirely optional fields.
|
254 | 386 |
allOptional
|
255 | 387 |
:: (SectionSpec s () -> IniSpec s ())
|
256 | 388 |
-> (SectionSpec s () -> IniSpec s ())
|
|
542 | 674 |
| otherwise =
|
543 | 675 |
mkIniValue "" descr True
|
544 | 676 |
|
545 | |
-- | An 'UpdatePolicy' describes how to
|
| 677 |
-- | An 'UpdatePolicy' guides certain choices made when an 'Ini' file
|
| 678 |
-- is updated: for example, how to add comments to the generated
|
| 679 |
-- fields, or how to treat fields which are optional.
|
546 | 680 |
data UpdatePolicy = UpdatePolicy
|
547 | 681 |
{ updateAddOptionalFields :: Bool
|
548 | 682 |
-- ^ If 'True', then optional fields not included in the INI file
|
|
569 | 703 |
|
570 | 704 |
-- | An 'UpdateCommentPolicy' describes what comments should accompany
|
571 | 705 |
-- a field added to or modified in an existing INI file when using
|
572 | |
-- 'updateIniFile'.
|
| 706 |
-- 'updateIni'.
|
573 | 707 |
data UpdateCommentPolicy
|
574 | 708 |
= CommentPolicyNone
|
575 | 709 |
-- ^ Do not add comments to new fields
|
|
577 | 711 |
-- ^ Add the same comment which appears in the 'IniSpec' value for
|
578 | 712 |
-- the field we're adding or modifying.
|
579 | 713 |
| CommentPolicyAddDefaultComment (Seq Text)
|
580 | |
-- ^ Add a consistent comment to all new fields added or modified
|
581 | |
-- by an 'updateIniFile' call.
|
| 714 |
-- ^ Add a common comment to all new fields added or modified
|
| 715 |
-- by an 'updateIni' call.
|
582 | 716 |
deriving (Eq, Show)
|
583 | 717 |
|
584 | 718 |
getComments :: FieldDescription s -> UpdateCommentPolicy -> (Seq BlankLine)
|
|
628 | 762 |
-- First, we process all the sections that actually appear in the
|
629 | 763 |
-- INI file in order
|
630 | 764 |
existingSections <- F.for sections $ \ (name, sec) -> do
|
631 | |
let err = (Left ("Unexpected top-level section: " ++ show name))
|
| 765 |
let err = Left ("Unexpected top-level section: " ++ show name)
|
632 | 766 |
Section _ spec _ <- maybe err Right
|
633 | 767 |
(F.find (\ (Section n _ _) -> n == name) fields)
|
634 | 768 |
newVals <- updateFields s (isVals sec) spec pol
|
|
794 | 928 |
Nothing -> Nothing
|
795 | 929 |
|
796 | 930 |
|
797 | |
{- $main
|
798 | |
|
799 | |
This module presents an alternate API for parsing INI files. Unlike
|
800 | |
the standard API, it is bidirectional: the same declarative structure
|
801 | |
can be used to parse an INI file to a value, serialize an INI file
|
802 | |
from a value, or even /update/ an INI file by comparing it against a
|
803 | |
value and serializing in a way that minimizes the differences between
|
804 | |
revisions of the file.
|
805 | |
|
806 | |
This API does make some extra assumptions about your configuration
|
807 | |
type and the way you interact with it: in particular, it assumes that
|
808 | |
you have lenses for all the fields you're parsing, and that you have
|
809 | |
some kind of sensible default value of that configuration
|
810 | |
type. Instead of providing combinators which can extract and parse a
|
811 | |
field of an INI file into a value, the bidirectional API allows you to
|
812 | |
declaratively associate a lens into your structure with a field of the
|
813 | |
INI file.
|
814 | |
|
815 | |
Consider the following example INI file:
|
816 | |
|
817 | |
> [NETWORK]
|
818 | |
> host = example.com
|
819 | |
> port = 7878
|
820 | |
>
|
821 | |
> [LOCAL]
|
822 | |
> user = terry
|
823 | |
|
824 | |
We'd like to parse this INI file into a @Config@ type which we've
|
825 | |
defined like this, using
|
826 | |
<https://hackage.haskell.org/package/lens lens> or a similar library
|
827 | |
to provide lenses:
|
828 | |
|
829 | |
> data Config = Config
|
830 | |
> { _cfHost :: String
|
831 | |
> , _cfPort :: Int
|
832 | |
> , _cfUser :: Maybe Text
|
833 | |
> } deriving (Eq, Show)
|
834 | |
>
|
835 | |
> ''makeLenses Config
|
836 | |
|
837 | |
We can now define a basic specification of the type @'IniSpec' Config
|
838 | |
()@ by using the provided operations to declare our top-level
|
839 | |
sections, and then within those sections associate fields with lenses
|
840 | |
into our @Config@ structure.
|
841 | |
|
842 | |
@
|
843 | |
'configSpec' :: 'IniSpec' Config ()
|
844 | |
'configSpec' = do
|
845 | |
'section' \"NETWORK\" $ do
|
846 | |
cfHost '.=' 'field' \"host\" 'string'
|
847 | |
cfPost '.=' 'field' \"port\" 'number'
|
848 | |
'sectionOpt' \"LOCAL\" $ do
|
849 | |
cfUser '.=?' 'field' \"user\" 'text'
|
850 | |
@
|
851 | |
|
852 | |
There are two operators used to associate lenses with fields:
|
853 | |
|
854 | |
['.='] Associates a lens of type @Lens' s a@ with a field description
|
855 | |
of type @FieldDescription a@
|
856 | |
|
857 | |
['.=?'] Associates a lens of type @Lens' s (Maybe a)@ with a field
|
858 | |
description of type @FieldDescription a@. If the value does
|
859 | |
not appear in an INI file, then the lens will be set to
|
860 | |
'Nothing'; similarly, if the value is 'Nothing', then the
|
861 | |
field will not be serialized in the file.
|
862 | |
|
863 | |
Each field must include the field's name as well as a 'FieldValue',
|
864 | |
which describes how to both parse and serialize a value of a given
|
865 | |
type. Several built-in 'FieldValue' descriptions are provided, but you
|
866 | |
can always build your own by providing parsing and serialization
|
867 | |
functions for individual fields.
|
868 | |
|
869 | |
We can also provide extra metadata about a field, allowing it to be
|
870 | |
skipped in parsing, or to provide an explicit default value, or to
|
871 | |
include an explanatory comment for that value to be used when we
|
872 | |
serialize an INI file. These are conventionally applied to the field
|
873 | |
using the '&' operator:
|
874 | |
|
875 | |
@
|
876 | |
configSpec :: 'IniSpec' Config ()
|
877 | |
configSpec = do
|
878 | |
'section' \"NETWORK\" $ do
|
879 | |
cfHost '.=' 'field' \"host\" 'string'
|
880 | |
& 'comment' [\"The desired hostname (optional)\"]
|
881 | |
& 'skipIfMissing'
|
882 | |
cfPost '.=' 'field' \"port\" 'number'
|
883 | |
& 'comment' [\"The port number\"]
|
884 | |
'sectionOpt' \"LOCAL\" $ do
|
885 | |
cfUser '.=?' 'field' \"user\" 'text'
|
886 | |
@
|
887 | |
|
888 | |
In order to parse an INI file, we need to provide a default value of
|
889 | |
our underlying @Config@ type on which we can perform our 'Lens'-based
|
890 | |
updates. Parsing will then walk the specification and update each
|
891 | |
field in the default value to the field provided in the INI file. We
|
892 | |
can also use a value of our @Config@ type and serialize it directly,
|
893 | |
which is useful for generating a default configuration: this will
|
894 | |
include the comments we've provided and (optionally) commented-out
|
895 | |
key-value pairs representing default values. Finally, we can /update/
|
896 | |
a configuration file, reflecting changes to a value back to an
|
897 | |
existing INI file in a way that preserves incidental structure like
|
898 | |
spacing and comments.
|
899 | |
|
900 | |
-}
|
901 | |
|
902 | 931 |
-- $using
|
903 | 932 |
-- Functions for parsing, serializing, and updating INI files.
|
904 | 933 |
|