gdritter repos config-ini / bidir
Update haddocks Getty Ritter 7 years ago
3 changed file(s) with 230 addition(s) and 177 deletion(s). Collapse all Expand all
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
1132 {-# LANGUAGE CPP #-}
2133 {-# LANGUAGE RankNTypes #-}
3134 {-# LANGUAGE OverloadedStrings #-}
8139
9140 module Data.Ini.Config.Bidir
10141 (
11 -- $main
12
13142 -- * Parsing, Serializing, and Updating Files
14143 -- $using
15144 Ini
195324 Left err -> error err
196325 Right i' -> i'
197326
327 -- | Use the provided 'UpdatePolicy' as a guide when creating future
328 -- updated versions of the given 'Ini' value.
198329 setIniUpdatePolicy :: UpdatePolicy -> Ini s -> Ini s
199330 setIniUpdatePolicy pol i = i { iniPol = pol }
200331
251382 where isOptional (Field _ fd) = fdSkipIfMissing fd
252383 isOptional (FieldMb _ _) = True
253384
385 -- | Treat an entire section as containing entirely optional fields.
254386 allOptional
255387 :: (SectionSpec s () -> IniSpec s ())
256388 -> (SectionSpec s () -> IniSpec s ())
542674 | otherwise =
543675 mkIniValue "" descr True
544676
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.
546680 data UpdatePolicy = UpdatePolicy
547681 { updateAddOptionalFields :: Bool
548682 -- ^ If 'True', then optional fields not included in the INI file
569703
570704 -- | An 'UpdateCommentPolicy' describes what comments should accompany
571705 -- a field added to or modified in an existing INI file when using
572 -- 'updateIniFile'.
706 -- 'updateIni'.
573707 data UpdateCommentPolicy
574708 = CommentPolicyNone
575709 -- ^ Do not add comments to new fields
577711 -- ^ Add the same comment which appears in the 'IniSpec' value for
578712 -- the field we're adding or modifying.
579713 | 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.
582716 deriving (Eq, Show)
583717
584718 getComments :: FieldDescription s -> UpdateCommentPolicy -> (Seq BlankLine)
628762 -- First, we process all the sections that actually appear in the
629763 -- INI file in order
630764 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)
632766 Section _ spec _ <- maybe err Right
633767 (F.find (\ (Section n _ _) -> n == name) fields)
634768 newVals <- updateFields s (isVals sec) spec pol
794928 Nothing -> Nothing
795929
796930
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
902931 -- $using
903932 -- Functions for parsing, serializing, and updating INI files.
904933
1 {-|
2 Module : Data.Ini.Config.Raw
3 Copyright : (c) Getty Ritter, 2017
4 License : BSD
5 Maintainer : Getty Ritter <config-ini@infinitenegativeutility.com>
6 Stability : experimental
7
8 __Warning!__ This module is subject to change in the future, and therefore should
9 not be relied upon to have a consistent API.
10
11 -}
112 module Data.Ini.Config.Raw
2 ( -- $main
3
4 -- * INI types
13 ( -- * INI types
514 RawIni(..)
615 , IniSection(..)
716 , IniValue(..)
3241
3342 type Parser = Parsec (ErrorFancy Void) Text
3443
44 -- | The 'NormalizedText' type is an abstract representation of text
45 -- which has had leading and trailing whitespace removed and been
46 -- normalized to lower-case, but from which we can still extract the
47 -- original, non-normalized version. This acts like the normalized
48 -- text for the purposes of 'Eq' and 'Ord' operations, so
49 --
50 -- @
51 -- 'normalize' " x " == 'normalize' \"X\"
52 -- @
53 --
54 -- This type is used to store section and key names in the
3555 data NormalizedText = NormalizedText
3656 { actualText :: Text
3757 , normalizedText :: Text
3858 } deriving (Show)
3959
60 -- | The constructor function to build a 'NormalizedText' value. You
61 -- probably shouldn't be using this module directly, but if for some
62 -- reason you are using it, then you should be using this function to
63 -- create 'NormalizedText' values.
4064 normalize :: Text -> NormalizedText
4165 normalize t = NormalizedText t (T.toLower (T.strip t))
4266
253277 -> Seq.Seq IniValue
254278 lookupValue name section =
255279 snd <$> Seq.filter ((== normalize name) . fst) (isVals section)
256
257 {- $main
258
259 __Warning!__ This module is subject to change in the future, and therefore should
260 not be relied upon to have a consistent API.
261
262 -}
1 {-|
2 Module : Data.Ini.Config
3 Copyright : (c) Getty Ritter, 2017
4 License : BSD
5 Maintainer : Getty Ritter <config-ini@infinitenegativeutility.com>
6 Stability : experimental
7
8 The 'config-ini' library exports some simple monadic functions to
9 make parsing INI-like configuration easier. INI files have a
10 two-level structure: the top-level named chunks of configuration,
11 and the individual key-value pairs contained within those chunks.
12 For example, the following INI file has two sections, @NETWORK@
13 and @LOCAL@, and each contains its own key-value pairs. Comments,
14 which begin with @#@ or @;@, are ignored:
15 --
16 > [NETWORK]
17 > host = example.com
18 > port = 7878
19 >
20 > # here is a comment
21 > [LOCAL]
22 > user = terry
23 --
24 The combinators provided here are designed to write quick and
25 idiomatic parsers for files of this form. Sections are parsed by
26 'IniParser' computations, like 'section' and its variations,
27 while the fields within sections are parsed by 'SectionParser'
28 computations, like 'field' and its variations. If we want to
29 parse an INI file like the one above, treating the entire
30 @LOCAL@ section as optional, we can write it like this:
31 --
32 > data Config = Config
33 > { cfNetwork :: NetworkConfig, cfLocal :: Maybe LocalConfig }
34 > deriving (Eq, Show)
35 >
36 > data NetworkConfig = NetworkConfig
37 > { netHost :: String, netPort :: Int }
38 > deriving (Eq, Show)
39 >
40 > data LocalConfig = LocalConfig
41 > { localUser :: Text }
42 > deriving (Eq, Show)
43 >
44 > configParser :: IniParser Config
45 > configParser = do
46 > netCf <- section "NETWORK" $ do
47 > host <- fieldOf "host" string
48 > port <- fieldOf "port" number
49 > return NetworkConfig { netHost = host, netPort = port }
50 > locCf <- sectionMb "LOCAL" $
51 > LocalConfig <$> field "user"
52 > return Config { cfNetwork = netCf, cfLocal = locCf }
53 --
54 We can run our computation with 'parseIniFile', which,
55 when run on our example file above, would produce the
56 following:
57 --
58 >>> parseIniFile example configParser
59 Right (Config {cfNetwork = NetworkConfig {netHost = "example.com", netPort = 7878}, cfLocal = Just (LocalConfig {localUser = "terry"})})
60
61 -}
62
163 {-# LANGUAGE OverloadedStrings #-}
264 {-# LANGUAGE ScopedTypeVariables #-}
365 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
466
567 module Data.Ini.Config
668 (
7 -- $main
869 -- * Parsing Files
970 parseIniFile
1071 -- * Parser Types
376437 -- >>> :{
377438 -- let example = "[NETWORK]\nhost = example.com\nport = 7878\n\n# here is a comment\n[LOCAL]\nuser = terry\n"
378439 -- >>> :}
379
380 -- $main
381 -- The 'config-ini' library exports some simple monadic functions to
382 -- make parsing INI-like configuration easier. INI files have a
383 -- two-level structure: the top-level named chunks of configuration,
384 -- and the individual key-value pairs contained within those chunks.
385 -- For example, the following INI file has two sections, @NETWORK@
386 -- and @LOCAL@, and each contains its own key-value pairs. Comments,
387 -- which begin with @#@ or @;@, are ignored:
388 --
389 -- > [NETWORK]
390 -- > host = example.com
391 -- > port = 7878
392 -- >
393 -- > # here is a comment
394 -- > [LOCAL]
395 -- > user = terry
396 --
397 -- The combinators provided here are designed to write quick and
398 -- idiomatic parsers for files of this form. Sections are parsed by
399 -- 'IniParser' computations, like 'section' and its variations,
400 -- while the fields within sections are parsed by 'SectionParser'
401 -- computations, like 'field' and its variations. If we want to
402 -- parse an INI file like the one above, treating the entire
403 -- @LOCAL@ section as optional, we can write it like this:
404 --
405 -- > data Config = Config
406 -- > { cfNetwork :: NetworkConfig, cfLocal :: Maybe LocalConfig }
407 -- > deriving (Eq, Show)
408 -- >
409 -- > data NetworkConfig = NetworkConfig
410 -- > { netHost :: String, netPort :: Int }
411 -- > deriving (Eq, Show)
412 -- >
413 -- > data LocalConfig = LocalConfig
414 -- > { localUser :: Text }
415 -- > deriving (Eq, Show)
416 -- >
417 -- > configParser :: IniParser Config
418 -- > configParser = do
419 -- > netCf <- section "NETWORK" $ do
420 -- > host <- fieldOf "host" string
421 -- > port <- fieldOf "port" number
422 -- > return NetworkConfig { netHost = host, netPort = port }
423 -- > locCf <- sectionMb "LOCAL" $
424 -- > LocalConfig <$> field "user"
425 -- > return Config { cfNetwork = netCf, cfLocal = locCf }
426 --
427 -- We can run our computation with 'parseIniFile', which,
428 -- when run on our example file above, would produce the
429 -- following:
430 --
431 -- >>> parseIniFile example configParser
432 -- Right (Config {cfNetwork = NetworkConfig {netHost = "example.com", netPort = 7878}, cfLocal = Just (LocalConfig {localUser = "terry"})})