Introduce NormalizedText alias for lower-casing and elimination of spaces
Getty Ritter
8 years ago
| 74 | 74 | -- type alias |
| 75 | 75 | type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t |
| 76 | 76 | |
| 77 | lkp :: Text -> Seq (Text, a) -> Maybe a | |
| 78 | lkp t = go . Seq.viewl | |
| 79 | where go ((t', x) Seq.:< rs) | |
| 80 | | T.toLower t == T.toLower t' = Just x | |
| 81 | | otherwise = go (Seq.viewl rs) | |
| 82 | go Seq.EmptyL = Nothing | |
| 83 | ||
| 84 | rmv :: Text -> Seq (Field s) -> Seq (Field s) | |
| 85 | rmv n = Seq.filter (\ f -> T.toLower (fieldName f) /= T.toLower n) | |
| 77 | lkp :: NormalizedText -> Seq (NormalizedText, a) -> Maybe a | |
| 78 | lkp t = fmap snd . F.find (\ (t', _) -> t' == t) | |
| 79 | ||
| 80 | rmv :: NormalizedText -> Seq (Field s) -> Seq (Field s) | |
| 81 | rmv n = Seq.filter (\ f -> fieldName f /= n) | |
| 86 | 82 | |
| 87 | 83 | -- The & operator is really useful here, but it didn't show up in |
| 88 | 84 | -- earlier versions, so it gets redefined here. |
| 137 | 133 | section :: Text -> SectionSpec s () -> IniSpec s () |
| 138 | 134 | section name (SectionSpec mote) = IniSpec $ do |
| 139 | 135 | let fields = runBidirM mote |
| 140 |
modify (Seq.|> Section |
|
| 136 | modify (Seq.|> Section (normalize name) fields (allOptional fields)) | |
| 141 | 137 | |
| 142 | 138 | allOptional :: (Seq (Field s)) -> Bool |
| 143 | 139 | allOptional = all isOptional |
| 144 | 140 | where isOptional (Field _ fd) = fdSkipIfMissing fd |
| 145 | 141 | isOptional (FieldMb _ fd) = fdSkipIfMissing fd |
| 146 | 142 | |
| 147 |
data Section s = Section |
|
| 143 | data Section s = Section NormalizedText (Seq (Field s)) Bool | |
| 148 | 144 | |
| 149 | 145 | -- | A "Field" is a description of |
| 150 | 146 | data Field s |
| 152 | 148 | | forall a. Eq a => FieldMb (Lens s s (Maybe a) (Maybe a)) (FieldDescription a) |
| 153 | 149 | |
| 154 | 150 | -- convenience accessors for things in a Field |
| 155 |
fieldName :: Field s -> |
|
| 151 | fieldName :: Field s -> NormalizedText | |
| 156 | 152 | fieldName (Field _ FieldDescription { fdName = n }) = n |
| 157 | 153 | fieldName (FieldMb _ FieldDescription { fdName = n }) = n |
| 158 | 154 | |
| 166 | 162 | -- well as other metadata that might be needed in the course of |
| 167 | 163 | -- parsing or serializing a structure. |
| 168 | 164 | data FieldDescription t = FieldDescription |
| 169 |
{ fdName :: |
|
| 165 | { fdName :: NormalizedText | |
| 170 | 166 | , fdValue :: FieldValue t |
| 171 | 167 | , fdComment :: Seq Text |
| 172 | 168 | , fdDummy :: Maybe Text |
| 239 | 235 | -- values associated with that field. |
| 240 | 236 | field :: Text -> FieldValue a -> FieldDescription a |
| 241 | 237 | field name value = FieldDescription |
| 242 |
{ fdName = n |
|
| 238 | { fdName = normalize name | |
| 243 | 239 | , fdValue = value |
| 244 | 240 | , fdComment = Seq.empty |
| 245 | 241 | , fdDummy = Nothing |
| 339 | 335 | -- yet. Just you wait. This is just the regular part. 'runSpec' is |
| 340 | 336 | -- easy: we walk the spec, and for each section, find the |
| 341 | 337 | -- corresponding section in the INI file and call runFields. |
| 342 |
runSpec :: s -> Seq.ViewL (Section s) -> Seq ( |
|
| 338 | runSpec :: s -> Seq.ViewL (Section s) -> Seq (NormalizedText, IniSection) | |
| 343 | 339 | -> Either String s |
| 344 | 340 | runSpec s Seq.EmptyL _ = Right s |
| 345 | 341 | runSpec s (Section name fs opt Seq.:< rest) ini |
| 346 |
| Just v <- lkp |
|
| 342 | | Just v <- lkp name ini = do | |
| 347 | 343 | s' <- runFields s (Seq.viewl fs) v |
| 348 | 344 | runSpec s' (Seq.viewl rest) ini |
| 349 | 345 | | opt = runSpec s (Seq.viewl rest) ini |
| 391 | 387 | emitIniFile :: s -> IniSpec s () -> Text |
| 392 | 388 | emitIniFile s (IniSpec mote) = |
| 393 | 389 | let spec = runBidirM mote in |
| 394 |
printIni $ Ini $ fmap (\ (Section name fs _) -> |
|
| 390 | printIni $ Ini $ fmap (\ (Section name fs _) -> | |
| 391 | (name, toSection s (actualText name) fs)) spec | |
| 395 | 392 | |
| 396 | 393 | mkComments :: Seq Text -> Seq BlankLine |
| 397 | 394 | mkComments comments = |
| 408 | 405 | ( fdName descr |
| 409 | 406 | , IniValue |
| 410 | 407 | { vLineNo = 0 |
| 411 |
, vName = |
|
| 408 | , vName = actualText (fdName descr) | |
| 412 | 409 | , vValue = val |
| 413 | 410 | , vComments = mkComments (fdComment descr) |
| 414 | 411 | , vCommentedOut = optional |
| 488 | 485 | ini' <- updateIniSections s ini spec pol |
| 489 | 486 | return (printIni (Ini ini')) |
| 490 | 487 | |
| 491 |
updateIniSections :: s -> Seq ( |
|
| 488 | updateIniSections :: s -> Seq (NormalizedText, IniSection) | |
| 492 | 489 | -> Seq (Section s) |
| 493 | 490 | -> UpdatePolicy |
| 494 |
-> Either String (Seq ( |
|
| 491 | -> Either String (Seq (NormalizedText, IniSection)) | |
| 495 | 492 | updateIniSections s sections fields pol = do |
| 496 | 493 | existingSections <- F.for sections $ \ (name, sec) -> do |
| 497 | 494 | let err = (Left ("Unexpected top-level section: " ++ show name)) |
| 498 | 495 | Section _ spec _ <- maybe err Right |
| 499 |
(F.find (\ (Section n _ _) -> |
|
| 496 | (F.find (\ (Section n _ _) -> n == name) fields) | |
| 500 | 497 | newVals <- updateIniSection s (isVals sec) spec pol |
| 501 | 498 | return (name, sec { isVals = newVals }) |
| 502 | 499 | let existingSectionNames = fmap fst existingSections |
| 504 | 501 | \ (Section nm spec isOpt) -> |
| 505 | 502 | if nm `elem` existingSectionNames |
| 506 | 503 | then return mempty |
| 507 |
else return |
|
| 504 | else return (Seq.singleton (nm, IniSection (actualText nm) mempty 0 0 mempty)) | |
| 508 | 505 | return (existingSections <> F.asum newSections) |
| 509 | 506 | |
| 510 | updateIniSection :: s -> Seq (Text, IniValue) -> Seq (Field s) | |
| 511 | -> UpdatePolicy -> Either String (Seq (Text, IniValue)) | |
| 507 | updateIniSection :: s -> Seq (NormalizedText, IniValue) -> Seq (Field s) | |
| 508 | -> UpdatePolicy -> Either String (Seq (NormalizedText, IniValue)) | |
| 512 | 509 | updateIniSection s values fields pol = go (Seq.viewl values) fields |
| 513 | 510 | where go ((t, val) :< vs) fs = |
| 514 | 511 | -- For each field, we need to fetch the description of the |
| 592 | 589 | mkComments cs |
| 593 | 590 | val = IniValue |
| 594 | 591 | { vLineNo = 0 |
| 595 |
, vName = |
|
| 592 | , vName = actualText t <> " " | |
| 596 | 593 | , vValue = "" |
| 597 | 594 | , vComments = comments |
| 598 | 595 | , vCommentedOut = False |
| 6 | 6 | , IniSection(..) |
| 7 | 7 | , IniValue(..) |
| 8 | 8 | , BlankLine(..) |
| 9 | , NormalizedText(..) | |
| 10 | , normalize | |
| 9 | 11 | -- * serializing and deserializing |
| 10 | 12 | , parseIni |
| 11 | 13 | , printIni |
| 23 | 25 | import Text.Megaparsec |
| 24 | 26 | import Text.Megaparsec.Text |
| 25 | 27 | |
| 28 | data NormalizedText = NormalizedText | |
| 29 | { actualText :: Text | |
| 30 | , normalizedText :: Text | |
| 31 | } deriving (Show) | |
| 32 | ||
| 33 | normalize :: Text -> NormalizedText | |
| 34 | normalize t = NormalizedText t (T.toLower (T.strip t)) | |
| 35 | ||
| 36 | instance Eq NormalizedText where | |
| 37 | NormalizedText _ x == NormalizedText _ y = | |
| 38 | x == y | |
| 39 | ||
| 40 | instance Ord NormalizedText where | |
| 41 | NormalizedText _ x `compare` NormalizedText _ y = | |
| 42 | x `compare` y | |
| 43 | ||
| 26 | 44 | -- | An 'Ini' value is a mapping from section names to |
| 27 | 45 | -- 'IniSection' values. The section names in this mapping are |
| 28 | 46 | -- normalized to lower-case and stripped of whitespace. This |
| 29 | 47 | -- sequence retains the ordering of the original source file. |
| 30 | 48 | newtype Ini = Ini |
| 31 |
{ fromIni :: Seq ( |
|
| 49 | { fromIni :: Seq (NormalizedText, IniSection) | |
| 32 | 50 | } deriving (Eq, Show) |
| 33 | 51 | |
| 34 | 52 | -- | An 'IniSection' consists of a name, a mapping of key-value pairs, |
| 40 | 58 | { isName :: Text |
| 41 | 59 | -- ^ The name of the section, as it appears in the |
| 42 | 60 | -- original INI source |
| 43 |
, isVals :: Seq ( |
|
| 61 | , isVals :: Seq (NormalizedText, IniValue) | |
| 44 | 62 | -- ^ The key-value mapping within that section. Key |
| 45 | 63 | -- names here are normalized to lower-case and |
| 46 | 64 | -- stripped of whitespace. This sequence retains |
| 114 | 132 | txt <- T.pack `fmap` manyTill anyChar eol |
| 115 | 133 | return (CommentLine c txt) |
| 116 | 134 | |
| 117 |
pSections :: Seq BlankLine -> Seq ( |
|
| 135 | pSections :: Seq BlankLine -> Seq (NormalizedText, IniSection) -> Parser Ini | |
| 118 | 136 | pSections leading prevs = |
| 119 | 137 | pSection leading prevs <|> (Ini prevs <$ void eof) |
| 120 | 138 | |
| 121 |
pSection :: Seq BlankLine -> Seq ( |
|
| 139 | pSection :: Seq BlankLine -> Seq (NormalizedText, IniSection) -> Parser Ini | |
| 122 | 140 | pSection leading prevs = do |
| 123 | 141 | start <- getCurrentLine |
| 124 | 142 | void (char '[') |
| 131 | 149 | pPairs :: Text |
| 132 | 150 | -> Int |
| 133 | 151 | -> Seq BlankLine |
| 134 |
-> Seq ( |
|
| 152 | -> Seq (NormalizedText, IniSection) | |
| 135 | 153 | -> Seq BlankLine |
| 136 |
-> Seq ( |
|
| 154 | -> Seq (NormalizedText, IniValue) | |
| 137 | 155 | -> Parser Ini |
| 138 | 156 | pPairs name start leading prevs comments pairs = newPair <|> finishedSection |
| 139 | 157 | where |
| 150 | 168 | , isEndLine = end |
| 151 | 169 | , isComments = leading |
| 152 | 170 | } |
| 153 | pSections comments (prevs Seq.|> (T.toLower name, newSection)) | |
| 154 | ||
| 155 |
|
|
| 171 | pSections comments (prevs Seq.|> (normalize name, newSection)) | |
| 172 | ||
| 173 | pPair :: Seq BlankLine -> Parser (NormalizedText, IniValue) | |
| 156 | 174 | pPair leading = do |
| 157 | 175 | pos <- getCurrentLine |
| 158 | 176 | key <- T.pack `fmap` some (noneOf "[]=:") |
| 159 | 177 | delim <- oneOf ":=" |
| 160 | 178 | val <- T.pack `fmap` manyTill anyChar eol |
| 161 |
return ( |
|
| 179 | return ( normalize key | |
| 162 | 180 | , IniValue |
| 163 | 181 | { vLineNo = pos |
| 164 | 182 | , vName = key |
| 43 | 43 | import GHC.Exts (IsList(..)) |
| 44 | 44 | import Text.Read (readMaybe) |
| 45 | 45 | |
| 46 |
lkp :: |
|
| 46 | lkp :: NormalizedText -> Seq (NormalizedText, a) -> Maybe a | |
| 47 | 47 | lkp t = go . Seq.viewl |
| 48 | 48 | where go ((t', x) Seq.:< rs) |
| 49 | 49 | | t == t' = Just x |
| 85 | 85 | -- Left "No top-level section named \"TWO\"" |
| 86 | 86 | section :: Text -> SectionParser a -> IniParser a |
| 87 | 87 | section name (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) -> |
| 88 |
case lkp ( |
|
| 88 | case lkp (normalize name) ini of | |
| 89 | 89 | Nothing -> Left ("No top-level section named " ++ show name) |
| 90 | 90 | Just sec -> runExceptT thunk sec |
| 91 | 91 | |
| 101 | 101 | -- Right Nothing |
| 102 | 102 | sectionMb :: Text -> SectionParser a -> IniParser (Maybe a) |
| 103 | 103 | sectionMb name (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) -> |
| 104 |
case lkp ( |
|
| 104 | case lkp (normalize name) ini of | |
| 105 | 105 | Nothing -> return Nothing |
| 106 | 106 | Just sec -> Just `fmap` runExceptT thunk sec |
| 107 | 107 | |
| 117 | 117 | -- Right "def" |
| 118 | 118 | sectionDef :: Text -> a -> SectionParser a -> IniParser a |
| 119 | 119 | sectionDef name def (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) -> |
| 120 |
case lkp ( |
|
| 120 | case lkp (normalize name) ini of | |
| 121 | 121 | Nothing -> return def |
| 122 | 122 | Just sec -> runExceptT thunk sec |
| 123 | 123 | |
| 131 | 131 | |
| 132 | 132 | rawFieldMb :: Text -> StParser IniSection (Maybe IniValue) |
| 133 | 133 | rawFieldMb name = ExceptT $ \m -> |
| 134 |
return (lkp |
|
| 134 | return (lkp (normalize name) (isVals m)) | |
| 135 | 135 | |
| 136 | 136 | rawField :: Text -> StParser IniSection IniValue |
| 137 | 137 | rawField name = do |
| 206 | 206 | -- Right "def" |
| 207 | 207 | fieldDef :: Text -> Text -> SectionParser Text |
| 208 | 208 | fieldDef name def = SectionParser $ ExceptT $ \m -> |
| 209 |
case lkp |
|
| 209 | case lkp (normalize name) (isVals m) of | |
| 210 | 210 | Nothing -> return def |
| 211 | 211 | Just x -> return (vValue x) |
| 212 | 212 | |
| 56 | 56 | toMaps :: I2.Ini -> HashMap Text (HashMap Text Text) |
| 57 | 57 | toMaps (I2.Ini m) = conv (fmap sectionToPair m) |
| 58 | 58 | where sectionToPair (name, section) = |
| 59 | (name, conv (fmap valueToPair (I2.isVals section))) | |
| 60 | valueToPair (name, value) = (T.toLower name, T.strip (I2.vValue value)) | |
| 59 | (I2.normalizedText name, conv (fmap valueToPair (I2.isVals section))) | |
| 60 | valueToPair (name, value) = | |
| 61 | (I2.normalizedText name, T.strip (I2.vValue value)) | |
| 61 | 62 | conv = HM.fromList . Fold.toList |
| 62 | 63 | |
| 63 | 64 | textChunk :: Monad m => Gen m Text |
| 87 | 88 | k <- textChunk |
| 88 | 89 | v <- textChunk |
| 89 | 90 | cs <- mkComments |
| 90 |
return ( |
|
| 91 | return ( I2.normalize k | |
| 91 | 92 | , I2.IniValue 0 k v cs False '=' |
| 92 | 93 | ) |
| 93 | 94 | cs <- mkComments |
| 94 |
return ( |
|
| 95 | return ( I2.normalize name | |
| 95 | 96 | , I2.IniSection name (Seq.fromList (nubBy ((==) `on` fst) section)) 0 0 cs |
| 96 | 97 | ) |
| 97 | 98 | return (I2.Ini (Seq.fromList (nubBy ((==) `on` fst) ss))) |
| 24 | 24 | |
| 25 | 25 | toMaps :: Ini -> IniSeq |
| 26 | 26 | toMaps (Ini m) = fmap sectionToPair m |
| 27 | where sectionToPair (name, section) = (name, fmap valueToPair (isVals section)) | |
| 28 | valueToPair (name, value) = (name, T.strip (vValue value)) | |
| 27 | where sectionToPair (name, section) = | |
| 28 | (normalizedText name, fmap valueToPair (isVals section)) | |
| 29 | valueToPair (name, value) = | |
| 30 | (normalizedText name, T.strip (vValue value)) | |
| 29 | 31 | |
| 30 | 32 | runTest :: FilePath -> IO () |
| 31 | 33 | runTest iniF = do |