Introduce NormalizedText alias for lower-casing and elimination of spaces
Getty Ritter
7 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 |