on master help sign in
Add functions for multiple document support

Add some corresponding tests Bump to version 0.12.0.0 and update changelog

commit3cda5b6b196b5b7f715810add390bd551528c9f7 authorShlomo Shuck <sjshuck@us.ibm.com> dateTue 18 Aug 2020, 22:45 (6 years ago) parentc272c62
6 files changed+147−41
yaml/ChangeLog.md+9 yaml/package.yaml+1−1 yaml/src/Data/Yaml.hs+49 yaml/src/Data/Yaml/Internal.hs+72−38 yaml/test/Data/YamlSpec.hs+14 yaml/yaml.cabal+2−2
yaml/ChangeLog.md+9
@@ -1,5 +1,14 @@
11# ChangeLog for yaml
22
3+## 0.12.0.0
4+
5+* New functions capable of parsing YAML streams containing multiple documents into a list of results:
6+ * `decodeAllEither'`
7+ * `decodeAllFileEither`
8+ * `decodeAllFileWithWarnings`
9+ * `decodeAllThrow`
10+ * `decodeAllFileThrow`
11+
312## 0.11.4.0
413
514* add `ToYaml` instance for `String` [#186](https://github.com/snoyberg/yaml/pull/186)
yaml/package.yaml+1−1
@@ -1,5 +1,5 @@
11name: yaml
2−version: 0.11.4.0
2+version: 0.12.0.0
33synopsis: Support for parsing and rendering YAML documents.
44description: README and API documentation are available at <https://www.stackage.org/package/yaml>
55category: Data
yaml/src/Data/Yaml.hs+49
@@ -37,6 +37,16 @@ module Data.Yaml
3737 , decodeFileWithWarnings
3838 , decodeThrow
3939 , decodeFileThrow
40+ -- ** Decoding multiple documents
41+ --
42+ -- | For situations where we need to be able to parse multiple documents
43+ -- separated by `---` in a YAML stream, these functions decode a list of
44+ -- values rather than a single value.
45+ , decodeAllEither'
46+ , decodeAllFileEither
47+ , decodeAllFileWithWarnings
48+ , decodeAllThrow
49+ , decodeAllFileThrow
4050 -- ** More control over decoding
4151 , decodeHelper
4252 -- * Types
@@ -193,6 +203,15 @@ decodeFileEither
193203 -> IO (Either ParseException a)
194204decodeFileEither = fmap (fmap snd) . decodeFileWithWarnings
195205
206+-- | Like `decodeFileEither`, but decode multiple documents.
207+--
208+-- @since 0.12.0.0
209+decodeAllFileEither
210+ :: FromJSON a
211+ => FilePath
212+ -> IO (Either ParseException [a])
213+decodeAllFileEither = fmap (fmap snd) . decodeAllFileWithWarnings
214+
196215-- | A version of `decodeFileEither` that returns warnings along with the parse
197216-- result.
198217--
@@ -203,6 +222,15 @@ decodeFileWithWarnings
203222 -> IO (Either ParseException ([Warning], a))
204223decodeFileWithWarnings = decodeHelper_ . Y.decodeFile
205224
225+-- | Like `decodeFileWithWarnings`, but decode multiple documents.
226+--
227+-- @since 0.12.0.0
228+decodeAllFileWithWarnings
229+ :: FromJSON a
230+ => FilePath
231+ -> IO (Either ParseException ([Warning], [a]))
232+decodeAllFileWithWarnings = decodeAllHelper_ . Y.decodeFile
233+
206234decodeEither :: FromJSON a => ByteString -> Either String a
207235decodeEither bs = unsafePerformIO
208236 $ either (Left . prettyPrintParseException) id
@@ -218,18 +246,39 @@ decodeEither' = either Left (either (Left . AesonException) Right)
218246 . fmap (fmap snd) . decodeHelper
219247 . Y.decode
220248
249+-- | Like 'decodeEither'', but decode multiple documents.
250+--
251+-- @since 0.12.0.0
252+decodeAllEither' :: FromJSON a => ByteString -> Either ParseException [a]
253+decodeAllEither' = either Left (either (Left . AesonException) Right)
254+ . unsafePerformIO
255+ . fmap (fmap snd) . decodeAllHelper
256+ . Y.decode
257+
221258-- | A version of 'decodeEither'' lifted to MonadThrow
222259--
223260-- @since 0.8.31
224261decodeThrow :: (MonadThrow m, FromJSON a) => ByteString -> m a
225262decodeThrow = either throwM return . decodeEither'
226263
264+-- | Like `decodeThrow`, but decode multiple documents.
265+--
266+-- @since 0.12.0.0
267+decodeAllThrow :: (MonadThrow m, FromJSON a) => ByteString -> m [a]
268+decodeAllThrow = either throwM return . decodeAllEither'
269+
227270-- | A version of 'decodeFileEither' lifted to MonadIO
228271--
229272-- @since 0.8.31
230273decodeFileThrow :: (MonadIO m, FromJSON a) => FilePath -> m a
231274decodeFileThrow f = liftIO $ decodeFileEither f >>= either throwIO return
232275
276+-- | Like `decodeFileThrow`, but decode multiple documents.
277+--
278+-- @since 0.12.0.0
279+decodeAllFileThrow :: (MonadIO m, FromJSON a) => FilePath -> m [a]
280+decodeAllFileThrow f = liftIO $ decodeAllFileEither f >>= either throwIO return
281+
233282-- | Construct a new 'Value' from a list of 'Value's.
234283array :: [Value] -> Value
235284array = Array . V.fromList
yaml/src/Data/Yaml/Internal.hs+72−38
@@ -11,6 +11,8 @@ module Data.Yaml.Internal
1111 , parse
1212 , decodeHelper
1313 , decodeHelper_
14+ , decodeAllHelper
15+ , decodeAllHelper_
1416 , textToScientific
1517 , stringScalar
1618 , defaultStringStyle
@@ -41,7 +43,7 @@ import qualified Data.ByteString.Lazy as BL
4143import Data.ByteString.Builder.Scientific (scientificBuilder)
4244import Data.Char (toUpper, ord)
4345import Data.List
44−import Data.Conduit ((.|), ConduitM, runConduit)
46+import Data.Conduit ((.|), ConduitM, Void, runConduit)
4547import qualified Data.Conduit.List as CL
4648import qualified Data.HashMap.Strict as M
4749import qualified Data.HashSet as HashSet
@@ -66,6 +68,7 @@ data ParseException = NonScalarKey
6668 , _expected :: Maybe Event
6769 }
6870 | InvalidYaml (Maybe YamlException)
71+ | MultipleDocuments
6972 | AesonException String
7073 | OtherParseException SomeException
7174 | NonStringKey JSONPath
@@ -115,6 +118,7 @@ prettyPrintParseException pe = case pe of
115118 _ -> ",\n" ++ context ++ ":\n"
116119 , problem
117120 ]
121+ MultipleDocuments -> "Multiple YAML documents encountered"
118122 AesonException s -> "Aeson exception:\n" ++ s
119123 OtherParseException exc -> "Generic parse exception:\n" ++ show exc
120124 NonStringKey path -> formatError path "Non-string keys are not supported"
@@ -158,24 +162,34 @@ requireEvent e = do
158162
159163parse :: ReaderT JSONPath (ConduitM Event o Parse) Value
160164parse = do
165+ docs <- parseAll
166+ case docs of
167+ [] -> return Null
168+ [doc] -> return doc
169+ _ -> liftIO $ throwIO MultipleDocuments
170+
171+parseAll :: ReaderT JSONPath (ConduitM Event o Parse) [Value]
172+parseAll = do
161173 streamStart <- lift CL.head
162174 case streamStart of
163175 Nothing ->
164176 -- empty string input
165− return Null
166− Just EventStreamStart -> do
167− documentStart <- lift CL.head
168− case documentStart of
169− Just EventStreamEnd ->
170− -- empty file input, comment only string/file input
171− return Null
172− Just EventDocumentStart -> do
173− res <- parseO
174− requireEvent EventDocumentEnd
175− requireEvent EventStreamEnd
176− return res
177− _ -> liftIO $ throwIO $ UnexpectedEvent documentStart Nothing
178− _ -> liftIO $ throwIO $ UnexpectedEvent streamStart Nothing
177+ return []
178+ Just EventStreamStart ->
179+ -- empty file input, comment only string/file input
180+ parseDocs
181+ _ -> missed streamStart
182+ where
183+ parseDocs = do
184+ documentStart <- lift CL.head
185+ case documentStart of
186+ Just EventStreamEnd -> return []
187+ Just EventDocumentStart -> do
188+ res <- parseO
189+ requireEvent EventDocumentEnd
190+ (res :) <$> parseDocs
191+ _ -> missed documentStart
192+ missed event = liftIO $ throwIO $ UnexpectedEvent event Nothing
179193
180194parseScalar :: ByteString -> Anchor -> Style -> Tag
181195 -> ReaderT JSONPath (ConduitM Event o Parse) Text
@@ -283,36 +297,56 @@ parseM mergedKeys a front = do
283297
284298 merge xs = (Set.fromList (M.keys xs \\ M.keys front), M.union front xs)
285299
300+parseSrc :: ReaderT JSONPath (ConduitM Event Void Parse) val
301+ -> ConduitM () Event Parse ()
302+ -> IO (val, ParseState)
303+parseSrc eventParser src = runResourceT $ runStateT
304+ (runConduit $ src .| runReaderT eventParser [])
305+ (ParseState Map.empty [])
306+
307+mkHelper :: ReaderT JSONPath (ConduitM Event Void Parse) val -- ^ parse libyaml events as Value or [Value]
308+ -> (SomeException -> IO (Either ParseException a)) -- ^ what to do with unhandled exceptions
309+ -> ((val, ParseState) -> Either ParseException a) -- ^ further transform and parse results
310+ -> ConduitM () Event Parse () -- ^ the libyaml event (string/file) source
311+ -> IO (Either ParseException a)
312+mkHelper eventParser onOtherExc extractResults src = catches
313+ (extractResults <$> parseSrc eventParser src)
314+ [ Handler $ \pe -> return $ Left (pe :: ParseException)
315+ , Handler $ \ye -> return $ Left $ InvalidYaml $ Just (ye :: YamlException)
316+ , Handler $ \sae -> throwIO (sae :: SomeAsyncException)
317+ , Handler onOtherExc
318+ ]
319+
286320decodeHelper :: FromJSON a
287321 => ConduitM () Y.Event Parse ()
288322 -> IO (Either ParseException ([Warning], Either String a))
289−decodeHelper src = do
290− -- This used to be tryAny, but the fact is that catching async
291− -- exceptions is fine here. We'll rethrow them immediately in the
292− -- otherwise clause.
293− x <- try $ runResourceT $ runStateT (runConduit $ src .| runReaderT parse []) (ParseState Map.empty [])
294− case x of
295− Left e
296− | Just pe <- fromException e -> return $ Left pe
297− | Just ye <- fromException e -> return $ Left $ InvalidYaml $ Just (ye :: YamlException)
298− | otherwise -> throwIO e
299− Right (y, st) -> return $ Right (parseStateWarnings st, parseEither parseJSON y)
323+decodeHelper = mkHelper parse throwIO $ \(v, st) ->
324+ Right (parseStateWarnings st, parseEither parseJSON v)
325+
326+decodeAllHelper :: FromJSON a
327+ => ConduitM () Event Parse ()
328+ -> IO (Either ParseException ([Warning], Either String [a]))
329+decodeAllHelper = mkHelper parseAll throwIO $ \(vs, st) ->
330+ Right (parseStateWarnings st, mapM (parseEither parseJSON) vs)
331+
332+catchLeft :: SomeException -> IO (Either ParseException a)
333+catchLeft = return . Left . OtherParseException
300334
301335decodeHelper_ :: FromJSON a
302336 => ConduitM () Event Parse ()
303337 -> IO (Either ParseException ([Warning], a))
304−decodeHelper_ src = do
305− x <- try $ runResourceT $ runStateT (runConduit $ src .| runReaderT parse []) (ParseState Map.empty [])
306− case x of
307− Left e
308− | Just pe <- fromException e -> return $ Left pe
309− | Just ye <- fromException e -> return $ Left $ InvalidYaml $ Just (ye :: YamlException)
310− | Just sae <- fromException e -> throwIO (sae :: SomeAsyncException)
311− | otherwise -> return $ Left $ OtherParseException e
312− Right (y, st) -> return $ either
313− (Left . AesonException)
314− Right
315− ((,) (parseStateWarnings st) <$> parseEither parseJSON y)
338+decodeHelper_ = mkHelper parse catchLeft $ \(v, st) ->
339+ case parseEither parseJSON v of
340+ Left e -> Left $ AesonException e
341+ Right x -> Right (parseStateWarnings st, x)
342+
343+decodeAllHelper_ :: FromJSON a
344+ => ConduitM () Event Parse ()
345+ -> IO (Either ParseException ([Warning], [a]))
346+decodeAllHelper_ = mkHelper parseAll catchLeft $ \(vs, st) ->
347+ case mapM (parseEither parseJSON) vs of
348+ Left e -> Left $ AesonException e
349+ Right xs -> Right (parseStateWarnings st, xs)
316350
317351type StringStyle = Text -> ( Tag, Style )
318352
yaml/test/Data/YamlSpec.hs+14
@@ -69,6 +69,11 @@ shouldDecode bs expected = do
6969 actual <- D.decodeThrow bs
7070 actual `shouldBe` expected
7171
72+shouldDecodeAll :: (Show a, D.FromJSON a, Eq a) => B8.ByteString -> [a] -> IO ()
73+shouldDecodeAll bs expected = do
74+ actual <- D.decodeAllThrow bs
75+ actual `shouldBe` expected
76+
7277shouldDecodeEvents :: B8.ByteString -> [Y.Event] -> IO ()
7378shouldDecodeEvents bs expected = do
7479 actual <- runConduitRes $ Y.decode bs .| CL.consume
@@ -193,6 +198,15 @@ spec = do
193198 it "returns Left" $ do
194199 (D.decodeFileEither "./does_not_exist.yaml" :: IO (Either D.ParseException D.Value)) >>= (`shouldSatisfy` isLeft)
195200
201+ describe "multiple document support" $ do
202+ it "decodes zero-length input" $
203+ "" `shouldDecodeAll` ([] :: [D.Value])
204+ it "decodes comment-only input" $
205+ "# foo\n# bar" `shouldDecodeAll` ([] :: [D.Value])
206+ it "decodes a single document stream" $
207+ "foo: true" `shouldDecodeAll` [object ["foo" .= True]]
208+ it "decodes multiple documents" $
209+ "--- 1\n--- 2" `shouldDecodeAll` [D.Number 1, D.Number 2]
196210
197211 describe "round-tripping of special scalars" $ do
198212 let special = words "y Y On ON false 12345 12345.0 12345a 12e3"
yaml/yaml.cabal+2−2
@@ -4,10 +4,10 @@ cabal-version: 1.12
44--
55-- see: https://github.com/sol/hpack
66--
7−-- hash: 1f75743c2fefefb33f6f3e8a51fce90cb3f286c975d205d7ded3789d5b15c488
7+-- hash: 118ef05af05c3228e80397659bc1769f2d5e733cf4111316fa3aebeaeb0d6152
88
99name: yaml
10−version: 0.11.4.0
10+version: 0.12.0.0
1111synopsis: Support for parsing and rendering YAML documents.
1212description: README and API documentation are available at <https://www.stackage.org/package/yaml>
1313category: Data