1{-| 2Module : Data.Ini.Config.Bidir 3Copyright : (c) Getty Ritter, 2017 4License : BSD 5Maintainer : Getty Ritter <config-ini@infinitenegativeutility.com> 6Stability : experimental 7 8This module presents an alternate API for parsing INI files. Unlike 9the standard API, it is bidirectional: the same declarative structure 10can be used to parse an INI file to a value, serialize an INI file 11from a value, or even /update/ an INI file by comparing it against a 12value and serializing in a way that minimizes the differences between 13revisions of the file. 14 15This API does make some extra assumptions about your configuration 16type and the way you interact with it: in particular, it assumes that 17you have lenses for all the fields you're parsing and that you have 18some kind of sensible default value of that configuration 19type. Instead of providing combinators which can extract and parse a 20field of an INI file into a value, the bidirectional API allows you to 21declaratively associate a lens into your structure with a field of the 22INI file. 23 24Consider the following example INI file: 25 26> [NETWORK] 27> host = example.com 28> port = 7878 29> 30> [LOCAL] 31> user = terry 32 33We'd like to parse this INI file into a @Config@ type which we've 34defined like this, using 35<https://hackage.haskell.org/package/lens lens> or a similar library 36to 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 46We can now define a basic specification of the type @'IniSpec' Config 47()@ by using the provided operations to declare our top-level 48sections, 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 61There 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 75Each field must include the field's name as well as a 'FieldValue', 76which describes how to both parse and serialize a value of a given 77type. Several built-in 'FieldValue' descriptions are provided, but you 78can always build your own by providing parsing and serialization 79functions for individual fields. 80 81We can also provide extra metadata about a field, allowing it to be 82skipped durin parsing, or to provide an explicit default value, or to 83include an explanatory comment for that value to be used when we 84serialize an INI file. These are conventionally applied to the field 85using the '&' operator: 86 87@ 88configSpec :: 'IniSpec' Config () 89configSpec = do 90 'section' \"NETWORK\" $ do 91 cfHost '.=' 'field' \"host\" 'string' 92 & 'comment' [\"The desired hostname (optional)\"] 93 & 'optional' 94 cfPost '.=' 'field' \"port\" 'number' 95 & 'comment' [\"The port number\"] 96 'sectionOpt' \"LOCAL\" $ do 97 cfUser '.=?' 'field' \"user\" 'text' 98@ 99 100When we want to use this specification, we need to create a value of 101type 'Ini', which is an abstract representation of an INI 102specification. To create an 'Ini' value, we need to use the 'ini' 103function, which combines the spec with the default version of our 104configuration value. 105 106Once we have a value of type 'Ini', we can use it for three basic 107operations: 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 132{-# LANGUAGE CPP #-} 133{-# LANGUAGE RankNTypes #-} 134{-# LANGUAGE OverloadedStrings #-} 135{-# LANGUAGE ScopedTypeVariables #-} 136{-# LANGUAGE ExistentialQuantification #-} 137{-# LANGUAGE GeneralizedNewtypeDeriving #-} 138{-# LANGUAGE MultiWayIf #-} 139 140module Data.Ini.Config.Bidir 141( 142-- * Parsing, Serializing, and Updating Files 143-- $using 144 Ini 145, ini 146, getIniValue 147, iniValueL 148, getRawIni 149-- ** Parsing INI files 150, parseIni 151-- ** Serializing INI files 152, serializeIni 153-- ** Updating INI Files 154, updateIni 155, setIniUpdatePolicy 156, UpdatePolicy(..) 157, UpdateCommentPolicy(..) 158, defaultUpdatePolicy 159-- * Bidirectional Parser Types 160-- $types 161, IniSpec 162, SectionSpec 163 164-- * Section-Level Parsing 165-- $sections 166, section 167, allOptional 168 169-- * Field-Level Parsing 170-- $fields 171, FieldDescription 172, (.=) 173, (.=?) 174, field 175, flag 176, comment 177, placeholderValue 178, optional 179 180-- * FieldValues 181-- $fieldvalues 182, FieldValue(..) 183, text 184, string 185, number 186, bool 187, readable 188, listWithSeparator 189, pairWithSeparator 190 191-- * Miscellaneous Helpers 192-- $misc 193, (&) 194, Lens 195 196) where 197 198import Control.Monad.Trans.State.Strict (State, runState, modify) 199import qualified Control.Monad.Trans.State.Strict as State 200import qualified Data.Foldable as F 201#if __GLASGOW_HASKELL__ >= 710 202import Data.Function ((&)) 203#endif 204import Data.Monoid ((<>)) 205import Data.Sequence ((<|), Seq, ViewL(..), ViewR(..)) 206import qualified Data.Sequence as Seq 207import Data.Text (Text) 208import qualified Data.Text as T 209import qualified Data.Traversable as F 210import Data.Typeable (Typeable, Proxy(..), typeRep) 211import GHC.Exts (IsList(..)) 212import Text.Read (readMaybe) 213 214import Data.Ini.Config.Raw 215 216-- * Utility functions + lens stuffs 217 218-- | This is a 219-- <https://hackage.haskell.org/package/lens lens>-compatible 220-- type alias 221type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t 222 223-- These are some inline reimplementations of "lens" operators. We 224-- need the identity functor to implement 'set': 225newtype I a = I { fromI :: a } 226instance Functor I where fmap f (I x) = I (f x) 227 228set :: Lens s t a b -> b -> s -> t 229set lns x a = fromI (lns (const (I x)) a) 230 231-- ... and we need the const functor to implement 'get': 232newtype C a b = C { fromC :: a } 233instance Functor (C a) where fmap _ (C x) = C x 234 235get :: Lens s t a b -> s -> a 236get lns a = fromC (lns C a) 237 238lkp :: NormalizedText -> Seq (NormalizedText, a) -> Maybe a 239lkp t = fmap snd . F.find (\ (t', _) -> t' == t) 240 241rmv :: NormalizedText -> Seq (Field s) -> Seq (Field s) 242rmv n = Seq.filter (\ f -> fieldName f /= n) 243 244-- The & operator is really useful here, but it didn't show up in 245-- earlier versions, so it gets redefined here. 246#if __GLASGOW_HASKELL__ < 710 247{- | '&' is a reverse application operator. This provides notational 248 convenience. Its precedence is one higher than that of the 249 forward application operator '$', which allows '&' to be nested 250 in '$'. -} 251(&) :: a -> (a -> b) -> b 252a & f = f a 253infixl 1 & 254#endif 255 256-- * The 'Ini' type 257 258-- | An 'Ini' is an abstract representation of an INI file, including 259-- both its textual representation and the Haskell value it 260-- represents. 261data Ini s = Ini 262 { iniSpec :: Spec s 263 , iniCurr :: s 264 , iniDef :: s 265 , iniLast :: Maybe RawIni 266 , iniPol :: UpdatePolicy 267 } 268 269-- | Create a basic 'Ini' value from a default value and a spec. 270ini :: s -> IniSpec s () -> Ini s 271ini def (IniSpec spec) = Ini 272 { iniSpec = runBidirM spec 273 , iniCurr = def 274 , iniDef = def 275 , iniLast = Nothing 276 , iniPol = defaultUpdatePolicy 277 } 278 279-- | Get the underlying Haskell value associated with the 'Ini'. 280getIniValue :: Ini s -> s 281getIniValue = iniCurr 282 283mkLens :: (a -> b) -> (b -> a -> a) -> Lens a a b b 284mkLens get' set' f a = (`set'` a) `fmap` f (get' a) 285 286-- | The lens equivalent of 'getIniValue' 287iniValueL :: Lens (Ini s) (Ini s) s s 288iniValueL = mkLens iniCurr (\ i v -> v { iniCurr = i }) 289 290-- | Get the textual representation of an 'Ini' value. If this 'Ini' 291-- value is the result of 'parseIni', then it will attempt to retain 292-- the textual characteristics of the parsed version as much as 293-- possible (e.g. by retaining comments, ordering, and whitespace in a 294-- way that will minimize the overall diff footprint.) If the 'Ini' 295-- value was created directly from a value and a specification, then 296-- it will pretty-print an initial version of the file with the 297-- comments and placeholder text specified in the spec. 298serializeIni :: Ini s -> Text 299serializeIni = printRawIni . getRawIni 300 301-- | Get the underlying 'RawIni' value for the file. 302getRawIni :: Ini s -> RawIni 303getRawIni (Ini { iniLast = Just raw }) = raw 304getRawIni (Ini { iniCurr = s 305 , iniSpec = spec 306 }) = emitIniFile s spec 307 308-- | Parse a textual representation of an 'Ini' file. If the file is 309-- malformed or if an obligatory field is not found, this will produce 310-- a human-readable error message. If an optional field is not found, 311-- then it will fall back on the existing value contained in the 312-- provided 'Ini' structure. 313parseIni :: Text -> Ini s -> Either String (Ini s) 314parseIni t i@Ini { iniSpec = spec 315 , iniCurr = def 316 } = do 317 RawIni raw <- parseRawIni t 318 s <- parseSections def (Seq.viewl spec) raw 319 return $ i 320 { iniCurr = s 321 , iniLast = Just (RawIni raw) 322 } 323 324-- | Update the internal value of an 'Ini' file. If this 'Ini' value 325-- is the result of 'parseIni', then the resulting 'Ini' value will 326-- attempt to retain the textual characteristics of the parsed version 327-- as much as possible (e.g. by retaining comments, ordering, and 328-- whitespace in a way that will minimize the overall diff footprint.) 329updateIni :: s -> Ini s -> Ini s 330updateIni new i = 331 case doUpdateIni new i of 332 Left err -> error err 333 Right i' -> i' 334 335-- | Use the provided 'UpdatePolicy' as a guide when creating future 336-- updated versions of the given 'Ini' value. 337setIniUpdatePolicy :: UpdatePolicy -> Ini s -> Ini s 338setIniUpdatePolicy pol i = i { iniPol = pol } 339 340-- * Type definitions 341 342-- | A value of type 'FieldValue' packages up a parser and emitter 343-- function into a single value. These are used for bidirectional 344-- parsing and emitting of the value of a field. 345data FieldValue a = FieldValue 346 { fvParse :: Text -> Either String a 347 -- ^ The function to use when parsing the value of a field; if 348 -- the parser fails, then the string will be shown as an error 349 -- message to the user. 350 , fvEmit :: a -> Text 351 -- ^ The function to use when serializing a value into an INI 352 -- file. 353 } 354 355-- This is actually being used as a writer monad, but using a state 356-- monad lets us avoid the space leaks. Not that those are likely to 357-- be a problem in this application, but it's not like it cost us 358-- none. 359type BidirM s a = State (Seq s) a 360 361runBidirM :: BidirM s a -> Seq s 362runBidirM = snd . flip runState Seq.empty 363 364type Spec s = Seq (Section s) 365 366-- | An 'IniSpec' value represents the structure of an entire 367-- INI-format file in a declarative way. The @s@ parameter represents 368-- the type of a Haskell structure which is being serialized to or 369-- from. 370newtype IniSpec s a = IniSpec (BidirM (Section s) a) 371 deriving (Functor, Applicative, Monad) 372 373-- | A 'SectionSpec' value represents the structure of a single 374-- section of an INI-format file in a declarative way. The @s@ 375-- parameter represents the type of a Haskell structure which is being 376-- serialized to or from. 377newtype SectionSpec s a = SectionSpec (BidirM (Field s) a) 378 deriving (Functor, Applicative, Monad) 379 380-- * Sections 381 382-- | Define the specification of a top-level INI section. 383section :: Text -> SectionSpec s () -> IniSpec s () 384section name (SectionSpec mote) = IniSpec $ do 385 let fields = runBidirM mote 386 modify (Seq.|> Section (normalize name) fields (allFieldsOptional fields)) 387 388allFieldsOptional :: (Seq (Field s)) -> Bool 389allFieldsOptional = all isOptional 390 where isOptional (Field _ fd) = fdSkipIfMissing fd 391 isOptional (FieldMb _ _) = True 392 393-- | Treat an entire section as containing entirely optional fields. 394allOptional 395 :: (SectionSpec s () -> IniSpec s ()) 396 -> (SectionSpec s () -> IniSpec s ()) 397allOptional k spec = IniSpec $ do 398 let IniSpec comp = k spec 399 comp 400 modify (\ s -> case Seq.viewr s of 401 EmptyR -> s 402 rs :> Section name fields _ -> 403 rs Seq.|> Section name (fmap makeOptional fields) True) 404 405makeOptional :: Field s -> Field s 406makeOptional (Field l d) = Field l d { fdSkipIfMissing = True } 407makeOptional (FieldMb l d) = FieldMb l d { fdSkipIfMissing = True } 408 409data Section s = Section NormalizedText (Seq (Field s)) Bool 410 411-- * Fields 412 413-- | A "Field" is a description of 414data Field s 415 = forall a. Eq a => Field (Lens s s a a) (FieldDescription a) 416 | forall a. Eq a => FieldMb (Lens s s (Maybe a) (Maybe a)) (FieldDescription a) 417 418-- convenience accessors for things in a Field 419fieldName :: Field s -> NormalizedText 420fieldName (Field _ FieldDescription { fdName = n }) = n 421fieldName (FieldMb _ FieldDescription { fdName = n }) = n 422 423fieldComment :: Field s -> Seq Text 424fieldComment (Field _ FieldDescription { fdComment = n }) = n 425fieldComment (FieldMb _ FieldDescription { fdComment = n }) = n 426 427-- | A 'FieldDescription' is a declarative representation of the 428-- structure of a field. This includes the name of the field and the 429-- 'FieldValue' used to parse and serialize values of that field, as 430-- well as other metadata that might be needed in the course of 431-- parsing or serializing a structure. 432data FieldDescription t = FieldDescription 433 { fdName :: NormalizedText 434 , fdValue :: FieldValue t 435 , fdComment :: Seq Text 436 , fdDummy :: Maybe Text 437 , fdSkipIfMissing :: Bool 438 } 439 440-- ** Field operators 441 442{- | 443Associate a field description with a field. If this field 444is not present when parsing, it will attempt to fall back 445on a default, and if no default value is present, it will 446fail to parse. 447 448When serializing an INI file, this will produce all the 449comments associated with the field description followed 450by the value of the field in the. 451-} 452(.=) :: Eq t => Lens s s t t -> FieldDescription t -> SectionSpec s () 453l .= f = SectionSpec $ modify (Seq.|> fd) 454 where fd = Field l f 455 456{- | 457Associate a field description with a field of type "Maybe a". 458When parsing, this field will be initialized to "Nothing" if 459it is not found, and to a "Just" value if it is. When 460serializing an INI file, this will try to serialize a value 461-} 462(.=?) :: Eq t => Lens s s (Maybe t) (Maybe t) -> FieldDescription t -> SectionSpec s () 463l .=? f = SectionSpec $ modify (Seq.|> fd) 464 where fd = FieldMb l f 465 466-- ** Field metadata 467 468{- | 469Associate a multiline comment with a "FieldDescription". When 470serializing a field that has a comment associated, the comment will 471appear before the field. 472-} 473comment :: [Text] -> FieldDescription t -> FieldDescription t 474comment cmt fd = fd { fdComment = Seq.fromList cmt } 475 476-- | Choose a placeholder value to be displayed for optional fields. 477-- This is used when serializing an optional Ini field: the 478-- field will appear commented out in the output using the 479-- placeholder text as a value, so a spec that includes 480-- 481-- @ 482-- myLens .=? field "x" & placeholderValue "\<val\>" 483-- @ 484-- 485-- will serialize into an INI file that contains the line 486-- 487-- @ 488-- # x = \<val\> 489-- @ 490-- 491-- A placeholder value will only appear in the serialized output if 492-- the field is optional, but will be preferred over serializing the 493-- default value for an optional field. This will not affect INI 494-- file updates. 495placeholderValue :: Text -> FieldDescription t -> FieldDescription t 496placeholderValue t fd = fd { fdDummy = Just t } 497 498-- | If the field is not found in parsing, simply skip instead of 499-- raising an error or setting anything. 500optional :: FieldDescription t -> FieldDescription t 501optional fd = fd { fdSkipIfMissing = True } 502 503infixr 0 .= 504infixr 0 .=? 505 506-- ** Creating fields 507 508-- | Create a description of a field by a combination of the name of 509-- the field and a "FieldValue" describing how to parse and emit 510-- values associated with that field. 511field :: Text -> FieldValue a -> FieldDescription a 512field name value = FieldDescription 513 { fdName = normalize (name <> " ") 514 , fdValue = value 515 , fdComment = Seq.empty 516 , fdDummy = Nothing 517 , fdSkipIfMissing = False 518 } 519 520-- | Create a description of a 'Bool'-valued field. 521flag :: Text -> FieldDescription Bool 522flag name = field name bool 523 524-- ** FieldValues 525 526-- | A "FieldValue" for parsing and serializing values according to 527-- the logic of the "Read" and "Show" instances for that type, 528-- providing a convenient human-readable error message if the 529-- parsing step fails. 530readable :: forall a. (Show a, Read a, Typeable a) => FieldValue a 531readable = FieldValue { fvParse = parse, fvEmit = emit } 532 where emit = T.pack . show 533 parse t = case readMaybe (T.unpack t) of 534 Just v -> Right v 535 Nothing -> Left ("Unable to parse " ++ show t ++ 536 " as a value of type " ++ show typ) 537 typ = typeRep (prx) 538 prx :: Proxy a 539 prx = Proxy 540 541-- | Represents a numeric field whose value is parsed according to the 542-- 'Read' implementation for that type, and is serialized according to 543-- the 'Show' implementation for that type. 544number :: (Show a, Read a, Num a, Typeable a) => FieldValue a 545number = readable 546 547-- | Represents a field whose value is a 'Text' value 548text :: FieldValue Text 549text = FieldValue { fvParse = Right, fvEmit = id } 550 551-- | Represents a field whose value is a 'String' value 552string :: FieldValue String 553string = FieldValue { fvParse = Right . T.unpack, fvEmit = T.pack } 554 555-- | Represents a field whose value is a 'Bool' value. This parser is 556-- case-insensitive, and matches the words @true@, @false@, @yes@, and 557-- @no@, as well as single-letter abbreviations for all of the 558-- above. This will serialize as @true@ for 'True' and @false@ for 559-- 'False'. 560bool :: FieldValue Bool 561bool = FieldValue { fvParse = parse, fvEmit = emit } 562 where parse s = case T.toLower s of 563 "true" -> Right True 564 "yes" -> Right True 565 "t" -> Right True 566 "y" -> Right True 567 "false" -> Right False 568 "no" -> Right False 569 "f" -> Right False 570 "n" -> Right False 571 _ -> Left ("Unable to parse " ++ show s ++ " as a boolean") 572 emit True = "true" 573 emit False = "false" 574 575-- | Represents a field whose value is a sequence of other values 576-- which are delimited by a given string, and whose individual values 577-- are described by another 'FieldValue' value. This uses GHC's 578-- `IsList` typeclass to convert back and forth between sequence 579-- types. 580listWithSeparator :: IsList l => Text -> FieldValue (Item l) -> FieldValue l 581listWithSeparator sep fv = FieldValue 582 { fvParse = fmap fromList . mapM (fvParse fv . T.strip) . T.splitOn sep 583 , fvEmit = T.intercalate sep . map (fvEmit fv) . toList 584 } 585 586-- | Represents a field whose value is a pair of two other values 587-- separated by a given string, whose individual values are described 588-- by two different 'FieldValue' values. 589pairWithSeparator :: FieldValue l -> Text -> FieldValue r -> FieldValue (l, r) 590pairWithSeparator left sep right = FieldValue 591 { fvParse = \ t -> 592 let (leftChunk, rightChunk) = T.breakOn sep t 593 in do 594 x <- fvParse left leftChunk 595 y <- fvParse right rightChunk 596 return (x, y) 597 , fvEmit = \ (x, y) -> fvEmit left x <> sep <> fvEmit right y 598 } 599 600-- * Parsing INI files 601 602-- Are you reading this source code? It's not even that gross 603-- yet. Just you wait. This is just the regular part. 'runSpec' is 604-- easy: we walk the spec, and for each section, find the 605-- corresponding section in the INI file and call runFields. 606parseSections 607 :: s 608 -> Seq.ViewL (Section s) 609 -> Seq (NormalizedText, IniSection) 610 -> Either String s 611parseSections s Seq.EmptyL _ = Right s 612parseSections s (Section name fs opt Seq.:< rest) i 613 | Just v <- lkp name i = do 614 s' <- parseFields s (Seq.viewl fs) v 615 parseSections s' (Seq.viewl rest) i 616 | opt = parseSections s (Seq.viewl rest) i 617 | otherwise = Left ("Unable to find section " ++ 618 show (normalizedText name)) 619 620-- Now that we've got 'set', we can walk the field descriptions and 621-- find them. There's some fiddly logic, but the high-level idea is 622-- that we try to look up a field, and if it exists, parse it using 623-- the provided parser and use the provided lens to add it to the 624-- value. We have to decide what to do if it's not there, which 625-- depends on lens metadata and whether it's an optional field or not. 626parseFields :: s -> Seq.ViewL (Field s) -> IniSection -> Either String s 627parseFields s Seq.EmptyL _ = Right s 628parseFields s (Field l descr Seq.:< fs) sect 629 | Just v <- lkp (fdName descr) (isVals sect) = do 630 value <- fvParse (fdValue descr) (T.strip (vValue v)) 631 parseFields (set l value s) (Seq.viewl fs) sect 632 | fdSkipIfMissing descr = 633 parseFields s (Seq.viewl fs) sect 634 | otherwise = Left ("Unable to find field " ++ 635 show (normalizedText (fdName descr))) 636parseFields s (FieldMb l descr Seq.:< fs) sect 637 | Just v <- lkp (fdName descr) (isVals sect) = do 638 value <- fvParse (fdValue descr) (T.strip (vValue v)) 639 parseFields (set l (Just value) s) (Seq.viewl fs) sect 640 | otherwise = 641 parseFields (set l Nothing s) (Seq.viewl fs) sect 642 643-- | Serialize a value as an INI file according to a provided 644-- 'IniSpec'. 645emitIniFile :: s -> Spec s -> RawIni 646emitIniFile s spec = 647 RawIni $ 648 fmap (\ (Section name fs _) -> 649 (name, toSection s (actualText name) fs)) spec 650 651mkComments :: Seq Text -> Seq BlankLine 652mkComments comments = 653 fmap (\ ln -> CommentLine '#' (" " <> ln)) comments 654 655toSection :: s -> Text -> Seq (Field s) -> IniSection 656toSection s name fs = IniSection 657 { isName = name 658 , isVals = fmap toVal fs 659 , isStartLine = 0 660 , isEndLine = 0 661 , isComments = Seq.empty 662 } where mkIniValue val descr opt = 663 ( fdName descr 664 , IniValue 665 { vLineNo = 0 666 , vName = actualText (fdName descr) 667 , vValue = " " <> val 668 , vComments = mkComments (fdComment descr) 669 , vCommentedOut = opt 670 , vDelimiter = '=' 671 } 672 ) 673 toVal (Field l descr) 674 | Just dummy <- fdDummy descr = 675 mkIniValue dummy descr False 676 | otherwise = 677 mkIniValue (fvEmit (fdValue descr) (get l s)) descr False 678 toVal (FieldMb l descr) 679 | Just dummy <- fdDummy descr = 680 mkIniValue dummy descr True 681 | Just v <- get l s = 682 mkIniValue (fvEmit (fdValue descr) v) descr True 683 | otherwise = 684 mkIniValue "" descr True 685 686-- | An 'UpdatePolicy' guides certain choices made when an 'Ini' file 687-- is updated: for example, how to add comments to the generated 688-- fields, or how to treat fields which are optional. 689data UpdatePolicy = UpdatePolicy 690 { updateAddOptionalFields :: Bool 691 -- ^ If 'True', then optional fields not included in the INI file 692 -- will be included in the updated INI file. Defaults to 'False'. 693 , updateIgnoreExtraneousFields :: Bool 694 -- ^ If 'True', then fields in the INI file that have no 695 -- corresponding description in the 'IniSpec' will be ignored; if 696 -- 'False', then those fields will return an error value. Defaults 697 -- to 'True'. 698 , updateGeneratedCommentPolicy :: UpdateCommentPolicy 699 -- ^ The policy for what to do to comments associated with 700 -- modified fields during an update. Defaults to 701 -- 'CommentPolicyNone'. 702 } deriving (Eq, Show) 703 704-- | A set of sensible 'UpdatePolicy' defaults which keep the diffs 705-- between file versions minimal. 706defaultUpdatePolicy :: UpdatePolicy 707defaultUpdatePolicy = UpdatePolicy 708 { updateAddOptionalFields = False 709 , updateIgnoreExtraneousFields = True 710 , updateGeneratedCommentPolicy = CommentPolicyNone 711 } 712 713-- | An 'UpdateCommentPolicy' describes what comments should accompany 714-- a field added to or modified in an existing INI file when using 715-- 'updateIni'. 716data UpdateCommentPolicy 717 = CommentPolicyNone 718 -- ^ Do not add comments to new fields 719 | CommentPolicyAddFieldComment 720 -- ^ Add the same comment which appears in the 'IniSpec' value for 721 -- the field we're adding or modifying. 722 | CommentPolicyAddDefaultComment (Seq Text) 723 -- ^ Add a common comment to all new fields added or modified 724 -- by an 'updateIni' call. 725 deriving (Eq, Show) 726 727getComments :: FieldDescription s -> UpdateCommentPolicy -> (Seq BlankLine) 728getComments _ CommentPolicyNone = Seq.empty 729getComments f CommentPolicyAddFieldComment = 730 mkComments (fdComment f) 731getComments _ (CommentPolicyAddDefaultComment cs) = 732 mkComments cs 733 734-- | Given a value, an 'IniSpec', and a 'Text' form of an INI file, 735-- parse 'Text' as INI and then selectively modify the file whenever 736-- the provided value differs from the file. This is designed to help 737-- applications update a user's configuration automatically while 738-- retaining the structure and comments of a user's application, 739-- ideally in a way which produces as few changes as possible to the 740-- resulting file (so that, for example, the diff between the two 741-- should be as small as possible.) 742-- 743-- A field is considered to have "changed" if the parsed 744-- representation of the field as extracted from the textual INI file 745-- is not equal to the corresponding value in the provided 746-- structure. Changed fields will retain their place in the overall 747-- file, while newly added fields (for example, fields which have 748-- been changed from a default value) will be added to the end of the 749-- section in which they appear. 750--doUpdateIni :: s -> s -> Spec s -> RawIni -> UpdatePolicy -> Either String (Ini s) 751doUpdateIni :: s -> Ini s -> Either String (Ini s) 752doUpdateIni s i@Ini { iniSpec = spec 753 , iniDef = def 754 , iniPol = pol 755 } = do -- spec (RawIni ini) pol = do 756 let RawIni ini' = getRawIni i 757 res <- updateSections s def ini' spec pol 758 return $ i 759 { iniCurr = s 760 , iniLast = Just (RawIni res) 761 } 762 763updateSections 764 :: s 765 -> s 766 -> Seq (NormalizedText, IniSection) 767 -> Seq (Section s) 768 -> UpdatePolicy 769 -> Either String (Seq (NormalizedText, IniSection)) 770updateSections s def sections fields pol = do 771 -- First, we process all the sections that actually appear in the 772 -- INI file in order 773 existingSections <- F.for sections $ \ (name, sec) -> do 774 let err = Left ("Unexpected top-level section: " ++ show name) 775 Section _ spec _ <- maybe err Right 776 (F.find (\ (Section n _ _) -> n == name) fields) 777 newVals <- updateFields s (isVals sec) spec pol 778 return (name, sec { isVals = newVals }) 779 -- And then 780 let existingSectionNames = fmap fst existingSections 781 newSections <- F.for fields $ 782 \ (Section nm spec _) -> 783 if | nm `elem` existingSectionNames -> return mempty 784 | otherwise -> 785 let rs = emitNewFields s def spec pol 786 in if Seq.null rs 787 then return mempty 788 else return $ Seq.singleton 789 ( nm 790 , IniSection (actualText nm) rs 0 0 mempty 791 ) 792 return (existingSections <> F.asum newSections) 793 794-- We won't emit a section if everything in the section is also 795-- missing 796emitNewFields 797 :: s -> s 798 -> Seq (Field s) 799 -> UpdatePolicy -> 800 Seq (NormalizedText, IniValue) 801emitNewFields s def fields pol = go (Seq.viewl fields) where 802 go EmptyL = Seq.empty 803 go (Field l d :< fs) 804 -- If a field is not present but is also the same as the default, 805 -- then we can safely omit it 806 | get l s == get l def && not (updateAddOptionalFields pol) = 807 go (Seq.viewl fs) 808 -- otherwise, we should add it to the result 809 | otherwise = 810 let cs = getComments d (updateGeneratedCommentPolicy pol) 811 new = ( fdName d 812 , IniValue 813 { vLineNo = 0 814 , vName = actualText (fdName d) 815 , vValue = " " <> fvEmit (fdValue d) (get l s) 816 , vComments = cs 817 , vCommentedOut = False 818 , vDelimiter = '=' 819 } 820 ) 821 in new <| go (Seq.viewl fs) 822 go (FieldMb l d :< fs) = 823 case get l s of 824 Nothing -> go (Seq.viewl fs) 825 Just v -> 826 let cs = getComments d (updateGeneratedCommentPolicy pol) 827 new = ( fdName d 828 , IniValue 829 { vLineNo = 0 830 , vName = actualText (fdName d) 831 , vValue = fvEmit (fdValue d) v 832 , vComments = cs 833 , vCommentedOut = False 834 , vDelimiter = '=' 835 } 836 ) 837 in new <| go (Seq.viewl fs) 838 839 840updateFields :: s -> Seq (NormalizedText, IniValue) -> Seq (Field s) 841 -> UpdatePolicy -> Either String (Seq (NormalizedText, IniValue)) 842updateFields s values fields pol = go (Seq.viewl values) fields 843 where go ((t, val) :< vs) fs = 844 -- For each field, we need to fetch the description of the 845 -- field in the spec 846 case F.find (\ f -> fieldName f == t) fs of 847 Just f@(Field l descr) -> 848 -- if it does exist, then we need to find out whether 849 -- the field has changed at all. We can do this with the 850 -- provided lens, and check it against the INI file 851 -- we've got. There's a minor complication: there's 852 -- nothing that forces the user to provide the same INI 853 -- file we originally parsed! One side-effect means that 854 -- the parsed INI file might not actually have a valid 855 -- field according to the field parser the user 856 -- provides. In that case, we'll assume the field is 857 -- outdated, and update it with the value in the 858 -- provided structure. 859 if Right (get l s) == fvParse (fdValue descr) (T.strip (vValue val)) 860 -- if the value in the INI file parses the same as 861 -- the one in the structure we were passed, then it 862 -- doesn't need any updating, and we keep going, 863 -- removing the field from our list 864 then ((t, val) <|) `fmap` go (Seq.viewl vs) (rmv t fs) 865 -- otherwise, we've got a new updated value! Let's 866 -- synthesize a new element, using our comment policy 867 -- to comment it accordingly. (This pattern is 868 -- partial, but we should never have a situation 869 -- where it returns Nothing, because we already know 870 -- that we've matched a Field!) 871 else let Just nv = mkValue t f (vDelimiter val) 872 in ((t, nv) <|) `fmap` go (Seq.viewl vs) (rmv t fs) 873 -- And we have to replicate the logic for the FieldMb 874 -- case, because (as an existential) it doesn't really 875 -- permit us usable abstractions here. See the previous 876 -- comments for descriptions of the cases. 877 Just f@(FieldMb l descr) -> 878 let parsed = fvParse (fdValue descr) (T.strip (vValue val)) 879 in if Right (get l s) == fmap Just parsed 880 then ((t, val) <|) `fmap` go (Seq.viewl vs) (rmv t fs) 881 -- this is in the only case where the FieldMb case 882 -- differs: we might NOT have a value in the 883 -- structure. In that case, we remove the value 884 -- from the file, as well! 885 else case mkValue t f (vDelimiter val) of 886 Just nv -> ((t, nv) <|) `fmap` go (Seq.viewl vs) (rmv t fs) 887 Nothing -> go (Seq.viewl vs) (rmv t fs) 888 -- Finally, if we can't find any description of the field, 889 -- then we might skip it or throw an error, depending on 890 -- the policy the user wants. 891 Nothing 892 | updateIgnoreExtraneousFields pol -> 893 ((t, val) <|) `fmap` go (Seq.viewl vs) fs 894 | otherwise -> Left ("Unexpected field: " ++ show t) 895 -- Once we've gone through all the fields in the file, we need 896 -- to see if there's anything left over that should be in the 897 -- file. We might want to include dummy values for things that 898 -- were left out, but if we have any non-optional fields left 899 -- over, then we definitely need to include them. 900 go EmptyL fs = return (finish (Seq.viewl fs)) 901 finish (f@(Field {}) :< fs) 902 | updateAddOptionalFields pol 903 , Just val <- mkValue (fieldName f) f '=' = 904 (fieldName f, val) <| finish (Seq.viewl fs) 905 | otherwise = finish (Seq.viewl fs) 906 finish (f@(FieldMb _ descr) :< fs) 907 | not (fdSkipIfMissing descr) 908 , Just val <- mkValue (fieldName f) f '=' = 909 (fieldName f, val) <| finish (Seq.viewl fs) 910 | updateAddOptionalFields pol 911 , Just val <- mkValue (fieldName f) f '=' = 912 (fieldName f, val) <| finish (Seq.viewl fs) 913 | otherwise = finish (Seq.viewl fs) 914 -- If there's nothing left, then we can return a final value! 915 finish EmptyL = Seq.empty 916 mkValue t fld delim = 917 let comments = case updateGeneratedCommentPolicy pol of 918 CommentPolicyNone -> Seq.empty 919 CommentPolicyAddFieldComment -> 920 mkComments (fieldComment fld) 921 CommentPolicyAddDefaultComment cs -> 922 mkComments cs 923 val = IniValue 924 { vLineNo = 0 925 , vName = actualText t 926 , vValue = "" 927 , vComments = comments 928 , vCommentedOut = False 929 , vDelimiter = delim 930 } 931 in case fld of 932 Field l descr -> 933 Just (val { vValue = " " <> fvEmit (fdValue descr) (get l s) }) 934 FieldMb l descr -> 935 case get l s of 936 Just v -> Just (val { vValue = " " <> fvEmit (fdValue descr) v }) 937 Nothing -> Nothing 938 939 940-- $using 941-- Functions for parsing, serializing, and updating INI files. 942 943-- $types 944-- Types which represent declarative specifications for INI 945-- file structure. 946 947-- $sections 948-- Declaring sections of an INI file specification 949 950-- $fields 951-- Declaring individual fields of an INI file specification. 952 953-- $fieldvalues 954-- Values of type 'FieldValue' represent both a parser and a 955-- serializer for a value of a given type. It's possible to manually 956-- create 'FieldValue' descriptions, but for simple configurations, 957-- but for the sake of convenience, several commonly-needed 958-- varieties of 'FieldValue' are defined here. 959 960-- $misc 961-- These values and types are exported for compatibility. 962