Skip to content

Conversation

@dtpowl
Copy link

@dtpowl dtpowl commented Mar 20, 2025

When possible, shows a snippet of the underlying Content when showing (or displayExceptioning) an HCContent.


Before submitting your PR, check that you've:

  • Bumped the version number

After submitting your PR:

  • Update the Changelog.md file with a link to your PR
  • Check that CI passes (or if it fails, for reasons unrelated to your change, like CI timeouts)

Comment on lines 309 to 321
contentToTruncatedString :: Content -> Int -> String
contentToTruncatedString (ContentBuilder builder maybeLength) maxLength =
let
truncated = take maxLength (show builder)
excess = case maybeLength of
(Just length) -> length - maxLength
Nothing -> 0
in case (excess > 0) of
True -> truncated ++ "... (+" ++ show excess ++ ")"
False -> truncated
contentToTruncatedString (ContentSource _) _ = "ContentSource"
contentToTruncatedString (ContentFile _ _) _ = "ContentFile"
contentToTruncatedString (ContentDontEvaluate _) _ = "ContentDontEvaluate"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is exported. Can we add a doc string with @SInCE annotation?

Suggested change
contentToTruncatedString :: Content -> Int -> String
contentToTruncatedString (ContentBuilder builder maybeLength) maxLength =
let
truncated = take maxLength (show builder)
excess = case maybeLength of
(Just length) -> length - maxLength
Nothing -> 0
in case (excess > 0) of
True -> truncated ++ "... (+" ++ show excess ++ ")"
False -> truncated
contentToTruncatedString (ContentSource _) _ = "ContentSource"
contentToTruncatedString (ContentFile _ _) _ = "ContentFile"
contentToTruncatedString (ContentDontEvaluate _) _ = "ContentDontEvaluate"
-- | doc me up boss
--
-- @since 1.6.28.0
contentToTruncatedString :: Content -> Int -> String
contentToTruncatedString (ContentBuilder builder maybeLength) maxLength =
let
truncated = take maxLength (show builder)
excess = case maybeLength of
(Just length) -> length - maxLength
Nothing -> 0
in case (excess > 0) of
True -> truncated ++ "... (+" ++ show excess ++ ")"
False -> truncated
contentToTruncatedString (ContentSource _) _ = "ContentSource"
contentToTruncatedString (ContentFile _ _) _ = "ContentFile"
contentToTruncatedString (ContentDontEvaluate _) _ = "ContentDontEvaluate"

contentToTruncatedString :: Content -> Int -> String
contentToTruncatedString (ContentBuilder builder maybeLength) maxLength =
let
truncated = take maxLength (show builder)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm. show is probably not what we want here, but it may be. Calling show on a string-ish type usually will add quotes around it, so it can be copy/pasted into Haskell code.

λ> import Data.ByteString.Builder
λ> show ("hello" :: Builder)
"\"hello\""
λ> print ("hello" :: Builder)
"hello"
λ> putStrLn "hello"
hello
λ> print ("hello" :: String)
"hello"

(print is putStrLn . show)

Likewise, show "\n" == "\"\\n\""

But, what we have is a bunch of bytes. Do we interpret them as ASCII and do Data.ByteString.Char8.unpack :: ByteString -> String? Or do we treat them as UTF8 and do Data.Text.Encoding.decodeUtf8 ?

Copy link
Author

@dtpowl dtpowl Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we were to accept TypedContent here instead of Content, we'd be able to infer the encoding. If it's ASCII or UTF-8 or some other text encoding, the right thing to do will be obvious. And if the content type tells us it's not text at all, we'll just refrain from generating a snippet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great call

Co-authored-by: Matt Parsons <parsonsmatt@gmail.com>
@dtpowl dtpowl force-pushed the show-truncated-content-in-handler-content branch 2 times, most recently from cafe865 to a81cb21 Compare March 20, 2025 22:19
@dtpowl dtpowl force-pushed the show-truncated-content-in-handler-content branch from a81cb21 to 0d907ea Compare March 20, 2025 22:42
excess = case maybeLength of
(Just length) -> length - (fromIntegral maxLength)
Nothing -> 0
in case (excess > 0) of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just my curiosity, is it a code formatter that surrounds Just length, excess > 0 and fromIntegral maxLength with parens or is it your personal preference?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's less a preference and more a habit inherited from working in other languages; I'm pretty new to Haskell.

I don't really think the parens aid legibility in the examples you pointed out, so I'll probably remove them.

Comment on lines 468 to 471
= mconcat [ "HCContent "
, show (status, t)
, contentToTruncatedString c 1000
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe want to parens + space it

Suggested change
= mconcat [ "HCContent "
, show (status, t)
, contentToTruncatedString c 1000
]
= mconcat [ "HCContent "
, show (status, t)
, " ("
, contentToTruncatedString c 1000
, ")"
]

contentToTruncatedString :: Content -> I.Int64 -> String
contentToTruncatedString (ContentBuilder builder maybeLength) maxLength =
let
truncated = (T.unpack . Data.Text.Encoding.decodeUtf8) $ L.toStrict $ L.take maxLength (BB.toLazyByteString builder)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style nit: it's nice to have a consistent flow of function application. You can do almost all . with a single $, or all $:

Suggested change
truncated = (T.unpack . Data.Text.Encoding.decodeUtf8) $ L.toStrict $ L.take maxLength (BB.toLazyByteString builder)
truncated = T.unpack $ Data.Text.Encoding.decodeUtf8 $ L.toStrict $ L.take maxLength $ BB.toLazyByteString builder
Suggested change
truncated = (T.unpack . Data.Text.Encoding.decodeUtf8) $ L.toStrict $ L.take maxLength (BB.toLazyByteString builder)
truncated = T.unpack . Data.Text.Encoding.decodeUtf8 . L.toStrict . L.take maxLength $ BB.toLazyByteString builder

-- bytes of the content, and annotating it with the remaining length.
--
-- @since 1.6.28.0
contentToTruncatedString :: Content -> I.Int64 -> String
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Int64 is a reasonable choice here but it's a little nicer to use Integer or Int here - those are in Prelude and don't require imports for end users.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, but take in Data.ByteString.Lazy actually requires Int64.

import qualified Data.Text.Lazy.Builder as TBuilder
import Data.Time (UTCTime)
import GHC.Generics (Generic)
import qualified GHC.Int as I
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GHC.Int is kind of an internal module - generally preferable to import from Data.Int which is less GHC internals-y

simpleContentType :: ContentType -> ContentType
simpleContentType = fst . B.break (== _semicolon)

decoderForCharset :: Maybe B.ByteString -> L.ByteString -> TL.Text
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wikipedia offers some information about how often non-UTF8 encodings are used.

Unsurprisingly, UTF-8 is overwhelmingly more common than all other encodings combined. I added support for some of the more common alternatives, regardless.

@@ -0,0 +1,37 @@
module Yesod.Core.HandlerContents
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I broke this type definition out into a separate file to avoid a circular dependency. Maybe someone else can see a better way to solve this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine - just make sure HandlerContents is being properly re-exported from the original module in which it is defined.

You'll need to modify the yesod-core.cabal file and add this as either an exposed-modules (which makes it public so you'll want to add a CHANGELOG entry + some haddocks here) or other-modules (in which case it is private and you don't need to do quite so much ((though having an @since tag in some moduel docs is nice for the developer trawlin code)))

@dtpowl dtpowl requested a review from parsonsmatt March 24, 2025 21:03

decoderForCharset :: Maybe B.ByteString -> L.ByteString -> TL.Text
decoderForCharset (Just encodingSymbol)
| encodingSymbol == (encodeUtf8 $ T.pack $ "utf-8") = LE.decodeUtf8With EE.lenientDecode
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the IANA documentation for the character set symbols. I didn't handle synonyms here.

Copy link
Contributor

@parsonsmatt parsonsmatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lots of comments, many of which are just on style/idiomatic Haskell

decoderForCharset :: Maybe B.ByteString -> L.ByteString -> TL.Text
decoderForCharset (Just encodingSymbol)
| encodingSymbol == (encodeUtf8 $ T.pack $ "utf-8") = LE.decodeUtf8With EE.lenientDecode
| encodingSymbol == (encodeUtf8 $ T.pack $ "US-ASCII") = TL.fromStrict . fst . decodeASCIIPrefix . B.toStrict
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you enable OverloadedStrings then you can write:

Suggested change
| encodingSymbol == (encodeUtf8 $ T.pack $ "US-ASCII") = TL.fromStrict . fst . decodeASCIIPrefix . B.toStrict
| encodingSymbol == "US-ASCII" = TL.fromStrict . fst . decodeASCIIPrefix . B.toStrict

decoderForCharset (Just encodingSymbol)
| encodingSymbol == (encodeUtf8 $ T.pack $ "utf-8") = LE.decodeUtf8With EE.lenientDecode
| encodingSymbol == (encodeUtf8 $ T.pack $ "US-ASCII") = TL.fromStrict . fst . decodeASCIIPrefix . B.toStrict
| encodingSymbol == (encodeUtf8 $ T.pack $ "latin1") = LE.decodeLatin1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i generally do not recommend using a formatter that aligns = like that - very sensitive to alignment breaking in other cases

instead, indenting after the = helps to have alignment on the expressions

Suggested change
| encodingSymbol == (encodeUtf8 $ T.pack $ "latin1") = LE.decodeLatin1
| encodingSymbol == (encodeUtf8 $ T.pack $ "latin1") =
LE.decodeLatin1

this is one of those things where the very nice aesthetics of Mathy Lookin Haskell Code don't play super nice with code-on-computer (vs code-on-paper)

Comment on lines 260 to 263
typeIsText = B.isPrefixOf (packString "text") t ||
B.isPrefixOf (packString "application/json") t ||
B.isPrefixOf (packString "application/rss") t ||
B.isPrefixOf (packString "application/atom") t
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar note re alignment - generally nicer to have operator first

Suggested change
typeIsText = B.isPrefixOf (packString "text") t ||
B.isPrefixOf (packString "application/json") t ||
B.isPrefixOf (packString "application/rss") t ||
B.isPrefixOf (packString "application/atom") t
typeIsText =
B.isPrefixOf (packString "text") t
|| B.isPrefixOf (packString "application/json") t
|| B.isPrefixOf (packString "application/rss") t
|| B.isPrefixOf (packString "application/atom") t

Comment on lines 258 to 259
(t, params) = NWP.parseContentType ct
charset = lookup (packString "charset") params
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(t, params) = NWP.parseContentType ct
charset = lookup (packString "charset") params
(t, params) =
NWP.parseContentType ct
charset =
lookup (packString "charset") params

more diff-friendly way to get alignment on the expressions

Comment on lines 255 to 256
textDecoderFor :: ContentType -> L.ByteString -> Maybe TL.Text
textDecoderFor ct =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is point-free - we can make the below a bit more legible if we accept the parameter explicitly:

Suggested change
textDecoderFor :: ContentType -> L.ByteString -> Maybe TL.Text
textDecoderFor ct =
textDecoderFor :: ContentType -> L.ByteString -> Maybe TL.Text
textDecoderFor ct bytes =

Copy link
Author

@dtpowl dtpowl Mar 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense, but I'll probably rename this function while I'm at. Obviously there's no behavioral difference, but textDecoderFor sounds like a unary function that accepts a content type and returns a decoder; decodeTextForContentType sounds like a two-place function that accepts both a content type and some bytes.

Comment on lines 15 to 23
data HandlerContents =
HCContent !H.Status !TypedContent
| HCError !ErrorResponse
| HCSendFile !ContentType !FilePath !(Maybe W.FilePart)
| HCRedirect !H.Status !Text
| HCCreated !Text
| HCWai !W.Response
| HCWaiApp !W.Application
instance Show HandlerContents where
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
data HandlerContents =
HCContent !H.Status !TypedContent
| HCError !ErrorResponse
| HCSendFile !ContentType !FilePath !(Maybe W.FilePart)
| HCRedirect !H.Status !Text
| HCCreated !Text
| HCWai !W.Response
| HCWaiApp !W.Application
instance Show HandlerContents where
data HandlerContents =
HCContent !H.Status !TypedContent
| HCError !ErrorResponse
| HCSendFile !ContentType !FilePath !(Maybe W.FilePart)
| HCRedirect !H.Status !Text
| HCCreated !Text
| HCWai !W.Response
| HCWaiApp !W.Application
instance Show HandlerContents where

Comment on lines 35 to 37
show (HCWai _) = "HCWai"
show (HCWaiApp _) = "HCWaiApp"
instance Exception HandlerContents
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
show (HCWai _) = "HCWai"
show (HCWaiApp _) = "HCWaiApp"
instance Exception HandlerContents
show (HCWai _) = "HCWai"
show (HCWaiApp _) = "HCWaiApp"
instance Exception HandlerContents

@@ -0,0 +1,37 @@
module Yesod.Core.HandlerContents
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine - just make sure HandlerContents is being properly re-exported from the original module in which it is defined.

You'll need to modify the yesod-core.cabal file and add this as either an exposed-modules (which makes it public so you'll want to add a CHANGELOG entry + some haddocks here) or other-modules (in which case it is private and you don't need to do quite so much ((though having an @since tag in some moduel docs is nice for the developer trawlin code)))

Comment on lines 269 to 277
contentToSnippet :: Content -> (L.ByteString -> Maybe TL.Text) -> I.Int64 -> Maybe TL.Text
contentToSnippet (ContentBuilder builder maybeLength) decoder maxLength = do
truncatedText <- decoder . L.take maxLength $ BB.toLazyByteString builder
pure $ truncatedText <> (TL.pack excessLengthString)
where
excessLength = fromMaybe 0 $ (subtract $ fromIntegral maxLength) <$> maybeLength
excessLengthString = case excessLength > 0 of
False -> ""
True -> "...+ " <> (show excessLength)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can return L.ByteString here and leave the decoding to callsites. That simplifies our signature and use a bit.

Suggested change
contentToSnippet :: Content -> (L.ByteString -> Maybe TL.Text) -> I.Int64 -> Maybe TL.Text
contentToSnippet (ContentBuilder builder maybeLength) decoder maxLength = do
truncatedText <- decoder . L.take maxLength $ BB.toLazyByteString builder
pure $ truncatedText <> (TL.pack excessLengthString)
where
excessLength = fromMaybe 0 $ (subtract $ fromIntegral maxLength) <$> maybeLength
excessLengthString = case excessLength > 0 of
False -> ""
True -> "...+ " <> (show excessLength)
contentToSnippet :: Content -> I.Int64 -> Maybe L.ByteString
contentToSnippet (ContentBuilder builder maybeLength) maxLength = do
truncatedText <- decoder . L.take maxLength $ BB.toLazyByteString builder
pure $ truncatedText <> (TL.pack excessLengthString)
where
excessLength = fromMaybe 0 $ (subtract $ fromIntegral maxLength) <$> maybeLength
excessLengthString = case excessLength > 0 of
False -> ""
True -> "...+ " <> (_f excessLength)

For _f, consider Data.ByteStrying.Lazy.Char8 which can pack :: [Char] -> ByteString or for supreme efficiency, using intDec :: Int -> Builder for constructing this, and then BB.toLazyByteString.

--
-- @since 1.6.28.0
typedContentToSnippet :: TypedContent -> I.Int64 -> Maybe TL.Text
typedContentToSnippet (TypedContent t c) maxLength = contentToSnippet c (textDecoderFor t) maxLength
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we extract the decoding responsibility, then we have:

Suggested change
typedContentToSnippet (TypedContent t c) maxLength = contentToSnippet c (textDecoderFor t) maxLength
typedContentToSnippet (TypedContent t c) maxLength = textDecoderFor t $ contentToSnippet c maxLength

@parsonsmatt parsonsmatt mentioned this pull request Oct 23, 2025
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants