|  | 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 |  | -- 'updateIni File'. | 
|  | 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 |  |