-
Notifications
You must be signed in to change notification settings - Fork 379
When showing HandlerContent, include a content snippet #1864
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
When showing HandlerContent, include a content snippet #1864
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.
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?
| 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.
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 ?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
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.
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?
There was a problem hiding this comment.
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.
yesod-core/src/Yesod/Core/Types.hs
Outdated
| = mconcat [ "HCContent " | ||
| , show (status, t) | ||
| , contentToTruncatedString c 1000 | ||
| ] |
There was a problem hiding this comment.
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
| = 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.
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 $:
| 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.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
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.
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 | |||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)))
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.
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.
parsonsmatt
left a comment
There was a problem hiding this 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
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.
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:
| | 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.
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
| | 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.
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
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| (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.
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:
| 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.
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.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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.
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)))
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.
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.
| 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.
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:
| 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: