Skip to content

Commit 9c445b6

Browse files
parsonsmattdtpowl
andauthored
HandlerContents snippet (#1894)
* when showing HandlerContent, include some information from the Content * add PR link * change version number Co-authored-by: Matt Parsons <parsonsmatt@gmail.com> * truncate lazily and decode as UTF-8 * respect content type better when generating snippets * handle some rare encodings * doc string and style * remove unused imports * adopt style recommendations from PR review * rearrange some things to avoid a breaking change * fix build * hm * fix warn * weird but ok * warnclean * ok do it * Restore commentary * ok --------- Co-authored-by: Daniel Powell <dtpowl@gmail.com> Co-authored-by: Daniel Powell <3739511+dtpowl@users.noreply.github.com>
1 parent c7bca02 commit 9c445b6

File tree

15 files changed

+327
-68
lines changed

15 files changed

+327
-68
lines changed

stack-lts-22.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ extra-deps:
1919
- attoparsec-aeson-2.1.0.0
2020
- crypton-1.0.0
2121
- crypton-conduit-0.2.3
22+
- encoding-0.10.2

yesod-core/ChangeLog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# ChangeLog for yesod-core
22

3+
## 1.6.29.0
4+
5+
* When showing HandlerContent, include a content snippet [#1864](https://github.com/yesodweb/yesod/pull/1864)
6+
37
## 1.6.28.1
48

59
* Add type arguments to the sub routes [#1866](https://github.com/yesodweb/yesod/pull/1866)

yesod-core/src/Yesod/Core/Content.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ module Yesod.Core.Content
3232
-- * Utilities
3333
, simpleContentType
3434
, contentTypeTypes
35+
, typedContentToSnippet
3536
-- * Evaluation strategy
3637
, DontFullyEvaluate (..)
3738
-- * Representations
@@ -51,10 +52,12 @@ module Yesod.Core.Content
5152

5253
import qualified Data.ByteString as B
5354
import qualified Data.ByteString.Lazy as L
55+
import qualified Data.ByteString.Builder as BB
5456
import Data.Text.Lazy (Text, pack)
5557
import qualified Data.Text as T
5658
import Data.Text.Encoding (encodeUtf8Builder)
5759
import qualified Data.Text.Lazy as TL
60+
5861
import Data.ByteString.Builder (Builder, byteString, lazyByteString, stringUtf8)
5962
import Text.Hamlet (Html)
6063
import Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
@@ -69,8 +72,12 @@ import Data.Void (Void, absurd)
6972
import Yesod.Core.Types
7073
import Text.Lucius (Css, renderCss)
7174
import Text.Julius (Javascript, unJavascript)
75+
import qualified Network.Wai.Parse as NWP
76+
import qualified Data.Int as I
7277
import Data.Word8 (_semicolon, _slash)
7378
import Control.Arrow (second)
79+
import Control.Exception (Exception)
80+
import Data.Maybe
7481

7582
-- | Zero-length enumerator.
7683
emptyContent :: Content

yesod-core/src/Yesod/Core/TypedContent.hs

Whitespace-only changes.

yesod-core/src/Yesod/Core/Types.hs

Lines changed: 24 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1-
{-# LANGUAGE DeriveFunctor #-}
2-
{-# LANGUAGE DeriveGeneric #-}
3-
{-# LANGUAGE FlexibleInstances #-}
4-
{-# LANGUAGE GADTs #-}
5-
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6-
{-# LANGUAGE MultiParamTypeClasses #-}
7-
{-# LANGUAGE RankNTypes #-}
81
{-# LANGUAGE TypeFamilies #-}
92
{-# LANGUAGE TypeOperators #-}
103
{-# LANGUAGE UndecidableInstances #-}
4+
{-# LANGUAGE RankNTypes #-}
5+
{-# LANGUAGE GADTs #-}
6+
{-# LANGUAGE MultiParamTypeClasses #-}
7+
{-# LANGUAGE FlexibleInstances #-}
8+
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
9+
{-# LANGUAGE DeriveFunctor #-}
10+
11+
module Yesod.Core.Types (
12+
module Yesod.Core.Types
13+
, module Yesod.Core.Types.ErrorResponse
14+
, module Yesod.Core.Types.Content
15+
, module Yesod.Core.Types.TypedContent
16+
, module Yesod.Core.Types.HandlerContents
1117

12-
module Yesod.Core.Types where
18+
, module Yesod.Core.Internal.Util
19+
, module Yesod.Routes.Class
20+
, module Yesod.Core.TypeCache
21+
) where
1322

1423
import Data.Aeson (ToJSON)
1524
import qualified Data.ByteString.Builder as BB
@@ -34,9 +43,11 @@ import Data.Serialize (Serialize (..),
3443
import Data.String (IsString (fromString))
3544
import Data.Text (Text)
3645
import qualified Data.Text as T
46+
import Data.Text.Encoding
3747
import qualified Data.Text.Lazy.Builder as TBuilder
3848
import Data.Time (UTCTime)
3949
import GHC.Generics (Generic)
50+
import qualified GHC.Int as I
4051
import Language.Haskell.TH.Syntax (Loc)
4152
import qualified Network.HTTP.Types as H
4253
import Network.Wai (FilePart,
@@ -57,6 +68,11 @@ import Yesod.Core.TypeCache (TypeMap, KeyedTypeMap)
5768
import Control.Monad.Logger (MonadLoggerIO (..))
5869
import UnliftIO (MonadUnliftIO (..), SomeException)
5970

71+
import Yesod.Core.Types.ErrorResponse
72+
import Yesod.Core.Types.Content
73+
import Yesod.Core.Types.TypedContent
74+
import Yesod.Core.Types.HandlerContents
75+
6076
-- Sessions
6177
type SessionMap = Map Text ByteString
6278

@@ -297,21 +313,12 @@ data PageContent url = PageContent
297313
, pageBody :: !(HtmlUrl url)
298314
}
299315

300-
data Content = ContentBuilder !BB.Builder !(Maybe Int) -- ^ The content and optional content length.
301-
| ContentSource !(ConduitT () (Flush BB.Builder) (ResourceT IO) ())
302-
| ContentFile !FilePath !(Maybe FilePart)
303-
| ContentDontEvaluate !Content
304-
305-
data TypedContent = TypedContent !ContentType !Content
306-
307316
type RepHtml = Html
308317
{-# DEPRECATED RepHtml "Please use Html instead" #-}
309318
newtype RepJson = RepJson Content
310319
newtype RepPlain = RepPlain Content
311320
newtype RepXml = RepXml Content
312321

313-
type ContentType = ByteString -- FIXME Text?
314-
315322
-- | Wrapper around types so that Handlers can return a domain type, even when
316323
-- the data will eventually be encoded as JSON.
317324
-- Example usage in a type signature:
@@ -332,34 +339,6 @@ data JSONResponse a where
332339
-- Since 1.1.0
333340
newtype DontFullyEvaluate a = DontFullyEvaluate { unDontFullyEvaluate :: a }
334341

335-
-- | Responses to indicate some form of an error occurred.
336-
data ErrorResponse =
337-
NotFound
338-
-- ^ The requested resource was not found.
339-
-- Examples of when this occurs include when an incorrect URL is used, or @yesod-persistent@'s 'get404' doesn't find a value.
340-
-- HTTP status: 404.
341-
| InternalError !Text
342-
-- ^ Some sort of unexpected exception.
343-
-- If your application uses `throwIO` or `error` to throw an exception, this is the form it would take.
344-
-- HTTP status: 500.
345-
| InvalidArgs ![Text]
346-
-- ^ Indicates some sort of invalid or missing argument, like a missing query parameter or malformed JSON body.
347-
-- Examples Yesod functions that send this include 'requireCheckJsonBody' and @Yesod.Auth.GoogleEmail2@.
348-
-- HTTP status: 400.
349-
| NotAuthenticated
350-
-- ^ Indicates the user is not logged in.
351-
-- This is thrown when 'isAuthorized' returns 'AuthenticationRequired'.
352-
-- HTTP code: 401.
353-
| PermissionDenied !Text
354-
-- ^ Indicates the user doesn't have permission to access the requested resource.
355-
-- This is thrown when 'isAuthorized' returns 'Unauthorized'.
356-
-- HTTP code: 403.
357-
| BadMethod !H.Method
358-
-- ^ Indicates the URL would have been valid if used with a different HTTP method (e.g. a GET was used, but only POST is handled.)
359-
-- HTTP code: 405.
360-
deriving (Show, Eq, Generic)
361-
instance NFData ErrorResponse
362-
363342
----- header stuff
364343
-- | Headers to be added to a 'Result'.
365344
data Header =
@@ -423,25 +402,6 @@ instance Semigroup (GWData a) where
423402
(mappend a7 b7)
424403
(mappend a8 b8)
425404

426-
data HandlerContents =
427-
HCContent !H.Status !TypedContent
428-
| HCError !ErrorResponse
429-
| HCSendFile !ContentType !FilePath !(Maybe FilePart)
430-
| HCRedirect !H.Status !Text
431-
| HCCreated !Text
432-
| HCWai !W.Response
433-
| HCWaiApp !W.Application
434-
435-
instance Show HandlerContents where
436-
show (HCContent status (TypedContent t _)) = "HCContent " ++ show (status, t)
437-
show (HCError e) = "HCError " ++ show e
438-
show (HCSendFile ct fp mfp) = "HCSendFile " ++ show (ct, fp, mfp)
439-
show (HCRedirect s t) = "HCRedirect " ++ show (s, t)
440-
show (HCCreated t) = "HCCreated " ++ show t
441-
show (HCWai _) = "HCWai"
442-
show (HCWaiApp _) = "HCWaiApp"
443-
instance Exception HandlerContents
444-
445405
-- Instances for WidgetFor
446406
instance Applicative (WidgetFor site) where
447407
pure = WidgetFor . const . pure
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module Yesod.Core.Types.Content where
2+
3+
import qualified Data.ByteString.Builder as BB
4+
import Control.Monad.Trans.Resource (ResourceT)
5+
import Data.Conduit (Flush, ConduitT)
6+
import Network.Wai (FilePart)
7+
8+
data Content
9+
= ContentBuilder !BB.Builder !(Maybe Int)
10+
-- ^ The content and optional content length.
11+
--
12+
-- Note that, despite @Builder@'s laziness, this is entirely forced
13+
-- into memory by default in order to catch imprecise exceptions
14+
-- before beginning to respond. If you are confident you don't have
15+
-- imprecise exceptions, you may disable this by wrapping the
16+
-- `ToContent` data in `DontFullyEvaluate`.-
17+
| ContentSource !(ConduitT () (Flush BB.Builder) (ResourceT IO) ())
18+
| ContentFile !FilePath !(Maybe FilePart)
19+
| ContentDontEvaluate !Content
20+
-- ^ Used internally to wrap @ContentBuilder@s to disable forcing
21+
-- them. No effect on other @Content@.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{-# LANGUAGE DeriveGeneric #-}
2+
3+
module Yesod.Core.Types.ErrorResponse where
4+
5+
import GHC.Generics (Generic)
6+
import Data.Text
7+
import Control.DeepSeq (NFData)
8+
import qualified Network.HTTP.Types as H
9+
10+
-- | Responses to indicate some form of an error occurred.
11+
data ErrorResponse =
12+
NotFound
13+
-- ^ The requested resource was not found.
14+
-- Examples of when this occurs include when an incorrect URL is used, or @yesod-persistent@'s 'get404' doesn't find a value.
15+
-- HTTP status: 404.
16+
| InternalError !Text
17+
-- ^ Some sort of unexpected exception.
18+
-- If your application uses `throwIO` or `error` to throw an exception, this is the form it would take.
19+
-- HTTP status: 500.
20+
| InvalidArgs ![Text]
21+
-- ^ Indicates some sort of invalid or missing argument, like a missing query parameter or malformed JSON body.
22+
-- Examples Yesod functions that send this include 'requireCheckJsonBody' and @Yesod.Auth.GoogleEmail2@.
23+
-- HTTP status: 400.
24+
| NotAuthenticated
25+
-- ^ Indicates the user is not logged in.
26+
-- This is thrown when 'isAuthorized' returns 'AuthenticationRequired'.
27+
-- HTTP code: 401.
28+
| PermissionDenied !Text
29+
-- ^ Indicates the user doesn't have permission to access the requested resource.
30+
-- This is thrown when 'isAuthorized' returns 'Unauthorized'.
31+
-- HTTP code: 403.
32+
| BadMethod !H.Method
33+
-- ^ Indicates the URL would have been valid if used with a different HTTP method (e.g. a GET was used, but only POST is handled.)
34+
-- HTTP code: 405.
35+
deriving (Show, Eq, Generic)
36+
37+
instance NFData ErrorResponse
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module Yesod.Core.Types.HandlerContents
2+
(
3+
HandlerContents (..)
4+
) where
5+
6+
import Control.Exception (Exception)
7+
import Data.Maybe
8+
import Data.Text
9+
import qualified Data.Text.Lazy as TL
10+
import qualified Network.HTTP.Types as H
11+
import qualified Network.Wai as W
12+
import Yesod.Core.Types.ErrorResponse
13+
import Yesod.Core.Types.TypedContent (ContentType, TypedContent (..), typedContentToSnippet)
14+
15+
data HandlerContents =
16+
HCContent !H.Status !TypedContent
17+
| HCError !ErrorResponse
18+
| HCSendFile !ContentType !FilePath !(Maybe W.FilePart)
19+
| HCRedirect !H.Status !Text
20+
| HCCreated !Text
21+
| HCWai !W.Response
22+
| HCWaiApp !W.Application
23+
24+
instance Show HandlerContents where
25+
show (HCContent status tc@(TypedContent t _))
26+
= mconcat [ "HCContent "
27+
, show (status, t)
28+
, " ("
29+
, fromMaybe "" $ TL.unpack <$> typedContentToSnippet tc 1000
30+
, ")"
31+
]
32+
show (HCError e) = "HCError " ++ show e
33+
show (HCSendFile ct fp mfp) = "HCSendFile " ++ show (ct, fp, mfp)
34+
show (HCRedirect s t) = "HCRedirect " ++ show (s, t)
35+
show (HCCreated t) = "HCCreated " ++ show t
36+
show (HCWai _) = "HCWai"
37+
show (HCWaiApp _) = "HCWaiApp"
38+
39+
instance Exception HandlerContents
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
4+
module Yesod.Core.Types.TypedContent
5+
(
6+
ContentType
7+
, TypedContent (..)
8+
, typedContentToSnippet
9+
) where
10+
11+
import Control.Applicative ((<|>))
12+
import Control.Monad (void, guard)
13+
import Data.Maybe (fromMaybe)
14+
15+
import qualified Data.ByteString as B
16+
import qualified Data.ByteString.Lazy as L
17+
import qualified Data.ByteString.Builder as BB
18+
19+
import qualified Data.Int as I
20+
21+
#if MIN_VERSION_text(2,1,0)
22+
import qualified Data.Text.Encoding as TE (decodeASCIIPrefix)
23+
#else
24+
import qualified Data.Text.Encoding as TE (decodeLatin1)
25+
#endif
26+
import qualified Data.Text.Lazy as TL
27+
import qualified Data.Text.Lazy.Encoding as LE (decodeUtf8With, decodeLatin1)
28+
import qualified Data.Text.Encoding.Error as EE (lenientDecode)
29+
30+
import qualified Data.Encoding as Enc
31+
import qualified Data.Encoding.GB18030 as Enc
32+
import qualified Data.Encoding.CP1251 as Enc
33+
import qualified Data.Encoding.ShiftJIS as Enc
34+
import qualified Data.Encoding.CP932 as Enc
35+
36+
import qualified Network.Wai.Parse as NWP
37+
38+
import Yesod.Core.Types.Content (Content (..))
39+
40+
type ContentType = B.ByteString -- FIXME Text?
41+
data TypedContent = TypedContent !ContentType !Content
42+
43+
decoderForCharset :: Maybe B.ByteString -> L.ByteString -> TL.Text
44+
decoderForCharset (Just encodingSymbol)
45+
| encodingSymbol == "utf-8" =
46+
LE.decodeUtf8With EE.lenientDecode
47+
| encodingSymbol == "US-ASCII" =
48+
#if MIN_VERSION_text(2,1,0)
49+
TL.fromStrict . fst . TE.decodeASCIIPrefix . L.toStrict
50+
#else
51+
TL.fromStrict . TE.decodeLatin1 . L.toStrict
52+
#endif
53+
| encodingSymbol == "latin1" =
54+
LE.decodeLatin1
55+
| encodingSymbol == "GB18030" =
56+
TL.pack . Enc.decodeLazyByteString Enc.GB18030
57+
| encodingSymbol == "windows-1251" =
58+
TL.pack . Enc.decodeLazyByteString Enc.CP1251
59+
| encodingSymbol == "Shift_JIS" =
60+
TL.pack . Enc.decodeLazyByteString Enc.ShiftJIS
61+
| encodingSymbol == "Windows-31J" =
62+
TL.pack . Enc.decodeLazyByteString Enc.CP932
63+
| otherwise =
64+
LE.decodeUtf8With EE.lenientDecode
65+
decoderForCharset Nothing = LE.decodeUtf8With EE.lenientDecode
66+
67+
decodeForContentType :: ContentType -> L.ByteString -> Maybe TL.Text
68+
decodeForContentType ct bytes = do
69+
let (t, params) =
70+
NWP.parseContentType ct
71+
charset =
72+
lookup "charset" params
73+
typeIsText =
74+
B.isPrefixOf "text" t
75+
|| B.isPrefixOf "application/json" t
76+
|| B.isPrefixOf "application/rss" t
77+
|| B.isPrefixOf "application/atom" t
78+
decoder = decoderForCharset charset
79+
void charset <|> guard typeIsText
80+
pure $ decoder bytes
81+
82+
contentToSnippet :: Content -> I.Int64 -> Maybe L.ByteString
83+
contentToSnippet (ContentBuilder builder maybeLength) maxLength =
84+
pure $ truncatedText <> excessLengthMsg
85+
where
86+
truncatedText = L.take maxLength $ BB.toLazyByteString builder
87+
excessLength = fromMaybe 0 $ (subtract $ fromIntegral maxLength) <$> maybeLength
88+
excessLengthMsg = case excessLength > 0 of
89+
False -> ""
90+
True -> "...+ " <> BB.toLazyByteString (BB.intDec excessLength)
91+
contentToSnippet (ContentSource _) _ = Nothing
92+
contentToSnippet (ContentFile _ _) _ = Nothing
93+
contentToSnippet (ContentDontEvaluate _) _ = Nothing
94+
95+
-- | Represents TypedContent as a String, rendering at most a specified number of
96+
-- bytes of the content, and annotating it with the remaining length. Returns Nothing
97+
-- if the content type indicates the content is binary data.
98+
--
99+
-- @since 1.6.28.0
100+
typedContentToSnippet :: TypedContent -> I.Int64 -> Maybe TL.Text
101+
typedContentToSnippet (TypedContent t c) maxLength = decodeForContentType t =<< contentToSnippet c maxLength

0 commit comments

Comments
 (0)