Prototype working Bidir module, removed St as obsolete
Getty Ritter
8 years ago
| 34 | 34 | library |
| 35 | 35 | hs-source-dirs: src |
| 36 | 36 | exposed-modules: Data.Ini.Config |
| 37 | , Data.Ini.Config.Bidir | |
| 37 | 38 | , Data.Ini.Config.Raw |
| 38 | , Data.Ini.Config.St | |
| 39 | 39 | ghc-options: -Wall |
| 40 | 40 | build-depends: base >=4.7 && <4.10 |
| 41 | , containers | |
| 41 | 42 | , text >=1.2.2 && <1.3 |
| 42 | 43 | , unordered-containers >=0.2.7 && <0.3 |
| 43 | 44 | , transformers >=0.4.1 && <0.6 |
| 55 | 56 | , config-ini |
| 56 | 57 | default-language: Haskell2010 |
| 57 | 58 | |
| 58 |
executable |
|
| 59 | executable bidir-example | |
| 59 | 60 | if !flag(build-examples) |
| 60 | 61 | buildable: False |
| 61 |
hs-source-dirs: examples/ |
|
| 62 | hs-source-dirs: examples/bidir-example | |
| 62 | 63 | main-is: Main.hs |
| 63 | 64 | ghc-options: -Wall |
| 64 | 65 | build-depends: base >=4.7 && <4.10 |
| 65 | 66 | , text |
| 66 | 67 | , config-ini |
| 68 | , microlens-th | |
| 67 | 69 | default-language: Haskell2010 |
| 68 | 70 | |
| 69 |
executable |
|
| 71 | executable config-example | |
| 70 | 72 | if !flag(build-examples) |
| 71 | 73 | buildable: False |
| 72 |
hs-source-dirs: examples/ |
|
| 74 | hs-source-dirs: examples/config-example | |
| 73 | 75 | main-is: Main.hs |
| 74 | 76 | ghc-options: -Wall |
| 75 | 77 | build-depends: base >=4.7 && <4.10 |
| 80 | 82 | |
| 81 | 83 | test-suite test-ini-compat |
| 82 | 84 | type: exitcode-stdio-1.0 |
| 83 |
ghc-options: -Wall |
|
| 85 | ghc-options: -Wall -prof -fprof-auto -rtsopts | |
| 84 | 86 | default-language: Haskell2010 |
| 85 | 87 | hs-source-dirs: test/ini-compat |
| 86 | 88 | main-is: Main.hs |
| 88 | 90 | , ini |
| 89 | 91 | , config-ini |
| 90 | 92 | , QuickCheck |
| 93 | , containers | |
| 91 | 94 | , unordered-containers |
| 92 | 95 | , text |
| 93 | 96 | |
| 99 | 102 | main-is: Main.hs |
| 100 | 103 | build-depends: base |
| 101 | 104 | , config-ini |
| 105 | , containers | |
| 102 | 106 | , unordered-containers |
| 103 | 107 | , text |
| 104 | 108 | , directory |
| 1 | {-# LANGUAGE OverloadedStrings #-} | |
| 2 | {-# LANGUAGE TemplateHaskell #-} | |
| 3 | ||
| 4 | module Main where | |
| 5 | ||
| 6 | import Data.Ini.Config.Bidir | |
| 7 | import Data.Text (Text, unpack) | |
| 8 | import Lens.Micro.TH (makeLenses) | |
| 9 | ||
| 10 | data Config = Config | |
| 11 | { _confUsername :: Text | |
| 12 | , _confPort :: Int | |
| 13 | , _confUseEncryption :: Bool | |
| 14 | , _confHostname :: Text | |
| 15 | } deriving (Eq, Show) | |
| 16 | ||
| 17 | makeLenses ''Config | |
| 18 | ||
| 19 | sampleConfig :: Config | |
| 20 | sampleConfig = Config | |
| 21 | { _confUsername = "<user>" | |
| 22 | , _confPort = 8080 | |
| 23 | , _confUseEncryption = True | |
| 24 | , _confHostname = "localhost" | |
| 25 | } | |
| 26 | ||
| 27 | parseConfig :: IniParser Config () | |
| 28 | parseConfig = section "NETWORK" $ do | |
| 29 | confUsername .= fieldOf "user" text <?> | |
| 30 | [ "your username" ] | |
| 31 | confPort .= fieldOf "port" number <?> | |
| 32 | [ "the port in question" ] | |
| 33 | confUseEncryption .= flagDef "encryption" True <?> | |
| 34 | [ "whether to use encryption (defaults to true)" ] | |
| 35 | confHostname .=? field "hostname" <?> | |
| 36 | [ "hostname to connect to (optional)" ] | |
| 37 | ||
| 38 | example :: Text | |
| 39 | example = "[NETWORK]\n\ | |
| 40 | \user = gdritter\n\ | |
| 41 | \port = 8888\n" | |
| 42 | ||
| 43 | main :: IO () | |
| 44 | main = do | |
| 45 | print (parseIniFile sampleConfig parseConfig example) | |
| 46 | putStrLn (unpack (emitIniFile sampleConfig parseConfig)) |
| 1 | {-# LANGUAGE OverloadedStrings #-} | |
| 2 | {-# LANGUAGE TemplateHaskell #-} | |
| 3 | ||
| 4 | module Main where | |
| 5 | ||
| 6 | import Data.Ini.Config.St | |
| 7 | import Data.Text (Text) | |
| 8 | import Lens.Micro.Platform (makeLenses) | |
| 9 | ||
| 10 | data Config = Config | |
| 11 | { _confUsername :: Text | |
| 12 | , _confPort :: Int | |
| 13 | , _confUseEncryption :: Bool | |
| 14 | } deriving (Eq, Show) | |
| 15 | ||
| 16 | makeLenses ''Config | |
| 17 | ||
| 18 | defaultConfig :: Config | |
| 19 | defaultConfig = Config | |
| 20 | { _confUsername = "undefined" | |
| 21 | , _confPort = 0 | |
| 22 | , _confUseEncryption = True | |
| 23 | } | |
| 24 | ||
| 25 | parseConfig :: IniStParser Config () | |
| 26 | parseConfig = sectionSt "network" $ do | |
| 27 | confUsername .= field "user" | |
| 28 | confPort .= fieldOf "port" number | |
| 29 | confUseEncryption .=? fieldMbOf "encryption" flag | |
| 30 | ||
| 31 | example :: Text | |
| 32 | example = "[NETWORK]\n\ | |
| 33 | \user = gdritter\n\ | |
| 34 | \port = 8888\n" | |
| 35 | ||
| 36 | main :: IO () | |
| 37 | main = do | |
| 38 | print (parseIniFileSt example defaultConfig parseConfig) |
| 1 | {-# LANGUAGE RankNTypes #-} | |
| 2 | {-# LANGUAGE OverloadedStrings #-} | |
| 3 | {-# LANGUAGE ScopedTypeVariables #-} | |
| 4 | {-# LANGUAGE ExistentialQuantification #-} | |
| 5 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} | |
| 6 | ||
| 7 | module Data.Ini.Config.Bidir | |
| 8 | ( | |
| 9 | parseIniFile | |
| 10 | , emitIniFile | |
| 11 | -- * Bidirectional Parser Types | |
| 12 | , IniParser | |
| 13 | , SectionParser | |
| 14 | -- * Section-Level Parsing | |
| 15 | , section | |
| 16 | -- * Field-Level Parsing | |
| 17 | , (.=) | |
| 18 | , (.=?) | |
| 19 | , (<?>) | |
| 20 | , field | |
| 21 | , fieldOf | |
| 22 | , fieldDef | |
| 23 | , fieldDefOf | |
| 24 | , flag | |
| 25 | , flagDef | |
| 26 | -- * FieldValues | |
| 27 | , FieldValue(..) | |
| 28 | , text | |
| 29 | , string | |
| 30 | , number | |
| 31 | , bool | |
| 32 | , readable | |
| 33 | ) where | |
| 34 | ||
| 35 | import Control.Monad.Trans.State.Strict (State, runState, modify) | |
| 36 | import Data.Monoid ((<>)) | |
| 37 | import Data.Sequence (Seq) | |
| 38 | import qualified Data.Sequence as Seq | |
| 39 | import Data.Text (Text) | |
| 40 | import qualified Data.Text as T | |
| 41 | import Data.Typeable (Typeable, Proxy(..), typeRep) | |
| 42 | import Text.Read (readMaybe) | |
| 43 | ||
| 44 | import Data.Ini.Config.Raw | |
| 45 | ||
| 46 | -- | This is a "lens"-compatible type alias | |
| 47 | type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t | |
| 48 | ||
| 49 | lkp :: Text -> Seq (Text, a) -> Maybe a | |
| 50 | lkp t = go . Seq.viewl | |
| 51 | where go ((t', x) Seq.:< rs) | |
| 52 | | t == t' = Just x | |
| 53 | | otherwise = go (Seq.viewl rs) | |
| 54 | go Seq.EmptyL = Nothing | |
| 55 | ||
| 56 | data FieldValue a = FieldValue | |
| 57 | { fvParse :: Text -> Either String a | |
| 58 | , fvEmit :: a -> Text | |
| 59 | } | |
| 60 | ||
| 61 | data OutputOptions = OutputOptions | |
| 62 | { outputOrdering :: OutputOrdering | |
| 63 | } deriving (Eq, Show) | |
| 64 | ||
| 65 | data OutputOrdering | |
| 66 | = SameAsSpecification | |
| 67 | | SameAsInputFile | |
| 68 | deriving (Eq, Show) | |
| 69 | ||
| 70 | type BidirM s a = State (Seq s) a | |
| 71 | ||
| 72 | runBidirM :: BidirM s a -> Seq s | |
| 73 | runBidirM = snd . flip runState Seq.empty | |
| 74 | ||
| 75 | newtype IniParser s a = IniParser (BidirM (Text, Seq (Field s)) a) | |
| 76 | deriving (Functor, Applicative, Monad) | |
| 77 | ||
| 78 | newtype SectionParser s a = SectionParser (BidirM (Field s) a) | |
| 79 | deriving (Functor, Applicative, Monad) | |
| 80 | ||
| 81 | section :: Text -> SectionParser s () -> IniParser s () | |
| 82 | section name (SectionParser mote) = IniParser $ do | |
| 83 | let fields = runBidirM mote | |
| 84 | modify (Seq.|> (name, fields)) | |
| 85 | ||
| 86 | data Field s | |
| 87 | = forall a. Field (Lens s s a a) (FieldDescription a) Bool | |
| 88 | ||
| 89 | data FieldDescription t = FieldDescription | |
| 90 | { fdName :: Text | |
| 91 | , fdValue :: FieldValue t | |
| 92 | , fdDefault :: Maybe t | |
| 93 | , fdComment :: Seq Text | |
| 94 | } | |
| 95 | ||
| 96 | (.=) :: Lens s s t t -> FieldDescription t -> SectionParser s () | |
| 97 | l .= f = SectionParser $ modify (Seq.|> fd) | |
| 98 | where fd = Field l f False | |
| 99 | ||
| 100 | (.=?) :: Lens s s t t -> FieldDescription t -> SectionParser s () | |
| 101 | l .=? f = SectionParser $ modify (Seq.|> fd) | |
| 102 | where fd = Field l f True | |
| 103 | ||
| 104 | (<?>) :: FieldDescription t -> [Text] -> FieldDescription t | |
| 105 | fd <?> comment = fd { fdComment = Seq.fromList comment } | |
| 106 | ||
| 107 | infixr 8 .= | |
| 108 | infixr 8 .=? | |
| 109 | infixr 9 <?> | |
| 110 | ||
| 111 | field :: Text -> FieldDescription Text | |
| 112 | field name = fieldOf name text | |
| 113 | ||
| 114 | fieldOf :: Text -> FieldValue a -> FieldDescription a | |
| 115 | fieldOf name value = FieldDescription | |
| 116 | { fdName = name | |
| 117 | , fdValue = value | |
| 118 | , fdDefault = Nothing | |
| 119 | , fdComment = Seq.empty | |
| 120 | } | |
| 121 | ||
| 122 | fieldDef :: Text -> Text -> FieldDescription Text | |
| 123 | fieldDef name def = fieldDefOf name def text | |
| 124 | ||
| 125 | fieldDefOf :: Text -> a -> FieldValue a -> FieldDescription a | |
| 126 | fieldDefOf name def value = FieldDescription | |
| 127 | { fdName = name | |
| 128 | , fdValue = value | |
| 129 | , fdDefault = Just def | |
| 130 | , fdComment = Seq.empty | |
| 131 | } | |
| 132 | ||
| 133 | flag :: Text -> FieldDescription Bool | |
| 134 | flag name = fieldOf name bool | |
| 135 | ||
| 136 | flagDef :: Text -> Bool -> FieldDescription Bool | |
| 137 | flagDef name def = fieldDefOf name def bool | |
| 138 | ||
| 139 | sample :: SectionParser (Text, Int) () | |
| 140 | sample = do | |
| 141 | _1 .= field "foo" <?> ["comment for foo"] | |
| 142 | _2 .= fieldDefOf "bar" 0 number | |
| 143 | ||
| 144 | readable :: forall a. (Show a, Read a, Typeable a) => FieldValue a | |
| 145 | readable = FieldValue { fvParse = parse, fvEmit = emit } | |
| 146 | where emit = T.pack . show | |
| 147 | parse t = case readMaybe (T.unpack t) of | |
| 148 | Just v -> Right v | |
| 149 | Nothing -> Left ("Unable to parse " ++ show t ++ | |
| 150 | " as a value of type " ++ show typ) | |
| 151 | typ = typeRep (prx) | |
| 152 | prx :: Proxy a | |
| 153 | prx = Proxy | |
| 154 | ||
| 155 | number :: (Show a, Read a, Num a, Typeable a) => FieldValue a | |
| 156 | number = readable | |
| 157 | ||
| 158 | text :: FieldValue Text | |
| 159 | text = FieldValue { fvParse = Right, fvEmit = id } | |
| 160 | ||
| 161 | string :: FieldValue String | |
| 162 | string = FieldValue { fvParse = Right . T.unpack, fvEmit = T.pack } | |
| 163 | ||
| 164 | bool :: FieldValue Bool | |
| 165 | bool = FieldValue { fvParse = parse, fvEmit = emit } | |
| 166 | where parse s = case T.toLower s of | |
| 167 | "true" -> Right True | |
| 168 | "yes" -> Right True | |
| 169 | "t" -> Right True | |
| 170 | "y" -> Right True | |
| 171 | "false" -> Right False | |
| 172 | "no" -> Right False | |
| 173 | "f" -> Right False | |
| 174 | "n" -> Right False | |
| 175 | _ -> Left ("Unable to parse " ++ show s ++ " as a boolean") | |
| 176 | emit True = "true" | |
| 177 | emit False = "false" | |
| 178 | ||
| 179 | parseIniFile :: s -> IniParser s () -> Text -> Either String s | |
| 180 | parseIniFile def (IniParser mote) t = | |
| 181 | let spec = runBidirM mote | |
| 182 | in case parseIni t of | |
| 183 | Left err -> Left err | |
| 184 | Right (Ini ini) -> runSpec def (Seq.viewl spec) ini | |
| 185 | ||
| 186 | runSpec :: s -> Seq.ViewL (Text, Seq (Field s)) -> Seq (Text, IniSection) -> Either String s | |
| 187 | runSpec s Seq.EmptyL _ = Right s | |
| 188 | runSpec s ((name, fs) Seq.:< rest) ini | |
| 189 | | Just v <- lkp (T.toLower name) ini = do | |
| 190 | s' <- runFields s (Seq.viewl fs) v | |
| 191 | runSpec s' (Seq.viewl rest) ini | |
| 192 | | otherwise = Left ("Unable to find section " ++ show name) | |
| 193 | ||
| 194 | newtype I a = I { fromI :: a } | |
| 195 | instance Functor I where fmap f (I x) = I (f x) | |
| 196 | ||
| 197 | set :: Lens s t a b -> b -> s -> t | |
| 198 | set lns x a = fromI (lns (const (I x)) a) | |
| 199 | ||
| 200 | newtype C a b = C { fromC :: a } | |
| 201 | instance Functor (C a) where fmap _ (C x) = C x | |
| 202 | ||
| 203 | get :: Lens s t a b -> s -> a | |
| 204 | get lns a = fromC (lns C a) | |
| 205 | ||
| 206 | runFields :: s -> Seq.ViewL (Field s) -> IniSection -> Either String s | |
| 207 | runFields s Seq.EmptyL _ = Right s | |
| 208 | runFields s (Field l descr optional Seq.:< fs) sect | |
| 209 | | Just v <- lkp (fdName descr) (isVals sect) = do | |
| 210 | value <- fvParse (fdValue descr) (vValue v) | |
| 211 | runFields (set l value s) (Seq.viewl fs) sect | |
| 212 | | Just def <- fdDefault descr = | |
| 213 | runFields (set l def s) (Seq.viewl fs) sect | |
| 214 | | optional = | |
| 215 | runFields s (Seq.viewl fs) sect | |
| 216 | | otherwise = Left ("Unable to find field " ++ show (fdName descr)) | |
| 217 | ||
| 218 | emitIniFile :: s -> IniParser s () -> Text | |
| 219 | emitIniFile s (IniParser mote) = | |
| 220 | let spec = runBidirM mote in | |
| 221 | printIni $ Ini $ fmap (\ (name, fs) -> (name, toSection s name fs)) spec | |
| 222 | ||
| 223 | toSection :: s -> Text -> Seq (Field s) -> IniSection | |
| 224 | toSection s name fs = IniSection | |
| 225 | { isName = name | |
| 226 | , isVals = fmap toVal fs | |
| 227 | , isStartLine = 0 | |
| 228 | , isEndLine = 0 | |
| 229 | , isComments = Seq.empty | |
| 230 | } where toVal (Field l descr optional) = | |
| 231 | ( fdName descr | |
| 232 | , IniValue | |
| 233 | { vLineNo = 0 | |
| 234 | , vName = fdName descr | |
| 235 | , vValue = fvEmit (fdValue descr) (get l s) | |
| 236 | , vComments = BlankLine Seq.<| | |
| 237 | fmap (\ ln -> CommentLine '#' (" " <> ln)) | |
| 238 | (fdComment descr) | |
| 239 | , vCommentedOut = optional | |
| 240 | } | |
| 241 | ) | |
| 242 | ||
| 243 | -- DELETE ME LATER | |
| 244 | ||
| 245 | lens :: (s -> a) -> (b -> s -> t) -> Lens s t a b | |
| 246 | lens gt st f a = (`st` a) `fmap` f (gt a) | |
| 247 | ||
| 248 | _1 :: Lens (a, b) (a, b) a a | |
| 249 | _1 = lens fst (\ a (_, b) -> (a, b)) | |
| 250 | ||
| 251 | _2 :: Lens (a, b) (a, b) b b | |
| 252 | _2 = lens snd (\ b (a, _) -> (a, b)) | |
| 253 | ||
| 254 | ||
| 255 | -- $main | |
| 256 | -- This module is an alternate API used for parsing INI files. | |
| 257 | -- unlike the standard API, it is bidirectional: it can be | |
| 258 | -- used to emit an INI or even produce a modified INI file | |
| 259 | -- with minimal modification. | |
| 260 | -- | |
| 261 | -- This module is designed to be used with lenses: instead of | |
| 262 | -- generating a new value as a result of parsing, we start | |
| 263 | -- with a fully constructed value and then associate each field | |
| 264 | -- of the INI file with a lens into that structure. Among other | |
| 265 | -- things, this introduces more flexibility in terms of how we | |
| 266 | -- organize both the configuration file and the data type that | |
| 267 | -- represents the configuration. Consider the same example code | |
| 268 | -- that appears in the documentation for the "Data.Ini.Config" | |
| 269 | -- module, which parses a configuration file like this: | |
| 270 | -- | |
| 271 | -- > [NETWORK] | |
| 272 | -- > host = example.com | |
| 273 | -- > port = 7878 | |
| 274 | -- > | |
| 275 | -- > [LOCAL] | |
| 276 | -- > user = terry | |
| 277 | -- | |
| 278 | -- In that example, we split the configuration into a @NetworkConfig@ | |
| 279 | -- and a @LocalConfig@ type to mirror the configuration file's use of | |
| 280 | -- @[LOCAL]@ and @[NETWORK]@ sections, but we might want to keep the | |
| 281 | -- configuration data type as a single flat record, in which case our | |
| 282 | -- parsing code becomes more awkward: | |
| 283 | -- | |
| 284 | -- > data Config = Config | |
| 285 | -- > { _cfHost :: String | |
| 286 | -- > , _cfPort :: Int | |
| 287 | -- > , _cfUser :: Text | |
| 288 | -- > } deriving (Eq, Show) | |
| 289 | -- > | |
| 290 | -- > -- this is not ideal | |
| 291 | -- > configParser :: IniParser Config | |
| 292 | -- > configParser = do | |
| 293 | -- > (host, port) <- section "NETWORK" $ do | |
| 294 | -- > host <- fieldOf "host" string | |
| 295 | -- > port <- fieldOf "port" number | |
| 296 | -- > return (host, port) | |
| 297 | -- > user <- section "LOCAL" $ field "user" | |
| 298 | -- > return (Config host port user) | |
| 299 | -- | |
| 300 | -- We could also use repeated invocations of 'section', but this | |
| 301 | -- also makes our parsing code a lot uglier and involves unnecessary | |
| 302 | -- repetition of the @\"NETWORK\"@ literal: | |
| 303 | -- | |
| 304 | -- > -- this is kind of ugly | |
| 305 | -- > configParser :: IniParser Config | |
| 306 | -- > configParser = do | |
| 307 | -- > host <- section "NETWORK" $ fieldOf "host" string | |
| 308 | -- > port <- section "NETWORK" $ fieldOf "port" number | |
| 309 | -- > user <- section "LOCAL" $ field "user" | |
| 310 | -- > return (Config host port user) | |
| 311 | -- | |
| 312 | -- Assuming that we generate lenses for the @Config@ type above, | |
| 313 | -- then we can use the lens-based combinators in this module to | |
| 314 | -- write terser parsing code by providing which lens to update | |
| 315 | -- along with each field: | |
| 316 | -- | |
| 317 | -- > configSpec :: IniSpec Config () | |
| 318 | -- > configSpec = do | |
| 319 | -- > sectionSt "NETWORK" $ do | |
| 320 | -- > cfHost .= field "host" string | |
| 321 | -- > cfPort .= fieldOf "port" number | |
| 322 | -- > sectionSt "LOCAL" $ do | |
| 323 | -- > cfUser .= field "user" | |
| 324 | -- | |
| 325 | -- Additionally, given a value of type @Config@, we can use the | |
| 326 | -- same specification to emit an INI file, which is useful for | |
| 327 | -- generating a valid sample configuration. To help with this, | |
| 328 | -- we can rewrite the spec to make use of the @<?>@ operator, | |
| 329 | -- which associates the field with a comment block that will | |
| 330 | -- be printed above the field in the generated INI file. | |
| 331 | -- | |
| 332 | -- > configSpec :: IniSpec Config () | |
| 333 | -- > configSpec = do | |
| 334 | -- > sectionSt "NETWORK" $ do | |
| 335 | -- > cfHost .= field "host" string <?> [ "the host" ] | |
| 336 | -- > cfPort .= fieldOf "port" number <?> [ "the port" ] | |
| 337 | -- > sectionSt "LOCAL" $ do | |
| 338 | -- > cfUser .= field "user" <?> [ "the username" ] | |
| 339 | -- | |
| 340 | -- Using an existing @Config@ value here, we can produce a sample | |
| 341 | -- configuration: |
| 2 | 2 | ( Ini(..) |
| 3 | 3 | , IniSection(..) |
| 4 | 4 | , IniValue(..) |
| 5 | , BlankLine(..) | |
| 5 | 6 | , parseIni |
| 7 | , printIni | |
| 6 | 8 | ) where |
| 7 | 9 | |
| 8 | 10 | import Control.Monad (void) |
| 9 | import Data.HashMap.Strict (HashMap) | |
| 10 | import qualified Data.HashMap.Strict as HM | |
| 11 | import qualified Data.Foldable as F | |
| 12 | import Data.Monoid ((<>)) | |
| 13 | import Data.Sequence (Seq) | |
| 14 | import qualified Data.Sequence as Seq | |
| 11 | 15 | import Data.Text (Text) |
| 12 | 16 | import qualified Data.Text as T |
| 17 | import qualified Data.Text.Lazy as LazyText | |
| 18 | import qualified Data.Text.Lazy.Builder as Builder | |
| 13 | 19 | import Text.Megaparsec |
| 14 | 20 | import Text.Megaparsec.Text |
| 15 | 21 | |
| 16 | 22 | -- | An 'Ini' value is a mapping from section names to |
| 17 | 23 | -- 'IniSection' values. |
| 18 | newtype Ini | |
| 19 | = Ini { fromIni :: HashMap Text IniSection } | |
| 20 | deriving (Eq, Show) | |
| 24 | newtype Ini = Ini | |
| 25 | { fromIni :: Seq (Text, IniSection) | |
| 26 | } deriving (Eq, Show) | |
| 21 | 27 | |
| 22 | 28 | -- | An 'IniSection' consists of a name, a mapping of key-value pairs, |
| 23 | 29 | -- and metadata about where the section starts and ends in the file. |
| 24 | 30 | data IniSection = IniSection |
| 25 | 31 | { isName :: Text |
| 26 |
, isVals :: |
|
| 32 | , isVals :: Seq (Text, IniValue) | |
| 27 | 33 | , isStartLine :: Int |
| 28 | 34 | , isEndLine :: Int |
| 35 | , isComments :: Seq BlankLine | |
| 29 | 36 | } deriving (Eq, Show) |
| 30 | 37 | |
| 31 | 38 | -- | An 'IniValue' represents a key-value mapping, and also stores the |
| 32 | 39 | -- line number where it appears. |
| 33 | 40 | data IniValue = IniValue |
| 34 | { vLineNo :: Int | |
| 35 | , vName :: Text | |
| 36 |
|
|
| 41 | { vLineNo :: Int | |
| 42 | , vName :: Text | |
| 43 | , vValue :: Text | |
| 44 | , vComments :: Seq BlankLine | |
| 45 | , vCommentedOut :: Bool | |
| 46 | -- ^ Right now, this will never show up in a parsed INI file, but | |
| 47 | -- it's used when emitting a default INI file: it causes the | |
| 48 | -- key-value line to include a leading comment as well. | |
| 37 | 49 | } deriving (Eq, Show) |
| 50 | ||
| 51 | -- | We want to keep track of the whitespace/comments in between KV | |
| 52 | -- lines, so this allows us to track those lines in a reproducible | |
| 53 | -- way. | |
| 54 | data BlankLine | |
| 55 | = CommentLine Char Text | |
| 56 | | BlankLine | |
| 57 | deriving (Eq, Show) | |
| 38 | 58 | |
| 39 | 59 | -- | Parse a 'Text' value into an 'Ini' value. |
| 40 | 60 | parseIni :: Text -> Either String Ini |
| 44 | 64 | |
| 45 | 65 | pIni :: Parser Ini |
| 46 | 66 | pIni = do |
| 47 | sBlanks | |
| 48 | vs <- many (pSection <?> "section") | |
| 49 | void eof | |
| 50 | return $ Ini $ HM.fromList [ (T.toLower (isName v), v) | |
| 51 | | v <- vs | |
| 52 | ] | |
| 67 | leading <- sBlanks | |
| 68 | pSections leading Seq.empty | |
| 53 | 69 | |
| 54 | sBlanks :: Parser () | |
| 55 | sBlanks = skipMany (void eol <|> sComment) | |
| 70 | sBlanks :: Parser (Seq BlankLine) | |
| 71 | sBlanks = Seq.fromList <$> many ((BlankLine <$ void eol) <|> sComment) | |
| 56 | 72 | |
| 57 |
sComment :: Parser |
|
| 73 | sComment :: Parser BlankLine | |
| 58 | 74 | sComment = do |
| 59 | void (oneOf ";#") | |
| 60 | void (manyTill anyChar eol) | |
| 75 | c <- oneOf ";#" | |
| 76 | txt <- T.pack `fmap` manyTill anyChar eol | |
| 77 | return (CommentLine c txt) | |
| 61 | 78 | |
| 62 | pSection :: Parser IniSection | |
| 63 | pSection = do | |
| 79 | pSections :: Seq BlankLine -> Seq (Text, IniSection) -> Parser Ini | |
| 80 | pSections leading prevs = | |
| 81 | pSection leading prevs <|> (Ini prevs <$ void eof) | |
| 82 | ||
| 83 | pSection :: Seq BlankLine -> Seq (Text, IniSection) -> Parser Ini | |
| 84 | pSection leading prevs = do | |
| 64 | 85 | start <- getCurrentLine |
| 65 | 86 | void (char '[') |
| 66 | 87 | name <- T.pack `fmap` some (noneOf "[]") |
| 67 | 88 | void (char ']') |
| 68 | sBlanks | |
| 69 | vals <- many (pPair <?> "key-value pair") | |
| 70 | end <- getCurrentLine | |
| 71 | sBlanks | |
| 72 | return IniSection | |
| 73 | { isName = T.strip name | |
| 74 | , isVals = HM.fromList [ (vName v, v) | v <- vals ] | |
| 75 | , isStartLine = start | |
| 76 | , isEndLine = end | |
| 77 | } | |
| 89 | comments <- sBlanks | |
| 90 | pPairs (T.strip name) start leading prevs comments Seq.empty | |
| 78 | 91 | |
| 79 | pPair :: Parser IniValue | |
| 80 | pPair = do | |
| 92 | pPairs :: Text | |
| 93 | -> Int | |
| 94 | -> Seq BlankLine | |
| 95 | -> Seq (Text, IniSection) | |
| 96 | -> Seq BlankLine | |
| 97 | -> Seq (Text, IniValue) | |
| 98 | -> Parser Ini | |
| 99 | pPairs name start leading prevs comments pairs = newPair <|> finishedSection | |
| 100 | where | |
| 101 | newPair = do | |
| 102 | pair <- pPair comments | |
| 103 | rs <- sBlanks | |
| 104 | pPairs name start leading prevs rs (pairs Seq.|> (vName pair, pair)) | |
| 105 | finishedSection = do | |
| 106 | end <- getCurrentLine | |
| 107 | let newSection = IniSection | |
| 108 | { isName = name | |
| 109 | , isVals = pairs | |
| 110 | , isStartLine = start | |
| 111 | , isEndLine = end | |
| 112 | , isComments = leading | |
| 113 | } | |
| 114 | pSections comments (prevs Seq.|> (T.toLower name, newSection)) | |
| 115 | ||
| 116 | pPair :: Seq BlankLine -> Parser IniValue | |
| 117 | pPair leading = do | |
| 81 | 118 | pos <- getCurrentLine |
| 82 | 119 | key <- T.pack `fmap` some (noneOf "[]=:") |
| 83 | 120 | void (oneOf ":=") |
| 84 | 121 | val <- T.pack `fmap` manyTill anyChar eol |
| 85 | sBlanks | |
| 86 | 122 | return IniValue |
| 87 | { vLineNo = pos | |
| 88 | , vName = T.strip key | |
| 89 |
|
|
| 123 | { vLineNo = pos | |
| 124 | , vName = T.strip key | |
| 125 | , vValue = T.strip val | |
| 126 | , vComments = leading | |
| 127 | , vCommentedOut = False | |
| 90 | 128 | } |
| 91 | 129 | |
| 92 | 130 | getCurrentLine :: Parser Int |
| 93 | 131 | getCurrentLine = (fromIntegral . unPos . sourceLine) `fmap` getPosition |
| 132 | ||
| 133 | ||
| 134 | printIni :: Ini -> Text | |
| 135 | printIni = LazyText.toStrict . Builder.toLazyText . F.foldMap build . fromIni | |
| 136 | where | |
| 137 | build (_, ini) = | |
| 138 | F.foldMap buildComment (isComments ini) <> | |
| 139 | Builder.singleton '[' <> | |
| 140 | Builder.fromText (isName ini) <> | |
| 141 | Builder.fromString "]\n" <> | |
| 142 | F.foldMap buildKV (isVals ini) | |
| 143 | buildComment BlankLine = Builder.singleton '\n' | |
| 144 | buildComment (CommentLine c txt) = | |
| 145 | Builder.singleton c <> Builder.fromText txt <> Builder.singleton '\n' | |
| 146 | buildKV (_, val) = | |
| 147 | F.foldMap buildComment (vComments val) <> | |
| 148 | (if vCommentedOut val then Builder.fromString "# " else mempty) <> | |
| 149 | Builder.fromText (vName val) <> | |
| 150 | Builder.fromString " = " <> | |
| 151 | Builder.fromText (vValue val) <> | |
| 152 | Builder.singleton '\n' | |
| 32 | 32 | |
| 33 | 33 | import Control.Applicative (Applicative(..), Alternative(..)) |
| 34 | 34 | import Control.Monad.Trans.Except |
| 35 | import qualified Data.HashMap.Strict as HM | |
| 36 | 35 | import Data.Ini.Config.Raw |
| 36 | import Data.Sequence (Seq) | |
| 37 | import qualified Data.Sequence as Seq | |
| 37 | 38 | import Data.String (IsString(..)) |
| 38 | 39 | import Data.Text (Text) |
| 39 | 40 | import qualified Data.Text as T |
| 40 | 41 | import Data.Typeable (Typeable, Proxy(..), typeRep) |
| 41 | 42 | import Text.Read (readMaybe) |
| 43 | ||
| 44 | lkp :: Text -> Seq (Text, a) -> Maybe a | |
| 45 | lkp t = go . Seq.viewl | |
| 46 | where go ((t', x) Seq.:< rs) | |
| 47 | | t == t' = Just x | |
| 48 | | otherwise = go (Seq.viewl rs) | |
| 49 | go Seq.EmptyL = Nothing | |
| 42 | 50 | |
| 43 | 51 | addLineInformation :: Int -> Text -> StParser s a -> StParser s a |
| 44 | 52 | addLineInformation lineNo sec = withExceptT go |
| 75 | 83 | -- Left "No top-level section named \"TWO\"" |
| 76 | 84 | section :: Text -> SectionParser a -> IniParser a |
| 77 | 85 | section name (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) -> |
| 78 |
case |
|
| 86 | case lkp (T.toLower name) ini of | |
| 79 | 87 | Nothing -> Left ("No top-level section named " ++ show name) |
| 80 | 88 | Just sec -> runExceptT thunk sec |
| 81 | 89 | |
| 91 | 99 | -- Right Nothing |
| 92 | 100 | sectionMb :: Text -> SectionParser a -> IniParser (Maybe a) |
| 93 | 101 | sectionMb name (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) -> |
| 94 |
case |
|
| 102 | case lkp (T.toLower name) ini of | |
| 95 | 103 | Nothing -> return Nothing |
| 96 | 104 | Just sec -> Just `fmap` runExceptT thunk sec |
| 97 | 105 | |
| 107 | 115 | -- Right "def" |
| 108 | 116 | sectionDef :: Text -> a -> SectionParser a -> IniParser a |
| 109 | 117 | sectionDef name def (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) -> |
| 110 |
case |
|
| 118 | case lkp (T.toLower name) ini of | |
| 111 | 119 | Nothing -> return def |
| 112 | 120 | Just sec -> runExceptT thunk sec |
| 113 | 121 | |
| 121 | 129 | |
| 122 | 130 | rawFieldMb :: Text -> StParser IniSection (Maybe IniValue) |
| 123 | 131 | rawFieldMb name = ExceptT $ \m -> |
| 124 |
return ( |
|
| 132 | return (lkp name (isVals m)) | |
| 125 | 133 | |
| 126 | 134 | rawField :: Text -> StParser IniSection IniValue |
| 127 | 135 | rawField name = do |
| 196 | 204 | -- Right "def" |
| 197 | 205 | fieldDef :: Text -> Text -> SectionParser Text |
| 198 | 206 | fieldDef name def = SectionParser $ ExceptT $ \m -> |
| 199 |
case |
|
| 207 | case lkp name (isVals m) of | |
| 200 | 208 | Nothing -> return def |
| 201 | 209 | Just x -> return (vValue x) |
| 202 | 210 | |
| 1 | 1 | module Main where |
| 2 | 2 | |
| 3 | 3 | import Data.Char |
| 4 | import qualified Data.Foldable as Fold | |
| 4 | 5 | import Data.HashMap.Strict (HashMap) |
| 5 | 6 | import qualified Data.HashMap.Strict as HM |
| 6 | 7 | import qualified Data.Ini as I1 |
| 7 | 8 | import qualified Data.Ini.Config.Raw as I2 |
| 9 | import Data.List (nub) | |
| 10 | import qualified Data.Sequence as Seq | |
| 8 | 11 | import Data.Text (Text) |
| 9 | 12 | import qualified Data.Text as T |
| 10 | 13 | |
| 11 | 14 | import Test.QuickCheck |
| 15 | ||
| 16 | myArgs :: Args | |
| 17 | myArgs = stdArgs | |
| 12 | 18 | |
| 13 | 19 | iniEquiv :: ArbIni -> Bool |
| 14 | 20 | iniEquiv (ArbIni raw) = case (i1, i2) of |
| 21 | 27 | i2 = I2.parseIni pr |
| 22 | 28 | i1 = I1.parseIni pr |
| 23 | 29 | |
| 30 | revIniEquiv :: RichIni -> Bool | |
| 31 | revIniEquiv (RichIni raw) = case (i1, i2) of | |
| 32 | (Right i1', Right i2') -> | |
| 33 | let i1'' = lower i1' | |
| 34 | i2'' = toMaps i2' | |
| 35 | in i1'' == i2'' | |
| 36 | _ -> False | |
| 37 | where pr = I2.printIni raw | |
| 38 | i1 = I1.parseIni pr | |
| 39 | i2 = I2.parseIni pr | |
| 40 | ||
| 24 | 41 | lower :: I1.Ini -> HashMap Text (HashMap Text Text) |
| 25 | 42 | lower (I1.Ini hm) = |
| 26 | 43 | HM.fromList [ (T.toLower k, v) | (k, v) <- HM.toList hm ] |
| 27 | 44 | |
| 28 | 45 | toMaps :: I2.Ini -> HashMap Text (HashMap Text Text) |
| 29 |
toMaps (I2.Ini m) = |
|
| 46 | toMaps (I2.Ini m) = conv (fmap sectionToPair m) | |
| 47 | where sectionToPair (name, section) = ( name | |
| 48 | , conv (fmap valueToPair (I2.isVals section)) | |
| 49 | ) | |
| 50 | valueToPair (name, value) = (name, I2.vValue value) | |
| 51 | conv = HM.fromList . Fold.toList | |
| 52 | ||
| 53 | newtype ArbText = ArbText { fromArbText :: Text } deriving (Show) | |
| 54 | ||
| 55 | instance Arbitrary ArbText where | |
| 56 | arbitrary = (ArbText . T.pack) `fmap` listOf1 (arbitrary `suchThat` isValid) | |
| 57 | where isValid ':' = False | |
| 58 | isValid '=' = False | |
| 59 | isValid '#' = False | |
| 60 | isValid ';' = False | |
| 61 | isValid '[' = False | |
| 62 | isValid ']' = False | |
| 63 | isValid c | |
| 64 | | isSpace c = False | |
| 65 | | otherwise = True | |
| 30 | 66 | |
| 31 | 67 | newtype ArbIni = ArbIni I1.Ini deriving (Show) |
| 32 | 68 | |
| 36 | 72 | name <- str |
| 37 | 73 | sec <- section |
| 38 | 74 | return (name, sec) |
| 39 | str = (T.pack `fmap` arbitrary) `suchThat` (\ t -> | |
| 40 | T.all (\ c -> isAlphaNum c || c == ' ') | |
| 41 |
|
|
| 75 | str = fromArbText `fmap` arbitrary | |
| 42 | 76 | section = HM.fromList `fmap` listOf kv |
| 43 | 77 | kv = do |
| 44 | 78 | name <- str |
| 45 | 79 | val <- str |
| 46 | 80 | return (name, val) |
| 47 | 81 | |
| 82 | newtype RichIni = RichIni (I2.Ini) deriving (Show) | |
| 83 | ||
| 84 | instance Arbitrary RichIni where | |
| 85 | arbitrary = (RichIni . I2.Ini . Seq.fromList . nub) `fmap` listOf sections | |
| 86 | where sections = do | |
| 87 | name <- (T.toLower . T.strip) `fmap` str | |
| 88 | sec <- section name | |
| 89 | return (name, sec) | |
| 90 | str = fromArbText `fmap` arbitrary | |
| 91 | section n = do | |
| 92 | vals <- listOf kv | |
| 93 | cs <- Seq.fromList `fmap` listOf comment | |
| 94 | return (I2.IniSection n (Seq.fromList $ nub vals) 0 0 cs) | |
| 95 | kv = do | |
| 96 | name <- T.strip `fmap` str | |
| 97 | val <- str | |
| 98 | cs <- Seq.fromList `fmap` listOf comment | |
| 99 | return (name, I2.IniValue 0 name val cs False) | |
| 100 | comment = oneof [ return I2.BlankLine | |
| 101 | , do { c <- elements ";#" | |
| 102 | ; txt <- str | |
| 103 | ; return (I2.CommentLine c txt) | |
| 104 | } | |
| 105 | ] | |
| 106 | ||
| 48 | 107 | main :: IO () |
| 49 |
main = |
|
| 108 | main = do | |
| 109 | quickCheckWith myArgs revIniEquiv | |
| 110 | quickCheckWith myArgs iniEquiv | |
| 2 | 2 | |
| 3 | 3 | import Data.List |
| 4 | 4 | import Data.Ini.Config.Raw |
| 5 |
import Data. |
|
| 5 | import Data.Sequence (Seq) | |
| 6 | 6 | import Data.Text (Text) |
| 7 | 7 | import qualified Data.Text.IO as T |
| 8 | 8 | import System.Directory |
| 19 | 19 | ] |
| 20 | 20 | mapM_ runTest inis |
| 21 | 21 | |
| 22 | toMaps :: Ini -> HashMap Text (HashMap Text Text) | |
| 23 | toMaps (Ini m) = fmap (fmap vValue . isVals) m | |
| 22 | toMaps :: Ini -> Seq (Text, Seq (Text, Text)) | |
| 23 | toMaps (Ini m) = fmap sectionToPair m | |
| 24 | where sectionToPair (name, section) = (name, fmap valueToPair (isVals section)) | |
| 25 | valueToPair (name, value) = (name, vValue value) | |
| 24 | 26 | |
| 25 | 27 | runTest :: FilePath -> IO () |
| 26 | 28 | runTest iniF = do |