on master help sign in
Merge pull request #54 from zalora/handle_async_exceptions

Handle async exceptions

commit96d77a740e25f8220801470405a8bde1745e1605 authorMichael Snoyman <michael@snoyman.com> dateWed 3 Dec 2014, 13:30 (12 years ago) parentb77785c45f6459
6 files changed+248−208
Data/Yaml.hs+9−204 Data/Yaml/Include.hs+2−3 Data/Yaml/Internal.hs+219 test/Data/Yaml/IncludeSpec.hs+7 test/Data/YamlSpec.hs+7−1 yaml.cabal+4
Data/Yaml.hs+9−204
@@ -1,8 +1,4 @@
1−{-# LANGUAGE FlexibleContexts #-}
2−{-# LANGUAGE FlexibleInstances #-}
31{-# LANGUAGE CPP #-}
4−{-# LANGUAGE DeriveDataTypeable #-}
5−{-# LANGUAGE MultiParamTypeClasses #-}
62{-# LANGUAGE PatternGuards #-}
73{-# LANGUAGE ScopedTypeVariables #-}
84{-# LANGUAGE OverloadedStrings #-}
@@ -66,37 +62,27 @@ import Data.Aeson
6662import Data.Aeson.Types (Pair, parseMaybe, parseEither, Parser)
6763import Text.Libyaml hiding (encode, decode, encodeFile, decodeFile)
6864import Data.ByteString (ByteString)
69−import qualified Data.Map as Map
7065import System.IO.Unsafe (unsafePerformIO)
71−import Control.Exception (try, throwIO, fromException, Exception, SomeException, AsyncException)
72−import Control.Monad.Trans.State
66+import Control.Exception
7367import qualified Data.Conduit as C
7468import qualified Data.Conduit.List as CL
75−import Control.Monad.Trans.Class (MonadTrans, lift)
76−import Control.Monad.IO.Class (MonadIO, liftIO)
77−import Control.Monad (liftM, ap)
78−import Control.Applicative (Applicative(..))
79−import Data.Char (toUpper)
8069import qualified Data.Vector as V
81−import Data.Text (Text, pack)
70+import Data.Text (Text)
8271import qualified Data.Text as T
83−import Data.Text.Encoding (encodeUtf8, decodeUtf8With)
84−import Data.Text.Encoding.Error (lenientDecode)
72+import Data.Text.Encoding (encodeUtf8)
8573import qualified Data.HashMap.Strict as M
86−import Data.Typeable
8774import qualified Data.HashSet as HashSet
88−import Data.Text.Read
8975#if MIN_VERSION_aeson(0, 7, 0)
90−import Data.Scientific (fromFloatDigits)
9176import qualified Data.Text.Encoding as TE
9277import qualified Data.Text.Lazy as TL
9378import Data.Text.Lazy.Builder (toLazyText)
9479import Data.Aeson.Encode (encodeToTextBuilder)
9580#else
96−import Data.Attoparsec.Number
9781import qualified Data.ByteString.Char8 as S8
9882#endif
99−import Control.Monad.Trans.Resource (ResourceT, runResourceT)
83+import Control.Monad.Trans.Resource (runResourceT)
84+
85+import Data.Yaml.Internal
10086
10187encode :: ToJSON a => a -> ByteString
10288encode obj = unsafePerformIO $
@@ -172,164 +158,12 @@ isNumeric =
172158 || c == '.'
173159 || c == '-'
174160
175−-- Parsing
176−
177−data ParseException = NonScalarKey
178− | UnknownAlias { _anchorName :: Y.AnchorName }
179− | UnexpectedEvent { _received :: Maybe Event
180− , _expected :: Maybe Event
181− }
182− | InvalidYaml (Maybe YamlException)
183− | AesonException String
184− | OtherParseException SomeException
185− | NonStringKeyAlias Y.AnchorName Value
186− | CyclicIncludes
187− deriving (Show, Typeable)
188−instance Exception ParseException
189−
190−newtype PErrorT m a = PErrorT { runPErrorT :: m (Either ParseException a) }
191−instance Monad m => Functor (PErrorT m) where
192− fmap = liftM
193−instance Monad m => Applicative (PErrorT m) where
194− pure = return
195− (<*>) = ap
196−instance Monad m => Monad (PErrorT m) where
197− return = PErrorT . return . Right
198− (PErrorT m) >>= f = PErrorT $ do
199− e <- m
200− case e of
201− Left e' -> return $ Left e'
202− Right a -> runPErrorT $ f a
203−instance MonadTrans PErrorT where
204− lift = PErrorT . liftM Right
205−instance MonadIO m => MonadIO (PErrorT m) where
206− liftIO = lift . liftIO
207−
208−type Parse = StateT (Map.Map String Value) (ResourceT IO)
209−
210−requireEvent :: Event -> C.Sink Event Parse ()
211−requireEvent e = do
212− f <- CL.head
213− if f == Just e
214− then return ()
215− else liftIO $ throwIO $ UnexpectedEvent f $ Just e
216−
217−parse :: C.Sink Event Parse Value
218−parse = do
219− requireEvent EventStreamStart
220− requireEvent EventDocumentStart
221− res <- parseO
222− requireEvent EventDocumentEnd
223− requireEvent EventStreamEnd
224− return res
225−
226−parseScalar :: ByteString -> Anchor -> Style -> Tag
227− -> C.Sink Event Parse Text
228−parseScalar v a style tag = do
229− let res = decodeUtf8With lenientDecode v
230− case a of
231− Nothing -> return res
232− Just an -> do
233− lift $ modify (Map.insert an $ textToValue style tag res)
234− return res
235−
236−textToValue :: Style -> Tag -> Text -> Value
237−textToValue SingleQuoted _ t = String t
238−textToValue DoubleQuoted _ t = String t
239−textToValue _ StrTag t = String t
240−textToValue Folded _ t = String t
241−textToValue _ _ t
242− | t `elem` ["null", "Null", "NULL", "~", ""] = Null
243− | any (t `isLike`) ["y", "yes", "on", "true"] = Bool True
244− | any (t `isLike`) ["n", "no", "off", "false"] = Bool False
245−#if MIN_VERSION_aeson(0, 7, 0)
246− | Right (x, "") <- signed decimal t = Number $ fromIntegral (x :: Integer)
247− | Right (x, "") <- double t = Number $ fromFloatDigits x
248−#else
249− | Right (x, "") <- signed decimal t = Number $ I x
250− | Right (x, "") <- double t = Number $ D x
251−#endif
252− | otherwise = String t
253− where x `isLike` ref = x `elem` [ref, T.toUpper ref, titleCased]
254− where titleCased = toUpper (T.head ref) `T.cons` T.tail ref
255−
256−
257−parseO :: C.Sink Event Parse Value
258−parseO = do
259− me <- CL.head
260− case me of
261− Just (EventScalar v tag style a) -> fmap (textToValue style tag) $ parseScalar v a style tag
262− Just (EventSequenceStart a) -> parseS a id
263− Just (EventMappingStart a) -> parseM a M.empty
264− Just (EventAlias an) -> do
265− m <- lift get
266− case Map.lookup an m of
267− Nothing -> liftIO $ throwIO $ UnknownAlias an
268− Just v -> return v
269− _ -> liftIO $ throwIO $ UnexpectedEvent me Nothing
270−
271−parseS :: Y.Anchor
272− -> ([Value] -> [Value])
273− -> C.Sink Event Parse Value
274−parseS a front = do
275− me <- CL.peek
276− case me of
277− Just EventSequenceEnd -> do
278− CL.drop 1
279− let res = Array $ V.fromList $ front []
280− case a of
281− Nothing -> return res
282− Just an -> do
283− lift $ modify $ Map.insert an res
284− return res
285− _ -> do
286− o <- parseO
287− parseS a $ front . (:) o
288−
289−parseM :: Y.Anchor
290− -> M.HashMap Text Value
291− -> C.Sink Event Parse Value
292−parseM a front = do
293− me <- CL.peek
294− case me of
295− Just EventMappingEnd -> do
296− CL.drop 1
297− let res = Object front
298− case a of
299− Nothing -> return res
300− Just an -> do
301− lift $ modify $ Map.insert an res
302− return res
303− _ -> do
304− CL.drop 1
305− s <- case me of
306− Just (EventScalar v tag style a') -> parseScalar v a' style tag
307− Just (EventAlias an) -> do
308− m <- lift get
309− case Map.lookup an m of
310− Nothing -> liftIO $ throwIO $ UnknownAlias an
311− Just (String t) -> return t
312− Just v -> liftIO $ throwIO $ NonStringKeyAlias an v
313− _ -> liftIO $ throwIO $ UnexpectedEvent me Nothing
314− o <- parseO
315−
316− let al = M.insert s o front
317− al' = if s == pack "<<"
318− then case o of
319− Object l -> M.union front l
320− Array l -> M.union front $ foldl merge' M.empty $ V.toList l
321− _ -> al
322− else al
323− parseM a al'
324− where merge' al (Object om) = M.union al om
325− merge' al _ = al
326−
327161decode :: FromJSON a
328162 => ByteString
329163 -> Maybe a
330164decode bs = unsafePerformIO
331− $ fmap (either (const Nothing) (either (const Nothing) Just))
332− $ decodeHelper (Y.decode bs)
165+ $ fmap (either (const Nothing) id)
166+ $ decodeHelper_ (Y.decode bs)
333167
334168decodeFile :: FromJSON a
335169 => FilePath
@@ -343,24 +177,7 @@ decodeFileEither
343177 :: FromJSON a
344178 => FilePath
345179 -> IO (Either ParseException a)
346−decodeFileEither fp = do
347− x <- try $ runResourceT $ flip evalStateT Map.empty $ Y.decodeFile fp C.$$ parse
348− case x of
349− Left e
350− | Just pe <- fromException e -> return $ Left pe
351− | Just ye <- fromException e -> return $ Left $ InvalidYaml $ Just (ye :: YamlException)
352− | shouldBeCaught e -> return $ Left $ OtherParseException e
353− | otherwise -> throwIO e
354− Right y ->
355− return $ case parseEither parseJSON y of
356− Left s -> Left $ AesonException s
357− Right v -> Right v
358− where
359− -- FIXME this function needs more thought
360− shouldBeCaught e
361− | Just (_ :: AsyncException) <- fromException e = False
362− -- Would be nice... | Just (_ :: Timeout) <- fromException e = False
363− | otherwise = True
180+decodeFileEither = decodeHelper_ . Y.decodeFile
364181
365182decodeEither :: FromJSON a => ByteString -> Either String a
366183decodeEither bs = unsafePerformIO
@@ -376,18 +193,6 @@ decodeEither' = either Left (either (Left . AesonException) Right)
376193 . decodeHelper
377194 . Y.decode
378195
379−decodeHelper :: FromJSON a
380− => C.Source Parse Y.Event
381− -> IO (Either ParseException (Either String a))
382−decodeHelper src = do
383− x <- try $ runResourceT $ flip evalStateT Map.empty $ src C.$$ parse
384− case x of
385− Left e
386− | Just pe <- fromException e -> return $ Left pe
387− | Just ye <- fromException e -> return $ Left $ InvalidYaml $ Just (ye :: YamlException)
388− | otherwise -> throwIO e
389− Right y -> return $ Right $ parseEither parseJSON y
390−
391196array :: [Value] -> Value
392197array = Array . V.fromList
393198
Data/Yaml/Include.hs+2−3
@@ -1,7 +1,6 @@
11{-# LANGUAGE RankNTypes #-}
22module Data.Yaml.Include (decodeFile, decodeFileEither) where
33
4−import Control.Applicative
54import Control.Monad (when)
65import Control.Exception (throwIO)
76import Control.Monad.IO.Class (liftIO)
@@ -13,7 +12,7 @@ import Data.Text.Encoding (decodeUtf8)
1312import Data.Aeson (FromJSON)
1413import Data.Conduit
1514import qualified Data.Conduit.List as CL
16−import Data.Yaml (ParseException(..), decodeHelper)
15+import Data.Yaml.Internal (ParseException(..), decodeHelper_, decodeHelper)
1716import Text.Libyaml hiding (decodeFile)
1817import qualified Text.Libyaml as Y
1918
@@ -59,4 +58,4 @@ decodeFileEither
5958 :: FromJSON a
6059 => FilePath
6160 -> IO (Either ParseException a)
62−decodeFileEither fp = either Left (either (Left . AesonException) Right) <$> decodeHelper (eventsFromFile fp)
61+decodeFileEither = decodeHelper_ . eventsFromFile
Data/Yaml/Internal.hsnew file+219
@@ -0,0 +1,219 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE PatternGuards #-}
4+{-# LANGUAGE OverloadedStrings #-}
5+module Data.Yaml.Internal
6+ (
7+ ParseException(..)
8+ , parse
9+ , decodeHelper
10+ , decodeHelper_
11+ ) where
12+
13+import qualified Text.Libyaml as Y
14+import Data.Aeson
15+import Data.Aeson.Types hiding (parse)
16+import Text.Libyaml hiding (encode, decode, encodeFile, decodeFile)
17+import Data.ByteString (ByteString)
18+import qualified Data.Map as Map
19+import Control.Exception
20+import Control.Exception.Enclosed
21+import Control.Monad.Trans.State
22+import qualified Data.Conduit as C
23+import qualified Data.Conduit.List as CL
24+import Control.Monad.Trans.Class (MonadTrans, lift)
25+import Control.Monad.IO.Class (MonadIO, liftIO)
26+import Control.Monad (liftM, ap)
27+import Control.Applicative (Applicative(..))
28+import Data.Char (toUpper)
29+import qualified Data.Vector as V
30+import Data.Text (Text, pack)
31+import qualified Data.Text as T
32+import Data.Text.Encoding (decodeUtf8With)
33+import Data.Text.Encoding.Error (lenientDecode)
34+import qualified Data.HashMap.Strict as M
35+import Data.Typeable
36+import Data.Text.Read
37+#if MIN_VERSION_aeson(0, 7, 0)
38+import Data.Scientific (fromFloatDigits)
39+#else
40+import Data.Attoparsec.Number
41+#endif
42+import Control.Monad.Trans.Resource (ResourceT, runResourceT)
43+
44+data ParseException = NonScalarKey
45+ | UnknownAlias { _anchorName :: Y.AnchorName }
46+ | UnexpectedEvent { _received :: Maybe Event
47+ , _expected :: Maybe Event
48+ }
49+ | InvalidYaml (Maybe YamlException)
50+ | AesonException String
51+ | OtherParseException SomeException
52+ | NonStringKeyAlias Y.AnchorName Value
53+ | CyclicIncludes
54+ deriving (Show, Typeable)
55+instance Exception ParseException
56+
57+newtype PErrorT m a = PErrorT { runPErrorT :: m (Either ParseException a) }
58+instance Monad m => Functor (PErrorT m) where
59+ fmap = liftM
60+instance Monad m => Applicative (PErrorT m) where
61+ pure = return
62+ (<*>) = ap
63+instance Monad m => Monad (PErrorT m) where
64+ return = PErrorT . return . Right
65+ (PErrorT m) >>= f = PErrorT $ do
66+ e <- m
67+ case e of
68+ Left e' -> return $ Left e'
69+ Right a -> runPErrorT $ f a
70+instance MonadTrans PErrorT where
71+ lift = PErrorT . liftM Right
72+instance MonadIO m => MonadIO (PErrorT m) where
73+ liftIO = lift . liftIO
74+
75+type Parse = StateT (Map.Map String Value) (ResourceT IO)
76+
77+requireEvent :: Event -> C.Sink Event Parse ()
78+requireEvent e = do
79+ f <- CL.head
80+ if f == Just e
81+ then return ()
82+ else liftIO $ throwIO $ UnexpectedEvent f $ Just e
83+
84+parse :: C.Sink Event Parse Value
85+parse = do
86+ requireEvent EventStreamStart
87+ requireEvent EventDocumentStart
88+ res <- parseO
89+ requireEvent EventDocumentEnd
90+ requireEvent EventStreamEnd
91+ return res
92+
93+parseScalar :: ByteString -> Anchor -> Style -> Tag
94+ -> C.Sink Event Parse Text
95+parseScalar v a style tag = do
96+ let res = decodeUtf8With lenientDecode v
97+ case a of
98+ Nothing -> return res
99+ Just an -> do
100+ lift $ modify (Map.insert an $ textToValue style tag res)
101+ return res
102+
103+textToValue :: Style -> Tag -> Text -> Value
104+textToValue SingleQuoted _ t = String t
105+textToValue DoubleQuoted _ t = String t
106+textToValue _ StrTag t = String t
107+textToValue Folded _ t = String t
108+textToValue _ _ t
109+ | t `elem` ["null", "Null", "NULL", "~", ""] = Null
110+ | any (t `isLike`) ["y", "yes", "on", "true"] = Bool True
111+ | any (t `isLike`) ["n", "no", "off", "false"] = Bool False
112+#if MIN_VERSION_aeson(0, 7, 0)
113+ | Right (x, "") <- signed decimal t = Number $ fromIntegral (x :: Integer)
114+ | Right (x, "") <- double t = Number $ fromFloatDigits x
115+#else
116+ | Right (x, "") <- signed decimal t = Number $ I x
117+ | Right (x, "") <- double t = Number $ D x
118+#endif
119+ | otherwise = String t
120+ where x `isLike` ref = x `elem` [ref, T.toUpper ref, titleCased]
121+ where titleCased = toUpper (T.head ref) `T.cons` T.tail ref
122+
123+
124+parseO :: C.Sink Event Parse Value
125+parseO = do
126+ me <- CL.head
127+ case me of
128+ Just (EventScalar v tag style a) -> fmap (textToValue style tag) $ parseScalar v a style tag
129+ Just (EventSequenceStart a) -> parseS a id
130+ Just (EventMappingStart a) -> parseM a M.empty
131+ Just (EventAlias an) -> do
132+ m <- lift get
133+ case Map.lookup an m of
134+ Nothing -> liftIO $ throwIO $ UnknownAlias an
135+ Just v -> return v
136+ _ -> liftIO $ throwIO $ UnexpectedEvent me Nothing
137+
138+parseS :: Y.Anchor
139+ -> ([Value] -> [Value])
140+ -> C.Sink Event Parse Value
141+parseS a front = do
142+ me <- CL.peek
143+ case me of
144+ Just EventSequenceEnd -> do
145+ CL.drop 1
146+ let res = Array $ V.fromList $ front []
147+ case a of
148+ Nothing -> return res
149+ Just an -> do
150+ lift $ modify $ Map.insert an res
151+ return res
152+ _ -> do
153+ o <- parseO
154+ parseS a $ front . (:) o
155+
156+parseM :: Y.Anchor
157+ -> M.HashMap Text Value
158+ -> C.Sink Event Parse Value
159+parseM a front = do
160+ me <- CL.peek
161+ case me of
162+ Just EventMappingEnd -> do
163+ CL.drop 1
164+ let res = Object front
165+ case a of
166+ Nothing -> return res
167+ Just an -> do
168+ lift $ modify $ Map.insert an res
169+ return res
170+ _ -> do
171+ CL.drop 1
172+ s <- case me of
173+ Just (EventScalar v tag style a') -> parseScalar v a' style tag
174+ Just (EventAlias an) -> do
175+ m <- lift get
176+ case Map.lookup an m of
177+ Nothing -> liftIO $ throwIO $ UnknownAlias an
178+ Just (String t) -> return t
179+ Just v -> liftIO $ throwIO $ NonStringKeyAlias an v
180+ _ -> liftIO $ throwIO $ UnexpectedEvent me Nothing
181+ o <- parseO
182+
183+ let al = M.insert s o front
184+ al' = if s == pack "<<"
185+ then case o of
186+ Object l -> M.union front l
187+ Array l -> M.union front $ foldl merge' M.empty $ V.toList l
188+ _ -> al
189+ else al
190+ parseM a al'
191+ where merge' al (Object om) = M.union al om
192+ merge' al _ = al
193+
194+decodeHelper :: FromJSON a
195+ => C.Source Parse Y.Event
196+ -> IO (Either ParseException (Either String a))
197+decodeHelper src = do
198+ x <- tryAny $ runResourceT $ flip evalStateT Map.empty $ src C.$$ parse
199+ case x of
200+ Left e
201+ | Just pe <- fromException e -> return $ Left pe
202+ | Just ye <- fromException e -> return $ Left $ InvalidYaml $ Just (ye :: YamlException)
203+ | otherwise -> throwIO e
204+ Right y -> return $ Right $ parseEither parseJSON y
205+
206+decodeHelper_ :: FromJSON a
207+ => C.Source Parse Event
208+ -> IO (Either ParseException a)
209+decodeHelper_ src = do
210+ x <- tryAny $ runResourceT $ flip evalStateT Map.empty $ src C.$$ parse
211+ return $ case x of
212+ Left e
213+ | Just pe <- fromException e -> Left pe
214+ | Just ye <- fromException e -> Left $ InvalidYaml $ Just (ye :: YamlException)
215+ | otherwise -> Left $ OtherParseException e
216+ Right y -> either
217+ (Left . AesonException)
218+ Right
219+ (parseEither parseJSON y)
test/Data/Yaml/IncludeSpec.hs+7
@@ -2,8 +2,10 @@
22module Data.Yaml.IncludeSpec (main, spec) where
33
44import Test.Hspec
5+import Test.Hspec.Expectations.Contrib
56import Data.Aeson
67import Data.Aeson.QQ
8+import Data.Yaml (ParseException)
79
810import Data.Yaml.Include
911
@@ -35,3 +37,8 @@ spec = do
3537
3638 it "aborts on cyclic includes" $ do
3739 (decodeFile "test/resources/loop/foo.yaml" :: IO (Maybe Value)) `shouldThrow` anyException
40+
41+ describe "decodeFileEither" $ do
42+ context "when file does not exist" $ do
43+ it "returns Left" $ do
44+ (decodeFileEither "./does_not_exist.yaml" :: IO (Either ParseException Value)) >>= (`shouldSatisfy` isLeft)
test/Data/YamlSpec.hs+7−1
@@ -19,6 +19,7 @@ import System.Directory
1919import Control.Monad
2020import Control.Exception (try, SomeException)
2121import Test.Hspec
22+import Test.Hspec.Expectations.Contrib
2223
2324import qualified Data.Yaml as D
2425import Data.Yaml (object, array, (.=))
@@ -114,7 +115,7 @@ spec = do
114115 in D.decode (D.encode value') `shouldBe` Just value'
115116 mapM_ tester specialStrings
116117
117− describe "yamlFileJSONparse" $
118+ describe "decodeFileEither" $ do
118119 it "loads YAML through JSON into Haskell data" $ do
119120 tj <- either (error . show) id `fmap` D.decodeFileEither "test/json.yaml"
120121 tj `shouldBe` TestJSON
@@ -125,6 +126,11 @@ spec = do
125126 , extrastring = "1234-foo"
126127 }
127128
129+ context "when file does not exist" $ do
130+ it "returns Left" $ do
131+ (D.decodeFileEither "./does_not_exist.yaml" :: IO (Either D.ParseException D.Value)) >>= (`shouldSatisfy` isLeft)
132+
133+
128134 describe "round-tripping of special scalars" $ do
129135 let special = words "y Y On ON false 12345 12345.0 12345a 12e3"
130136 forM_ special $ \w -> it w $
yaml.cabal+4
@@ -52,12 +52,15 @@ library
5252 , scientific
5353 , filepath
5454 , directory
55+ , enclosed-exceptions
5556 exposed-modules: Text.Libyaml
5657 Data.Yaml
5758 Data.Yaml.Aeson
5859 Data.Yaml.Builder
5960 Data.Yaml.Parser
6061 Data.Yaml.Include
62+ other-modules:
63+ Data.Yaml.Internal
6164 ghc-options: -Wall
6265 c-sources: c/helper.c
6366 include-dirs: c
@@ -111,6 +114,7 @@ test-suite spec
111114 Data.Yaml.IncludeSpec
112115 cpp-options: -DTEST
113116 build-depends: hspec >= 1.3
117+ , hspec-expectations
114118 , HUnit
115119 , directory
116120 , base >= 4 && < 5