servant: allow whitespace after parsing JSON

This includes some de-duplication in the test suite.
This commit is contained in:
Christian Marie 2015-05-16 11:11:38 +10:00
parent 955261ddd3
commit 43d0620403
2 changed files with 35 additions and 21 deletions

View file

@ -70,11 +70,12 @@ import Control.Applicative ((<*))
#endif #endif
import Control.Arrow (left) import Control.Arrow (left)
import Control.Monad import Control.Monad
import Data.Aeson (FromJSON, ToJSON, Value, import Data.Aeson (FromJSON, ToJSON, encode,
encode, parseJSON) parseJSON)
import Data.Aeson.Parser (value) import Data.Aeson.Parser (value)
import Data.Aeson.Types (parseEither) import Data.Aeson.Types (parseEither)
import Data.Attoparsec.ByteString (endOfInput, parseOnly) import Data.Attoparsec.ByteString.Char8 (endOfInput, parseOnly,
skipSpace, (<?>))
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Data.ByteString.Lazy (ByteString, fromStrict, toStrict) import Data.ByteString.Lazy (ByteString, fromStrict, toStrict)
import qualified Data.ByteString.Lazy as B import qualified Data.ByteString.Lazy as B
@ -291,10 +292,22 @@ instance MimeRender OctetStream BS.ByteString where
-- | Like 'Data.Aeson.eitherDecode' but allows all JSON values instead of just -- | Like 'Data.Aeson.eitherDecode' but allows all JSON values instead of just
-- objects and arrays. -- objects and arrays.
--
-- Will handle trailing whitespace, but not trailing junk. ie.
--
-- >>> eitherDecodeLenient "1 " :: Either String Int
-- Right 1
--
-- >>> eitherDecodeLenient "1 junk" :: Either String Int
-- Left "trailing junk after valid JSON: endOfInput"
eitherDecodeLenient :: FromJSON a => ByteString -> Either String a eitherDecodeLenient :: FromJSON a => ByteString -> Either String a
eitherDecodeLenient input = do eitherDecodeLenient input =
v :: Value <- parseOnly (Data.Aeson.Parser.value <* endOfInput) (cs input) parseOnly parser (cs input) >>= parseEither parseJSON
parseEither parseJSON v where
parser =
Data.Aeson.Parser.value
<* skipSpace
<* (endOfInput <?> "trailing junk after valid JSON")
-- | `eitherDecode` -- | `eitherDecode`
instance FromJSON a => MimeUnrender JSON a where instance FromJSON a => MimeUnrender JSON a where

View file

@ -10,6 +10,7 @@ module Servant.API.ContentTypesSpec where
import Control.Applicative import Control.Applicative
#endif #endif
import Control.Arrow import Control.Arrow
import Data.Either
import Data.Aeson import Data.Aeson
import Data.Aeson.Parser (jstring) import Data.Aeson.Parser (jstring)
import Data.Attoparsec.ByteString (parseOnly) import Data.Attoparsec.ByteString (parseOnly)
@ -25,6 +26,7 @@ import Data.String.Conversions (cs)
import qualified Data.Text as TextS import qualified Data.Text as TextS
import qualified Data.Text.Lazy as TextL import qualified Data.Text.Lazy as TextL
import GHC.Generics import GHC.Generics
import Data.Monoid
import Network.URL (exportParams, importParams) import Network.URL (exportParams, importParams)
import Test.Hspec import Test.Hspec
import Test.QuickCheck import Test.QuickCheck
@ -36,54 +38,53 @@ spec :: Spec
spec = describe "Servant.API.ContentTypes" $ do spec = describe "Servant.API.ContentTypes" $ do
describe "The JSON Content-Type type" $ do describe "The JSON Content-Type type" $ do
let p = Proxy :: Proxy JSON
it "handles whitespace at end of input" $ do
mimeUnrender p "[1] " `shouldBe` Right [1 :: Int]
it "does not like junk at end of input" $ do
mimeUnrender p "[1] this probably shouldn't work"
`shouldSatisfy` (isLeft :: Either a [Int] -> Bool)
it "has mimeUnrender reverse mimeRender for valid top-level json ([Int]) " $ do it "has mimeUnrender reverse mimeRender for valid top-level json ([Int]) " $ do
let p = Proxy :: Proxy JSON
property $ \x -> mimeUnrender p (mimeRender p x) == Right (x::[Int]) property $ \x -> mimeUnrender p (mimeRender p x) == Right (x::[Int])
it "has mimeUnrender reverse mimeRender for valid top-level json " $ do it "has mimeUnrender reverse mimeRender for valid top-level json " $ do
let p = Proxy :: Proxy JSON
property $ \x -> mimeUnrender p (mimeRender p x) == Right (x::SomeData) property $ \x -> mimeUnrender p (mimeRender p x) == Right (x::SomeData)
describe "The FormUrlEncoded Content-Type type" $ do describe "The FormUrlEncoded Content-Type type" $ do
let p = Proxy :: Proxy FormUrlEncoded
let isNonNull ("", "") = False
isNonNull _ = True
it "has mimeUnrender reverse mimeRender" $ do it "has mimeUnrender reverse mimeRender" $ do
let p = Proxy :: Proxy FormUrlEncoded property $ \x -> all (/= mempty) x
property $ \x -> all isNonNull x
==> mimeUnrender p (mimeRender p x) == Right (x::[(TextS.Text,TextS.Text)]) ==> mimeUnrender p (mimeRender p x) == Right (x::[(TextS.Text,TextS.Text)])
it "has mimeUnrender reverse exportParams (Network.URL)" $ do it "has mimeUnrender reverse exportParams (Network.URL)" $ do
let p = Proxy :: Proxy FormUrlEncoded property $ \x -> all (/= mempty) x
property $ \x -> all isNonNull x
==> (mimeUnrender p . cs . exportParams . map (cs *** cs) $ x) == Right (x::[(TextS.Text,TextS.Text)]) ==> (mimeUnrender p . cs . exportParams . map (cs *** cs) $ x) == Right (x::[(TextS.Text,TextS.Text)])
it "has importParams (Network.URL) reverse mimeRender" $ do it "has importParams (Network.URL) reverse mimeRender" $ do
let p = Proxy :: Proxy FormUrlEncoded property $ \x -> all (/= mempty) x
property $ \x -> all isNonNull x
==> (fmap (map (cs *** cs)) . importParams . cs . mimeRender p $ x) == Just (x::[(TextS.Text,TextS.Text)]) ==> (fmap (map (cs *** cs)) . importParams . cs . mimeRender p $ x) == Just (x::[(TextS.Text,TextS.Text)])
describe "The PlainText Content-Type type" $ do describe "The PlainText Content-Type type" $ do
let p = Proxy :: Proxy PlainText
it "has mimeUnrender reverse mimeRender (lazy Text)" $ do it "has mimeUnrender reverse mimeRender (lazy Text)" $ do
let p = Proxy :: Proxy PlainText
property $ \x -> mimeUnrender p (mimeRender p x) == Right (x::TextL.Text) property $ \x -> mimeUnrender p (mimeRender p x) == Right (x::TextL.Text)
it "has mimeUnrender reverse mimeRender (strict Text)" $ do it "has mimeUnrender reverse mimeRender (strict Text)" $ do
let p = Proxy :: Proxy PlainText
property $ \x -> mimeUnrender p (mimeRender p x) == Right (x::TextS.Text) property $ \x -> mimeUnrender p (mimeRender p x) == Right (x::TextS.Text)
describe "The OctetStream Content-Type type" $ do describe "The OctetStream Content-Type type" $ do
let p = Proxy :: Proxy OctetStream
it "is id (Lazy ByteString)" $ do it "is id (Lazy ByteString)" $ do
let p = Proxy :: Proxy OctetStream
property $ \x -> mimeRender p x == (x :: BSL.ByteString) property $ \x -> mimeRender p x == (x :: BSL.ByteString)
&& mimeUnrender p x == Right x && mimeUnrender p x == Right x
it "is fromStrict/toStrict (Strict ByteString)" $ do it "is fromStrict/toStrict (Strict ByteString)" $ do
let p = Proxy :: Proxy OctetStream
property $ \x -> mimeRender p x == BSL.fromStrict (x :: ByteString) property $ \x -> mimeRender p x == BSL.fromStrict (x :: ByteString)
&& mimeUnrender p (BSL.fromStrict x) == Right x && mimeUnrender p (BSL.fromStrict x) == Right x