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