git.iko.soy/typeable/servant
on master help sign in
Use lenient decoding for JSON.
1a200f1Julian K. Arni
servant.cabal+2
@@ -45,6 +45,7 @@ library
45 build-depends:
45 build-depends:
46 base >=4.7 && <5
46 base >=4.7 && <5
47 , aeson >= 0.7
47 , aeson >= 0.7
48+ , attoparsec >= 0.12
48 , bytestring == 0.10.*
49 , bytestring == 0.10.*
49 , http-media >= 0.4 && < 0.7
50 , http-media >= 0.4 && < 0.7
50 , http-types == 0.8.*
51 , http-types == 0.8.*
@@ -86,6 +87,7 @@ test-suite spec
86 build-depends:
87 build-depends:
87 base == 4.*
88 base == 4.*
88 , aeson
89 , aeson
90+ , attoparsec
89 , bytestring
91 , bytestring
90 , hspec == 2.*
92 , hspec == 2.*
91 , QuickCheck
93 , QuickCheck
src/Servant/API/ContentTypes.hs+31−16
@@ -11,24 +11,28 @@
11{-# LANGUAGE UndecidableInstances #-}
11{-# LANGUAGE UndecidableInstances #-}
12module Servant.API.ContentTypes where
12module Servant.API.ContentTypes where
13
13
14−import Control.Arrow (left)
14+import Control.Applicative ((<*))
15+import Control.Arrow (left)
15import Control.Monad
16import Control.Monad
16−import Data.Aeson (FromJSON, ToJSON, eitherDecode,
17+import Data.Aeson (FromJSON, ToJSON, Value,
17− encode)
18+ encode, parseJSON)
19+import Data.Aeson.Parser (value)
20+import Data.Aeson.Types (parseEither)
21+import Data.Attoparsec.ByteString (endOfInput, parseOnly)
18−import qualified Data.ByteString as BS
22+import qualified Data.ByteString as BS
19−import Data.ByteString.Lazy (ByteString, fromStrict, toStrict)
23+import Data.ByteString.Lazy (ByteString, fromStrict, toStrict)
20−import qualified Data.ByteString.Lazy as B
24+import qualified Data.ByteString.Lazy as B
21−import Data.String.Conversions (cs)
22import Data.Monoid
25import Data.Monoid
26+import Data.String.Conversions (cs)
23−import qualified Data.Text.Lazy as TextL
27+import qualified Data.Text as TextS
24−import qualified Data.Text.Lazy.Encoding as TextL
28+import qualified Data.Text.Encoding as TextS
25−import qualified Data.Text as TextS
29+import qualified Data.Text.Lazy as TextL
26−import qualified Data.Text.Encoding as TextS
30+import qualified Data.Text.Lazy.Encoding as TextL
27import Data.Typeable
31import Data.Typeable
28−import GHC.Exts (Constraint)
32+import GHC.Exts (Constraint)
29−import qualified Network.HTTP.Media as M
33+import qualified Network.HTTP.Media as M
30−import Network.URI (unEscapeString, escapeURIString,
34+import Network.URI (escapeURIString, isUnreserved,
31− isUnreserved)
35+ unEscapeString)
32
36
33-- * Provided content types
37-- * Provided content types
34data JSON deriving Typeable
38data JSON deriving Typeable
@@ -190,6 +194,8 @@ instance ToJSON a => MimeRender JSON a where
190 toByteString _ = encode
194 toByteString _ = encode
191
195
192-- | `encodeFormUrlEncoded . toFormUrlEncoded`
196-- | `encodeFormUrlEncoded . toFormUrlEncoded`
197+-- Note that the `fromByteString p (toByteString p x) == Right x` law only
198+-- holds if every element of x is non-null (i.e., not `("", "")`)
193instance ToFormUrlEncoded a => MimeRender FormUrlEncoded a where
199instance ToFormUrlEncoded a => MimeRender FormUrlEncoded a where
194 toByteString _ = encodeFormUrlEncoded . toFormUrlEncoded
200 toByteString _ = encodeFormUrlEncoded . toFormUrlEncoded
195
201
@@ -213,11 +219,20 @@ instance MimeRender OctetStream BS.ByteString where
213--------------------------------------------------------------------------
219--------------------------------------------------------------------------
214-- * MimeUnrender Instances
220-- * MimeUnrender Instances
215
221
222+-- | Like 'Data.Aeson.eitherDecode' but allows all JSON values instead of just
223+-- objects and arrays.
224+eitherDecodeLenient :: FromJSON a => ByteString -> Either String a
225+eitherDecodeLenient input = do
226+ v :: Value <- parseOnly (Data.Aeson.Parser.value <* endOfInput) (cs input)
227+ parseEither parseJSON v
228+
216-- | `eitherDecode`
229-- | `eitherDecode`
217instance FromJSON a => MimeUnrender JSON a where
230instance FromJSON a => MimeUnrender JSON a where
218− fromByteString _ = eitherDecode
231+ fromByteString _ = eitherDecodeLenient
219
232
220-- | `decodeFormUrlEncoded >=> fromFormUrlEncoded`
233-- | `decodeFormUrlEncoded >=> fromFormUrlEncoded`
234+-- Note that the `fromByteString p (toByteString p x) == Right x` law only
235+-- holds if every element of x is non-null (i.e., not `("", "")`)
221instance FromFormUrlEncoded a => MimeUnrender FormUrlEncoded a where
236instance FromFormUrlEncoded a => MimeUnrender FormUrlEncoded a where
222 fromByteString _ = decodeFormUrlEncoded >=> fromFormUrlEncoded
237 fromByteString _ = decodeFormUrlEncoded >=> fromFormUrlEncoded
223
238
test/Servant/API/ContentTypesSpec.hs+30−14
@@ -8,22 +8,24 @@ module Servant.API.ContentTypesSpec where
8import Control.Applicative
8import Control.Applicative
9import Control.Arrow
9import Control.Arrow
10import Data.Aeson
10import Data.Aeson
11+import Data.Aeson.Parser (jstring)
12+import Data.Attoparsec.ByteString (parseOnly)
11−import Data.Function (on)
13+import Data.Function (on)
12import Data.Proxy
14import Data.Proxy
13
15
14−import Data.ByteString.Char8 (ByteString, append, pack)
16+import Data.ByteString.Char8 (ByteString, append, pack)
15−import qualified Data.ByteString.Lazy as BSL
17+import qualified Data.ByteString.Lazy as BSL
16−import Data.List (maximumBy)
18+import Data.List (maximumBy)
17−import Data.Maybe (fromJust, isJust, isNothing)
19+import Data.Maybe (fromJust, isJust, isNothing)
18−import Data.String (IsString (..))
20+import Data.String (IsString (..))
19−import Data.String.Conversions (cs)
21+import Data.String.Conversions (cs)
20−import qualified Data.Text as TextS
22+import qualified Data.Text as TextS
21−import qualified Data.Text.Lazy as TextL
23+import qualified Data.Text.Lazy as TextL
22import GHC.Generics
24import GHC.Generics
23−import Network.URL (importParams, exportParams)
25+import Network.URL (exportParams, importParams)
24import Test.Hspec
26import Test.Hspec
25import Test.QuickCheck
27import Test.QuickCheck
26−import Test.QuickCheck.Instances ()
28+import Test.QuickCheck.Instances ()
27
29
28import Servant.API.ContentTypes
30import Servant.API.ContentTypes
29
31
@@ -42,17 +44,23 @@ spec = describe "Servant.API.ContentTypes" $ do
42
44
43 describe "The FormUrlEncoded Content-Type type" $ do
45 describe "The FormUrlEncoded Content-Type type" $ do
44
46
47+ let isNonNull ("", "") = False
48+ isNonNull _ = True
49+
45 it "has fromByteString reverse toByteString" $ do
50 it "has fromByteString reverse toByteString" $ do
46 let p = Proxy :: Proxy FormUrlEncoded
51 let p = Proxy :: Proxy FormUrlEncoded
52+ property $ \x -> all isNonNull x
47− property $ \x -> fromByteString p (toByteString p x) == Right (x::[(TextS.Text,TextS.Text)])
53+ ==> fromByteString p (toByteString p x) == Right (x::[(TextS.Text,TextS.Text)])
48
54
49 it "has fromByteString reverse exportParams (Network.URL)" $ do
55 it "has fromByteString reverse exportParams (Network.URL)" $ do
50 let p = Proxy :: Proxy FormUrlEncoded
56 let p = Proxy :: Proxy FormUrlEncoded
57+ property $ \x -> all isNonNull x
51− property $ \x -> (fromByteString p . cs . exportParams . map (cs *** cs) $ x) == Right (x::[(TextS.Text,TextS.Text)])
58+ ==> (fromByteString p . cs . exportParams . map (cs *** cs) $ x) == Right (x::[(TextS.Text,TextS.Text)])
52
59
53 it "has importParams (Network.URL) reverse toByteString" $ do
60 it "has importParams (Network.URL) reverse toByteString" $ do
54 let p = Proxy :: Proxy FormUrlEncoded
61 let p = Proxy :: Proxy FormUrlEncoded
62+ property $ \x -> all isNonNull x
55− property $ \x -> (fmap (map (cs *** cs)) . importParams . cs . toByteString p $ x) == Just (x::[(TextS.Text,TextS.Text)])
63+ ==> (fmap (map (cs *** cs)) . importParams . cs . toByteString p $ x) == Just (x::[(TextS.Text,TextS.Text)])
56
64
57 describe "The PlainText Content-Type type" $ do
65 describe "The PlainText Content-Type type" $ do
58
66
@@ -148,6 +156,14 @@ spec = describe "Servant.API.ContentTypes" $ do
148 (encode val)
156 (encode val)
149 `shouldBe` Just (Right val)
157 `shouldBe` Just (Right val)
150
158
159+ describe "eitherDecodeLenient" $ do
160+
161+ it "parses top-level strings" $ do
162+ let toMaybe = either (const Nothing) Just
163+ -- The Left messages differ, so convert to Maybe
164+ property $ \x -> toMaybe (eitherDecodeLenient x)
165+ `shouldBe` toMaybe (parseOnly jstring $ cs x)
166+
151
167
152data SomeData = SomeData { record1 :: String, record2 :: Int }
168data SomeData = SomeData { record1 :: String, record2 :: Int }
153 deriving (Generic, Eq, Show)
169 deriving (Generic, Eq, Show)