When showing HandlerContent, include a content snippet#1864
When showing HandlerContent, include a content snippet#1864dtpowl wants to merge 14 commits intoyesodweb:masterfrom
Conversation
yesod-core/src/Yesod/Core/Types.hs
Outdated
| 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" |
There was a problem hiding this comment.
This function is exported. Can we add a doc string with @SInCE annotation?
| 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" |
yesod-core/src/Yesod/Core/Types.hs
Outdated
| contentToTruncatedString :: Content -> Int -> String | ||
| contentToTruncatedString (ContentBuilder builder maybeLength) maxLength = | ||
| let | ||
| truncated = take maxLength (show builder) |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
Co-authored-by: Matt Parsons <[email protected]>
cafe865 to
a81cb21
Compare
a81cb21 to
0d907ea
Compare
yesod-core/src/Yesod/Core/Types.hs
Outdated
| excess = case maybeLength of | ||
| (Just length) -> length - (fromIntegral maxLength) | ||
| Nothing -> 0 | ||
| in case (excess > 0) of |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
yesod-core/src/Yesod/Core/Types.hs
Outdated
| = mconcat [ "HCContent " | ||
| , show (status, t) | ||
| , contentToTruncatedString c 1000 | ||
| ] |
There was a problem hiding this comment.
Maybe want to parens + space it
| = mconcat [ "HCContent " | |
| , show (status, t) | |
| , contentToTruncatedString c 1000 | |
| ] | |
| = mconcat [ "HCContent " | |
| , show (status, t) | |
| , " (" | |
| , contentToTruncatedString c 1000 | |
| , ")" | |
| ] |
yesod-core/src/Yesod/Core/Types.hs
Outdated
| 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) |
There was a problem hiding this comment.
style nit: it's nice to have a consistent flow of function application. You can do almost all . with a single $, or all $:
| 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 |
| 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 |
yesod-core/src/Yesod/Core/Types.hs
Outdated
| -- bytes of the content, and annotating it with the remaining length. | ||
| -- | ||
| -- @since 1.6.28.0 | ||
| contentToTruncatedString :: Content -> I.Int64 -> String |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
GHC.Int is kind of an internal module - generally preferable to import from Data.Int which is less GHC internals-y
yesod-core/src/Yesod/Core/Content.hs
Outdated
| simpleContentType :: ContentType -> ContentType | ||
| simpleContentType = fst . B.break (== _semicolon) | ||
|
|
||
| decoderForCharset :: Maybe B.ByteString -> L.ByteString -> TL.Text |
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)))
yesod-core/src/Yesod/Core/Content.hs
Outdated
|
|
||
| decoderForCharset :: Maybe B.ByteString -> L.ByteString -> TL.Text | ||
| decoderForCharset (Just encodingSymbol) | ||
| | encodingSymbol == (encodeUtf8 $ T.pack $ "utf-8") = LE.decodeUtf8With EE.lenientDecode |
There was a problem hiding this comment.
See the IANA documentation for the character set symbols. I didn't handle synonyms here.
parsonsmatt
left a comment
There was a problem hiding this comment.
lots of comments, many of which are just on style/idiomatic Haskell
yesod-core/src/Yesod/Core/Content.hs
Outdated
| 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 |
There was a problem hiding this comment.
If you enable OverloadedStrings then you can write:
| | encodingSymbol == (encodeUtf8 $ T.pack $ "US-ASCII") = TL.fromStrict . fst . decodeASCIIPrefix . B.toStrict | |
| | encodingSymbol == "US-ASCII" = TL.fromStrict . fst . decodeASCIIPrefix . B.toStrict |
yesod-core/src/Yesod/Core/Content.hs
Outdated
| 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 |
There was a problem hiding this comment.
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
| | 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)
yesod-core/src/Yesod/Core/Content.hs
Outdated
| typeIsText = B.isPrefixOf (packString "text") t || | ||
| B.isPrefixOf (packString "application/json") t || | ||
| B.isPrefixOf (packString "application/rss") t || | ||
| B.isPrefixOf (packString "application/atom") t |
There was a problem hiding this comment.
Similar note re alignment - generally nicer to have operator first
| 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 |
yesod-core/src/Yesod/Core/Content.hs
Outdated
| (t, params) = NWP.parseContentType ct | ||
| charset = lookup (packString "charset") params |
There was a problem hiding this comment.
| (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
yesod-core/src/Yesod/Core/Content.hs
Outdated
| textDecoderFor :: ContentType -> L.ByteString -> Maybe TL.Text | ||
| textDecoderFor ct = |
There was a problem hiding this comment.
This is point-free - we can make the below a bit more legible if we accept the parameter explicitly:
| textDecoderFor :: ContentType -> L.ByteString -> Maybe TL.Text | |
| textDecoderFor ct = | |
| textDecoderFor :: ContentType -> L.ByteString -> Maybe TL.Text | |
| textDecoderFor ct bytes = |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
| 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 |
| show (HCWai _) = "HCWai" | ||
| show (HCWaiApp _) = "HCWaiApp" | ||
| instance Exception HandlerContents |
There was a problem hiding this comment.
| 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 | |||
There was a problem hiding this comment.
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)))
yesod-core/src/Yesod/Core/Content.hs
Outdated
| 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) |
There was a problem hiding this comment.
We can return L.ByteString here and leave the decoding to callsites. That simplifies our signature and use a bit.
| 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.
yesod-core/src/Yesod/Core/Content.hs
Outdated
| -- | ||
| -- @since 1.6.28.0 | ||
| typedContentToSnippet :: TypedContent -> I.Int64 -> Maybe TL.Text | ||
| typedContentToSnippet (TypedContent t c) maxLength = contentToSnippet c (textDecoderFor t) maxLength |
There was a problem hiding this comment.
If we extract the decoding responsibility, then we have:
| typedContentToSnippet (TypedContent t c) maxLength = contentToSnippet c (textDecoderFor t) maxLength | |
| typedContentToSnippet (TypedContent t c) maxLength = textDecoderFor t $ contentToSnippet c maxLength |
When possible, shows a snippet of the underlying
Contentwhenshowing (ordisplayExceptioning) anHCContent.Before submitting your PR, check that you've:
After submitting your PR: