From d463b63f2bc8aacc0244d87bd7e211aa74696853 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Sun, 19 Nov 2023 15:59:21 +0100 Subject: [PATCH 01/14] URI: formatted --- Network/HTTP/Types/URI.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Network/HTTP/Types/URI.hs b/Network/HTTP/Types/URI.hs index 0770723..bbab3d5 100644 --- a/Network/HTTP/Types/URI.hs +++ b/Network/HTTP/Types/URI.hs @@ -117,6 +117,9 @@ import Data.Word (Word8) type QueryItem = (B.ByteString, Maybe B.ByteString) -- | A sequence of 'QueryItem's. +-- +-- General form: @a=b&c=d@, but if for example the value of @a@ is @Nothing@ +-- instead of @Just "b"@, it becomes @a&c=d@. type Query = [QueryItem] -- | Like Query, but with 'Text' instead of 'B.ByteString' (UTF8-encoded). From 5b0df40c1986bae0f0acd52526ea2a04f36b657d Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Tue, 21 Nov 2023 23:29:06 +0100 Subject: [PATCH 02/14] QueryLike: 'toStrict' is shorter and probably also better --- Network/HTTP/Types/QueryLike.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Network/HTTP/Types/QueryLike.hs b/Network/HTTP/Types/QueryLike.hs index 1c24843..17b4b8f 100644 --- a/Network/HTTP/Types/QueryLike.hs +++ b/Network/HTTP/Types/QueryLike.hs @@ -9,8 +9,8 @@ module Network.HTTP.Types.QueryLike ( where import Control.Arrow ((***)) -import Data.ByteString as B (ByteString, concat) -import Data.ByteString.Lazy as L (ByteString, toChunks) +import Data.ByteString as B (ByteString) +import Data.ByteString.Lazy as L (ByteString, toStrict) import Data.Maybe (catMaybes) import Data.Text as T (Text, pack) import Data.Text.Encoding as T (encodeUtf8) @@ -48,12 +48,12 @@ instance (QueryKeyLike k, QueryValueLike v) => QueryLike [Maybe (k, v)] where toQuery = toQuery . catMaybes instance QueryKeyLike B.ByteString where toQueryKey = id -instance QueryKeyLike L.ByteString where toQueryKey = B.concat . L.toChunks +instance QueryKeyLike L.ByteString where toQueryKey = L.toStrict instance QueryKeyLike T.Text where toQueryKey = T.encodeUtf8 instance QueryKeyLike [Char] where toQueryKey = T.encodeUtf8 . T.pack instance QueryValueLike B.ByteString where toQueryValue = Just -instance QueryValueLike L.ByteString where toQueryValue = Just . B.concat . L.toChunks +instance QueryValueLike L.ByteString where toQueryValue = Just . L.toStrict instance QueryValueLike T.Text where toQueryValue = Just . T.encodeUtf8 instance QueryValueLike [Char] where toQueryValue = Just . T.encodeUtf8 . T.pack instance QueryValueLike a => QueryValueLike (Maybe a) where From 25777ac2e74b902faa05b4cb705bb747720c875c Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Tue, 21 Nov 2023 23:30:55 +0100 Subject: [PATCH 03/14] Status: some refactoring of code. would want tests to verify everything works, though --- Network/HTTP/Types/Status.hs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index 808f9e7..1443fdd 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -126,6 +126,7 @@ module Network.HTTP.Types.Status ( import Data.ByteString as B (ByteString, empty) import Data.Data (Data) +import Data.Function (on) import Data.Typeable (Typeable) import GHC.Generics (Generic) @@ -166,11 +167,11 @@ data Status = Status -- | A 'Status' is equal to another 'Status' if the status codes are equal. instance Eq Status where - Status { statusCode = a } == Status { statusCode = b } = a == b + (==) = (==) `on` statusCode -- | 'Status'es are ordered according to their status codes only. instance Ord Status where - compare Status { statusCode = a } Status { statusCode = b } = a `compare` b + compare = compare `on` statusCode -- | Be advised, that when using the \"enumFrom*\" family of methods or -- ranges in lists, it will generate all possible status codes. @@ -790,7 +791,7 @@ networkAuthenticationRequired511 = status511 -- -- @since 0.8.0 statusIsInformational :: Status -> Bool -statusIsInformational (Status {statusCode=code}) = code >= 100 && code < 200 +statusIsInformational = statusIs 1 -- | Successful class -- @@ -798,7 +799,7 @@ statusIsInformational (Status {statusCode=code}) = code >= 100 && code < 200 -- -- @since 0.8.0 statusIsSuccessful :: Status -> Bool -statusIsSuccessful (Status {statusCode=code}) = code >= 200 && code < 300 +statusIsSuccessful = statusIs 2 -- | Redirection class -- @@ -806,7 +807,7 @@ statusIsSuccessful (Status {statusCode=code}) = code >= 200 && code < 300 -- -- @since 0.8.0 statusIsRedirection :: Status -> Bool -statusIsRedirection (Status {statusCode=code}) = code >= 300 && code < 400 +statusIsRedirection = statusIs 3 -- | Client Error class -- @@ -814,7 +815,7 @@ statusIsRedirection (Status {statusCode=code}) = code >= 300 && code < 400 -- -- @since 0.8.0 statusIsClientError :: Status -> Bool -statusIsClientError (Status {statusCode=code}) = code >= 400 && code < 500 +statusIsClientError = statusIs 4 -- | Server Error class -- @@ -822,4 +823,7 @@ statusIsClientError (Status {statusCode=code}) = code >= 400 && code < 500 -- -- @since 0.8.0 statusIsServerError :: Status -> Bool -statusIsServerError (Status {statusCode=code}) = code >= 500 && code < 600 +statusIsServerError = statusIs 5 + +statusIs :: Int -> Status -> Bool +statusIs i Status{statusCode = code} = (code `div` 100) == i From f7594ffb332728369b96d66776a43caff961fa4d Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Tue, 21 Nov 2023 23:31:37 +0100 Subject: [PATCH 04/14] Version: added 'HTTP/3', and functions to parse/render an HTTP version --- Network/HTTP/Types/Version.hs | 78 +++++++++++++++++++++++++++++++++++ http-types.cabal | 3 +- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/Network/HTTP/Types/Version.hs b/Network/HTTP/Types/Version.hs index ccdd7aa..185a1d9 100644 --- a/Network/HTTP/Types/Version.hs +++ b/Network/HTTP/Types/Version.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} -- | Types and constants to describe the HTTP version. module Network.HTTP.Types.Version ( @@ -8,10 +9,16 @@ module Network.HTTP.Types.Version ( http10, http11, http20, + http30, + parseHttpVersion, + renderHttpVersion, ) where +import Data.ByteString as B (ByteString, foldl', null, span, stripPrefix, uncons) +import Data.ByteString.Char8 as B8 (cons, pack) import Data.Data (Data) import Data.Typeable (Typeable) +import Data.Word8 (isDigit, _0, _period) import GHC.Generics (Generic) -- | HTTP Version. @@ -53,3 +60,74 @@ http11 = HttpVersion 1 1 -- @since 0.10 http20 :: HttpVersion http20 = HttpVersion 2 0 + +-- | HTTP 3.0 +-- +-- @since 0.12.5 +http30 :: HttpVersion +http30 = HttpVersion 3 0 + +-- | Attempt to parse a 'ByteString' as an 'HttpVersion'. +-- +-- If there is no dot and minor version, then a minor version of +-- zero is implied. +-- +-- === __Examples__ +-- +-- > parseHttpVersion "HTTP/1.1" == Right (HttpVersion 1 1) +-- > parseHttpVersion "HTTP/2" == Right (HttpVersion 2 0) +-- > parseHttpVersion "Hello" == Left "Not an HTTP protocol version" +-- > parseHttpVersion "HTTP:2" == Left "Not an HTTP protocol version" +-- > parseHttpVersion "HTTP/TWO" == Left "No HTTP protocol major version provided" +-- > parseHttpVersion "HTTP/2DOT" == Left "Expected '.' after first digit(s)" +-- > parseHttpVersion "HTTP/2." == Left "No HTTP protocol minor version provided" +-- > parseHttpVersion "HTTP/2.@" == Left "Unexpected bytes after HTTP minor version" +-- +-- @since 0.12.5 +parseHttpVersion :: ByteString -> Either String HttpVersion +parseHttpVersion bs = + case B.stripPrefix "HTTP/" bs of + Nothing -> Left "Not an HTTP protocol version" + Just rest -> + withDigits "major" rest $ \majV more -> + HttpVersion majV <$> getMinorVersion more + where + withDigits s rest f = + case B.span isDigit rest of + ("", _) -> Left $ "No HTTP protocol " <> s <> " version provided" + (ds, more) -> f (unsafeDigitsToInt ds) more + getMinorVersion = + maybe (pure 0) go . B.uncons + where + go (w8, final) + | w8 /= _period = Left "Expected '.' after first digit(s)" + | otherwise = + withDigits "minor" final $ \minV extra -> + if B.null extra + then pure minV + else Left "Unexpected bytes after HTTP minor version" + +unsafeDigitsToInt :: ByteString -> Int +unsafeDigitsToInt = B.foldl' go 0 + where + go i w8 = 10 * i + fromIntegral (w8 - _0) + +-- | Convert an 'HttpVersion' to a 'ByteString'. +-- +-- If the minor version is zero, it is not rendered. +-- +-- > renderHttpVersion http11 == "HTTP/1.1" +-- > renderHttpVersion http20 == "HTTP/2" +-- +-- @since 0.12.5 +renderHttpVersion :: HttpVersion -> ByteString +renderHttpVersion httpV = + "HTTP/" + <> toBS majV + <> if minV == 0 + then "" + else B8.cons '/' $ toBS minV + where + majV = httpMajor httpV + minV = httpMinor httpV + toBS = B8.pack . show diff --git a/http-types.cabal b/http-types.cabal index 244ce4a..71e4408 100644 --- a/http-types.cabal +++ b/http-types.cabal @@ -40,7 +40,8 @@ Library bytestring >=0.10.4.0 && <1.0, array >=0.2 && <0.6, case-insensitive >=0.2 && <1.3, - text >= 0.11.0.2 + text >= 0.11.0.2, + word8 Default-language: Haskell2010 Test-suite spec From fc09600865300ff5fff59077bae9cb18ce6f14d5 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Tue, 21 Nov 2023 23:35:12 +0100 Subject: [PATCH 05/14] Export everything from 'Types.hs' and a bit of reordering --- Network/HTTP/Types.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Network/HTTP/Types.hs b/Network/HTTP/Types.hs index ec4deeb..a65b69f 100644 --- a/Network/HTTP/Types.hs +++ b/Network/HTTP/Types.hs @@ -31,6 +31,9 @@ module Network.HTTP.Types ( http10, http11, http20, + http30, + parseHttpVersion, + renderHttpVersion, -- * Status From f7ca39e6a93c3ae27a1cb6f76288d6acd69097fc Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 15 Jan 2024 01:39:44 +0100 Subject: [PATCH 06/14] Version: added 'HTTP/3.0' and pattern synonyms for the common 'HttpVersion's --- Network/HTTP/Types/Version.hs | 21 +++++++++++++++++++++ stack.yaml | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Network/HTTP/Types/Version.hs b/Network/HTTP/Types/Version.hs index 185a1d9..f41cada 100644 --- a/Network/HTTP/Types/Version.hs +++ b/Network/HTTP/Types/Version.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE OverloadedStrings #-} -- | Types and constants to describe the HTTP version. @@ -10,6 +11,11 @@ module Network.HTTP.Types.Version ( http11, http20, http30, + pattern Http09, + pattern Http10, + pattern Http11, + pattern Http20, + pattern Http30, parseHttpVersion, renderHttpVersion, ) where @@ -47,26 +53,41 @@ instance Show HttpVersion where http09 :: HttpVersion http09 = HttpVersion 0 9 +pattern Http09 :: HttpVersion +pattern Http09 <- HttpVersion 0 9 + -- | HTTP 1.0 http10 :: HttpVersion http10 = HttpVersion 1 0 +pattern Http10 :: HttpVersion +pattern Http10 <- HttpVersion 1 0 + -- | HTTP 1.1 http11 :: HttpVersion http11 = HttpVersion 1 1 +pattern Http11 :: HttpVersion +pattern Http11 <- HttpVersion 1 1 + -- | HTTP 2.0 -- -- @since 0.10 http20 :: HttpVersion http20 = HttpVersion 2 0 +pattern Http20 :: HttpVersion +pattern Http20 <- HttpVersion 2 0 + -- | HTTP 3.0 -- -- @since 0.12.5 http30 :: HttpVersion http30 = HttpVersion 3 0 +pattern Http30 :: HttpVersion +pattern Http30 <- HttpVersion 3 0 + -- | Attempt to parse a 'ByteString' as an 'HttpVersion'. -- -- If there is no dot and minor version, then a minor version of diff --git a/stack.yaml b/stack.yaml index 67e9c6f..de84d1f 100644 --- a/stack.yaml +++ b/stack.yaml @@ -17,7 +17,7 @@ # # resolver: ./custom-snapshot.yaml # resolver: https://example.com/snapshots/2018-01-01.yaml -resolver: lts-20.26 +resolver: lts-22.5 # User packages to be built. # Various formats can be used as shown in the example below. From bebedd159470a1e01f529788f7cbd007dd4310d1 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 15 Jan 2024 01:41:38 +0100 Subject: [PATCH 07/14] Version: adjusted parse- and render- functions and added tests to check all the new things --- Network/HTTP/Types/Version.hs | 71 +++++++++++++++----------- test/Network/HTTP/Types/VersionSpec.hs | 51 +++++++++++++----- 2 files changed, 80 insertions(+), 42 deletions(-) diff --git a/Network/HTTP/Types/Version.hs b/Network/HTTP/Types/Version.hs index f41cada..83b6475 100644 --- a/Network/HTTP/Types/Version.hs +++ b/Network/HTTP/Types/Version.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE PatternSynonyms #-} @@ -21,7 +22,8 @@ module Network.HTTP.Types.Version ( ) where import Data.ByteString as B (ByteString, foldl', null, span, stripPrefix, uncons) -import Data.ByteString.Char8 as B8 (cons, pack) +import Data.ByteString.Builder as B (toLazyByteString, string7, word8) +import Data.ByteString.Lazy as BL (toStrict) import Data.Data (Data) import Data.Typeable (Typeable) import Data.Word8 (isDigit, _0, _period) @@ -96,59 +98,68 @@ pattern Http30 <- HttpVersion 3 0 -- === __Examples__ -- -- > parseHttpVersion "HTTP/1.1" == Right (HttpVersion 1 1) +-- > parseHttpVersion "HTTP/2.0" == Right (HttpVersion 2 0) -- > parseHttpVersion "HTTP/2" == Right (HttpVersion 2 0) -- > parseHttpVersion "Hello" == Left "Not an HTTP protocol version" --- > parseHttpVersion "HTTP:2" == Left "Not an HTTP protocol version" +-- > parseHttpVersion "HTTP2" == Left "Not an HTTP protocol version" -- > parseHttpVersion "HTTP/TWO" == Left "No HTTP protocol major version provided" -- > parseHttpVersion "HTTP/2DOT" == Left "Expected '.' after first digit(s)" -- > parseHttpVersion "HTTP/2." == Left "No HTTP protocol minor version provided" --- > parseHttpVersion "HTTP/2.@" == Left "Unexpected bytes after HTTP minor version" +-- > parseHttpVersion "HTTP/2.0@" == Left "Unexpected bytes after HTTP minor version" -- -- @since 0.12.5 parseHttpVersion :: ByteString -> Either String HttpVersion -parseHttpVersion bs = - case B.stripPrefix "HTTP/" bs of - Nothing -> Left "Not an HTTP protocol version" - Just rest -> - withDigits "major" rest $ \majV more -> - HttpVersion majV <$> getMinorVersion more +-- This order is from most likely to be checked with this function. +-- HTTP/2 and /3 don't use this format as the version string, they use "h2" and "h3". +parseHttpVersion "HTTP/1.1" = Right http11 +parseHttpVersion "HTTP/1.0" = Right http10 +parseHttpVersion "HTTP/2.0" = Right http20 +parseHttpVersion "HTTP/3.0" = Right http30 +parseHttpVersion "HTTP/0.9" = Right http09 +parseHttpVersion bs = do + rest <- note "Not an HTTP protocol version" $ B.stripPrefix "HTTP/" bs + (ds, more) <- withDigits "major" rest + let majV = unsafeDigitsToInt ds + HttpVersion majV <$> getMinorVersion more where - withDigits s rest f = + note s = maybe (Left s) Right + withDigits s rest = case B.span isDigit rest of ("", _) -> Left $ "No HTTP protocol " <> s <> " version provided" - (ds, more) -> f (unsafeDigitsToInt ds) more + tup -> Right tup getMinorVersion = maybe (pure 0) go . B.uncons where go (w8, final) | w8 /= _period = Left "Expected '.' after first digit(s)" - | otherwise = - withDigits "minor" final $ \minV extra -> - if B.null extra - then pure minV - else Left "Unexpected bytes after HTTP minor version" + | otherwise = do + (ds, extra) <- withDigits "minor" final + if B.null extra + then Right $ unsafeDigitsToInt ds + else Left "Unexpected bytes after HTTP minor version" unsafeDigitsToInt :: ByteString -> Int unsafeDigitsToInt = B.foldl' go 0 where - go i w8 = 10 * i + fromIntegral (w8 - _0) + go !i w8 = 10 * i + fromIntegral (w8 - _0) -- | Convert an 'HttpVersion' to a 'ByteString'. -- --- If the minor version is zero, it is not rendered. --- --- > renderHttpVersion http11 == "HTTP/1.1" --- > renderHttpVersion http20 == "HTTP/2" +-- >>> renderHttpVersion http11 +-- "HTTP/1.1" +-- >>> renderHttpVersion http20 +-- "HTTP/2.0" -- -- @since 0.12.5 renderHttpVersion :: HttpVersion -> ByteString -renderHttpVersion httpV = - "HTTP/" - <> toBS majV - <> if minV == 0 - then "" - else B8.cons '/' $ toBS minV +-- This order is from most likely to be checked with this function. +renderHttpVersion Http11 = "HTTP/1.1" +renderHttpVersion Http10 = "HTTP/1.0" +renderHttpVersion Http20 = "HTTP/2.0" +renderHttpVersion Http30 = "HTTP/3.0" +renderHttpVersion Http09 = "HTTP/0.9" +renderHttpVersion (HttpVersion majV minV) = + BL.toStrict . B.toLazyByteString $ + B.string7 "HTTP/" <> toBS majV <> word8 _period <> toBS minV where - majV = httpMajor httpV - minV = httpMinor httpV - toBS = B8.pack . show + toBS = B.string7 . show diff --git a/test/Network/HTTP/Types/VersionSpec.hs b/test/Network/HTTP/Types/VersionSpec.hs index 90c454a..cddf1f7 100644 --- a/test/Network/HTTP/Types/VersionSpec.hs +++ b/test/Network/HTTP/Types/VersionSpec.hs @@ -1,8 +1,13 @@ {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -Wno-orphans #-} module Network.HTTP.Types.VersionSpec (main, spec) where +import Data.String (fromString) +import Data.Word (Word8) import Test.Hspec +import Test.QuickCheck (Arbitrary (..), property) import Network.HTTP.Types @@ -10,23 +15,45 @@ main :: IO () main = hspec spec spec :: Spec -spec = +spec = do describe "Regression tests" $ mapM_ checkVersion allVersions - --- | [("Rendered", {constant}, {literal}, "Shown")] -allVersions :: [(String, HttpVersion, HttpVersion, String)] + describe "parseHttpVersion" $ do + it "also works with unorthodox versions" $ + parseHttpVersion "HTTP/15.32" `shouldBe` Right (HttpVersion 15 32) + it "works with any number" $ + property $ \a b -> + let fromW8 = fromIntegral @Word8 @Int + majV = fromW8 a + minV = fromW8 b + v = fromString $ "HTTP/" <> show majV <> "." <> show minV + in parseHttpVersion v == Right (HttpVersion majV minV) + describe "renderHttpVersion" $ do + it "also works with unorthodox versions" $ + renderHttpVersion (HttpVersion 61 98) `shouldBe` "HTTP/61.98" + it "works with any number" $ + property $ \v@(HttpVersion majV minV) -> + renderHttpVersion v == fromString ("HTTP/" <> show majV <> "." <> show minV) + +-- | [({string}, {constant}, {literal})] +allVersions :: [(String, HttpVersion, HttpVersion)] allVersions = - [ ("HTTP/0.9", http09, HttpVersion 0 9, "HTTP/0.9") - , ("HTTP/1.0", http10, HttpVersion 1 0, "HTTP/1.0") - , ("HTTP/1.1", http11, HttpVersion 1 1, "HTTP/1.1") - , ("HTTP/2.0", http20, HttpVersion 2 0, "HTTP/2.0") + [ ("HTTP/0.9", http09, HttpVersion 0 9) + , ("HTTP/1.0", http10, HttpVersion 1 0) + , ("HTTP/1.1", http11, HttpVersion 1 1) + , ("HTTP/2.0", http20, HttpVersion 2 0) + , ("HTTP/3.0", http30, HttpVersion 3 0) ] -checkVersion :: (String, HttpVersion, HttpVersion, String) -> Spec -checkVersion (msg, v1, v2, str) = - it msg $ do +checkVersion :: (String, HttpVersion, HttpVersion) -> Spec +checkVersion (str, v1, v2) = + it str $ do v1 `shouldBe` v2 show v1 `shouldBe` str + renderHttpVersion v1 `shouldBe` bsStr + parseHttpVersion bsStr `shouldBe` Right v1 + where + bsStr = fromString str --- it "parses to HTTP/3" $ http30 `shouldBe` HttpVersion 3 0 +instance Arbitrary HttpVersion where + arbitrary = HttpVersion <$> arbitrary <*> arbitrary From a85499b2c3b2f00015f9e0819b59c8f20b3e052d Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 15 Jan 2024 01:48:01 +0100 Subject: [PATCH 08/14] export pattern synonyms in main module and added myself to 'Copyright' field in .cabal file --- Network/HTTP/Types.hs | 6 ++++++ http-types.cabal | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Network/HTTP/Types.hs b/Network/HTTP/Types.hs index a65b69f..9fa52b1 100644 --- a/Network/HTTP/Types.hs +++ b/Network/HTTP/Types.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE PatternSynonyms #-} module Network.HTTP.Types ( -- * Methods @@ -28,10 +29,15 @@ module Network.HTTP.Types ( HttpVersion (..), http09, + pattern Http09, http10, + pattern Http10, http11, + pattern Http11, http20, + pattern Http20, http30, + pattern Http30, parseHttpVersion, renderHttpVersion, diff --git a/http-types.cabal b/http-types.cabal index 71e4408..ba730c8 100644 --- a/http-types.cabal +++ b/http-types.cabal @@ -9,7 +9,7 @@ License: BSD-3-Clause License-file: LICENSE Author: Aristid Breitkreuz, Michael Snoyman Maintainer: felix.paulusma@gmail.com -Copyright: (C) 2011 Aristid Breitkreuz +Copyright: (C) 2011 Aristid Breitkreuz, (C) 2023 Felix Paulusma Category: Network, Web Build-type: Simple Extra-source-files: From 4920cb2e4948d866edc71782225ebc7821a5aedd Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Sun, 28 Jan 2024 22:56:27 +0100 Subject: [PATCH 09/14] added Status 451 --- Network/HTTP/Types.hs | 2 ++ Network/HTTP/Types/Status.hs | 32 +++++++++++++++++++++------ test/Network/HTTP/Types/StatusSpec.hs | 3 ++- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/Network/HTTP/Types.hs b/Network/HTTP/Types.hs index 9fa52b1..d1f30bc 100644 --- a/Network/HTTP/Types.hs +++ b/Network/HTTP/Types.hs @@ -131,6 +131,8 @@ module Network.HTTP.Types ( tooManyRequests429, status431, requestHeaderFieldsTooLarge431, + status451, + unavailableForLegalReasons451, status500, internalServerError500, status501, diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index 1443fdd..88e8523 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -8,12 +8,7 @@ module Network.HTTP.Types.Status ( -- * HTTP Status - -- If we ever want to deprecate the 'Status' data constructor: - -- #if __GLASGOW_HASKELL__ >= 908 - -- {-# DEPRECATED "Use 'mkStatus' when constructing a 'Status'" #-} Status(Status) - -- #else Status (Status), - -- #endif statusCode, statusMessage, mkStatus, @@ -101,6 +96,8 @@ module Network.HTTP.Types.Status ( tooManyRequests429, status431, requestHeaderFieldsTooLarge431, + status451, + unavailableForLegalReasons451, status500, internalServerError500, status501, @@ -162,8 +159,14 @@ data Status = Status -- name of the constructor, so that it doesn't clash with the new pattern synonym -- that's replacing it. -- --- > data Status = MkStatus ... --- > pattern Status code msg = MkStatus code msg +-- > {-# LANGUAGE PatternSynonyms #-} +-- > data Status = MkStatus +-- > { statusCode :: Int -- ^ 404 +-- > , statusMessage :: B.ByteString -- ^ "Not Found" +-- > , statusRaw :: B.ByteString -- ^ "404 Not Found" +-- > } +-- > pattern Status code msg <- MkStatus code msg _ +-- > where Status code msg = MkStatus code msg (fromString (show code <> " ") <> msg) -- | A 'Status' is equal to another 'Status' if the status codes are equal. instance Eq Status where @@ -224,6 +227,7 @@ instance Enum Status where toEnum 428 = status428 toEnum 429 = status429 toEnum 431 = status431 + toEnum 451 = status451 toEnum 500 = status500 toEnum 501 = status501 toEnum 502 = status502 @@ -703,6 +707,20 @@ status431 = mkStatus 431 "Request Header Fields Too Large" requestHeaderFieldsTooLarge431 :: Status requestHeaderFieldsTooLarge431 = status431 +-- | Unavailable For Legal Reasons 451 +-- () +-- +-- @since 0.13 +status451 :: Status +status451 = mkStatus 451 "Unavailable For Legal Reasons" + +-- | Unavailable For Legal Reasons 451 +-- () +-- +-- @since 0.13 +unavailableForLegalReasons451 :: Status +unavailableForLegalReasons451 = status451 + -- | Internal Server Error 500 status500 :: Status status500 = mkStatus 500 "Internal Server Error" diff --git a/test/Network/HTTP/Types/StatusSpec.hs b/test/Network/HTTP/Types/StatusSpec.hs index 6f06701..6ec26f9 100644 --- a/test/Network/HTTP/Types/StatusSpec.hs +++ b/test/Network/HTTP/Types/StatusSpec.hs @@ -42,7 +42,7 @@ categoryCheck name p shoulds = do mapM_ (\(st, _, _, _) -> st `shouldNotSatisfy` p) $ allStatusses L.\\ shoulds where - msg = "'" <> name <> "' " <> "identifies correct statusses" <> extra + msg = "'" <> name <> "' " <> extra <> "identifies correct statusses" extra = replicate (length ("statusIsInformational" :: String) - length name) ' ' type StatusTuple = (Status, Status, Int, B.ByteString) @@ -102,6 +102,7 @@ _400Statusses = , (status428, preconditionRequired428, 428, "Precondition Required") , (status429, tooManyRequests429, 429, "Too Many Requests") , (status431, requestHeaderFieldsTooLarge431, 431, "Request Header Fields Too Large") + , (status451, unavailableForLegalReasons451, 451, "Unavailable For Legal Reasons") ] _500Statusses :: [StatusTuple] From fa96c5453d89fe5ddd8730f0de3c59b4cf253667 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Sun, 28 Jan 2024 23:01:25 +0100 Subject: [PATCH 10/14] changed version bump to 0.13 --- CHANGELOG.md | 9 +++++++-- Network/HTTP/Types/Method.hs | 18 +++++++++--------- Network/HTTP/Types/Version.hs | 6 +++--- http-types.cabal | 4 ++-- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ca5e53..c8426d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ # Changelog for `http-types` -## 0.12.5 [XXXX-XX-XX] +## 0.13 [XXXX-XX-XX] -* Export everything from `Network.HTTP.Types` +* Export everything from `Network.HTTP.Types`, so that is technically + the only module you'd have to import. * Added a bunch of regression, unit and property tests for stability. * Updated the `README.md` +* Added new `HttpVersion`: `http30` +* Added new parse and render functions for `HttpVersion` +* Added pattern synonyms for `HttpVersion`s +* Added new `Status`: `status451` / `unavailableForLegalReasons451` ## 0.12.4 [2023-11-29] diff --git a/Network/HTTP/Types/Method.hs b/Network/HTTP/Types/Method.hs index 2d70cc9..3ff0e27 100644 --- a/Network/HTTP/Types/Method.hs +++ b/Network/HTTP/Types/Method.hs @@ -53,39 +53,39 @@ import GHC.Generics (Generic) -- | HTTP method (flat 'ByteString' type). type Method = B.ByteString --- | HTTP GET Method +-- | GET Method methodGet :: Method methodGet = renderStdMethod GET --- | HTTP POST Method +-- | POST Method methodPost :: Method methodPost = renderStdMethod POST --- | HTTP HEAD Method +-- | HEAD Method methodHead :: Method methodHead = renderStdMethod HEAD --- | HTTP PUT Method +-- | PUT Method methodPut :: Method methodPut = renderStdMethod PUT --- | HTTP DELETE Method +-- | DELETE Method methodDelete :: Method methodDelete = renderStdMethod DELETE --- | HTTP TRACE Method +-- | TRACE Method methodTrace :: Method methodTrace = renderStdMethod TRACE --- | HTTP CONNECT Method +-- | CONNECT Method methodConnect :: Method methodConnect = renderStdMethod CONNECT --- | HTTP OPTIONS Method +-- | OPTIONS Method methodOptions :: Method methodOptions = renderStdMethod OPTIONS --- | HTTP PATCH Method +-- | PATCH Method -- -- @since 0.8.0 methodPatch :: Method diff --git a/Network/HTTP/Types/Version.hs b/Network/HTTP/Types/Version.hs index 83b6475..ec730bc 100644 --- a/Network/HTTP/Types/Version.hs +++ b/Network/HTTP/Types/Version.hs @@ -83,7 +83,7 @@ pattern Http20 <- HttpVersion 2 0 -- | HTTP 3.0 -- --- @since 0.12.5 +-- @since 0.13 http30 :: HttpVersion http30 = HttpVersion 3 0 @@ -107,7 +107,7 @@ pattern Http30 <- HttpVersion 3 0 -- > parseHttpVersion "HTTP/2." == Left "No HTTP protocol minor version provided" -- > parseHttpVersion "HTTP/2.0@" == Left "Unexpected bytes after HTTP minor version" -- --- @since 0.12.5 +-- @since 0.13 parseHttpVersion :: ByteString -> Either String HttpVersion -- This order is from most likely to be checked with this function. -- HTTP/2 and /3 don't use this format as the version string, they use "h2" and "h3". @@ -150,7 +150,7 @@ unsafeDigitsToInt = B.foldl' go 0 -- >>> renderHttpVersion http20 -- "HTTP/2.0" -- --- @since 0.12.5 +-- @since 0.13 renderHttpVersion :: HttpVersion -> ByteString -- This order is from most likely to be checked with this function. renderHttpVersion Http11 = "HTTP/1.1" diff --git a/http-types.cabal b/http-types.cabal index ba730c8..8758f72 100644 --- a/http-types.cabal +++ b/http-types.cabal @@ -1,6 +1,6 @@ Cabal-version: 3.0 Name: http-types -Version: 0.12.5 +Version: 0.13 Synopsis: Generic HTTP types for Haskell (for both client and server code). Description: Types and functions to describe and handle HTTP concepts. Including "methods", "headers", "query strings", "paths" and "HTTP versions". @@ -21,7 +21,7 @@ Extra-source-files: Source-repository this type: git location: https://github.com/Vlix/http-types.git - tag: v0.12.5 + tag: v0.13 Source-repository head type: git From 35c6a356461f68bba2271557937f5ef132fd4bd8 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Sun, 28 Jan 2024 23:28:34 +0100 Subject: [PATCH 11/14] be obvious and human-readable in which bytes we're scrutinizing on with Statusses --- Network/HTTP/Types/Status.hs | 2 +- Network/HTTP/Types/URI.hs | 89 ++++++++++++++++++++---------------- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index 88e8523..fe8267f 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -844,4 +844,4 @@ statusIsServerError :: Status -> Bool statusIsServerError = statusIs 5 statusIs :: Int -> Status -> Bool -statusIs i Status{statusCode = code} = (code `div` 100) == i +statusIs i = (== i) . (`div` 100) . statusCode diff --git a/Network/HTTP/Types/URI.hs b/Network/HTTP/Types/URI.hs index bbab3d5..eef822a 100644 --- a/Network/HTTP/Types/URI.hs +++ b/Network/HTTP/Types/URI.hs @@ -88,7 +88,6 @@ import Data.Bits (shiftL, (.|.)) import qualified Data.ByteString as B import qualified Data.ByteString.Builder as B import qualified Data.ByteString.Lazy as BL -import Data.Char (ord) import Data.List (intersperse) import Data.Maybe (fromMaybe) #if __GLASGOW_HASKELL__ < 710 @@ -97,7 +96,7 @@ import Data.Monoid import Data.Text (Text) import Data.Text.Encoding (decodeUtf8With, encodeUtf8) import Data.Text.Encoding.Error (lenientDecode) -import Data.Word (Word8) +import Data.Word8 -- This section is needed to run doctests for GHC 8.10.7 #if !MIN_VERSION_bytestring(0,11,1) @@ -185,9 +184,9 @@ renderQueryBuilder qmark' (p : ps) = go (if qmark' then qmark else mempty) p : map (go amp) ps where - qmark = B.byteString "?" - amp = B.byteString "&" - equal = B.byteString "=" + qmark = B.word8 _question + amp = B.word8 _ampersand + equal = B.word8 _equal go sep (k, mv) = mconcat [ sep @@ -240,7 +239,7 @@ parseQueryReplacePlus replacePlus bs = parseQueryString' $ dropQuestion bs where dropQuestion q = case B.uncons q of - Just (63, q') -> q' + Just (w8, q') | w8 == _question -> q' _ -> q parseQueryString' q | B.null q = [] parseQueryString' q = @@ -248,7 +247,7 @@ parseQueryReplacePlus replacePlus bs = parseQueryString' $ dropQuestion bs in parsePair x : parseQueryString' xs where parsePair x = - let (k, v) = B.break (== 61) x -- equal sign + let (k, v) = B.break (== _equal) x v'' = case B.uncons v of Just (_, v') -> Just $ urlDecode replacePlus v' @@ -256,7 +255,7 @@ parseQueryReplacePlus replacePlus bs = parseQueryString' $ dropQuestion bs in (urlDecode replacePlus k, v'') queryStringSeparators :: B.ByteString -queryStringSeparators = B.pack [38, 59] -- ampersand, semicolon +queryStringSeparators = B.pack [_ampersand, _semicolon] -- | Break the second bytestring at the first occurrence of any bytes from -- the first bytestring, discarding that byte. @@ -274,16 +273,27 @@ breakDiscard seps s = parseSimpleQuery :: B.ByteString -> SimpleQuery parseSimpleQuery = map (second $ fromMaybe B.empty) . parseQuery -ord8 :: Char -> Word8 -ord8 = fromIntegral . ord - -unreservedQS, unreservedPI :: [Word8] -unreservedQS = map ord8 "-_.~" -- FIXME: According to RFC 3986, the following are also allowed in path segments: -- "!'()*;" -- -- https://www.rfc-editor.org/rfc/rfc3986#section-3.3 -unreservedPI = map ord8 "-_.~:@&=+$," +unreservedQS, unreservedPI :: [Word8] +-- "-_.~" +unreservedQS = [_hyphen, _underscore, _period, _tilde] +-- "-_.~:@&=+$," +unreservedPI = + [ _hyphen + , _underscore + , _period + , _tilde + , _colon + , _at + , _ampersand + , _equal + , _plus + , _dollar + , _comma + ] -- | Percent-encoding for URLs. -- @@ -301,18 +311,18 @@ urlEncodeBuilder' extraUnreserved = | otherwise = h2 ch unreserved ch - | ch >= 65 && ch <= 90 = True -- A-Z - | ch >= 97 && ch <= 122 = True -- a-z - | ch >= 48 && ch <= 57 = True -- 0-9 + | ch >= _A && ch <= _Z = True + | ch >= _a && ch <= _z = True + | ch >= _0 && ch <= _9 = True unreserved c = c `elem` extraUnreserved -- must be upper-case - h2 v = B.word8 37 `mappend` B.word8 (h a) `mappend` B.word8 (h b) -- 37 = % + h2 v = B.word8 _percent `mappend` B.word8 (h a) `mappend` B.word8 (h b) where (a, b) = v `divMod` 16 h i - | i < 10 = 48 + i -- zero (0) - | otherwise = 65 + i - 10 -- 65: A + | i < 10 = _0 + i + | otherwise = _A + i - 10 -- | Percent-encoding for URLs. -- @@ -358,20 +368,21 @@ urlDecode replacePlus z = fst $ B.unfoldrN (B.length z) go z go bs = case B.uncons bs of Nothing -> Nothing - -- plus to space - Just (43, ws) | replacePlus -> Just (32, ws) - -- percent - Just (37, ws) -> Just $ fromMaybe (37, ws) $ do - (x, xs) <- B.uncons ws - x' <- hexVal x - (y, ys) <- B.uncons xs - y' <- hexVal y - Just (combine x' y', ys) + Just (w8, ws) + -- plus to space + | replacePlus && w8 == _plus -> Just (_space, ws) + -- percent + | w8 == _percent -> Just $ fromMaybe (_percent, ws) $ do + (x, xs) <- B.uncons ws + x' <- hexVal x + (y, ys) <- B.uncons xs + y' <- hexVal y + Just (combine x' y', ys) Just (w, ws) -> Just (w, ws) hexVal w - | 48 <= w && w <= 57 = Just $ w - 48 -- 0 - 9 - | 65 <= w && w <= 70 = Just $ w - 55 -- A - F - | 97 <= w && w <= 102 = Just $ w - 87 -- a - f + | _0 <= w && w <= _9 = Just $ w - _0 + | _A <= w && w <= _F = Just $ w - 55 -- (_A - 10) + | _a <= w && w <= _f = Just $ w - 87 -- (_a - 10) | otherwise = Nothing combine :: Word8 -> Word8 -> Word8 combine a b = shiftL a 4 .|. b @@ -433,10 +444,10 @@ decodePathSegments a = where drop1Slash bs = case B.uncons bs of - Just (47, bs') -> bs' -- 47 == / + Just (w8, bs') | w8 == _slash -> bs' _ -> bs go bs = - let (x, y) = B.break (== 47) bs + let (x, y) = B.break (== _slash) bs in decodePathSegment x : if B.null y then [] @@ -478,7 +489,7 @@ extractPath = ensureNonEmpty . extract | "http://" `B.isPrefixOf` path = (snd . breakOnSlash . B.drop 7) path | "https://" `B.isPrefixOf` path = (snd . breakOnSlash . B.drop 8) path | otherwise = path - breakOnSlash = B.break (== 47) + breakOnSlash = B.break (== _slash) ensureNonEmpty "" = "/" ensureNonEmpty p = p @@ -494,7 +505,7 @@ encodePath x y = encodePathSegments x `mappend` renderQueryBuilder True y -- @since 0.5 decodePath :: B.ByteString -> ([Text], Query) decodePath b = - let (x, y) = B.break (== 63) b -- question mark + let (x, y) = B.break (== _question) b in (decodePathSegments x, parseQuery y) ----------------------------------------------------------------------------------------- @@ -551,9 +562,9 @@ renderQueryBuilderPartialEscape qmark' (p : ps) = go (if qmark' then qmark else mempty) p : map (go amp) ps where - qmark = B.byteString "?" - amp = B.byteString "&" - equal = B.byteString "=" + qmark = B.word8 _question + amp = B.word8 _ampersand + equal = B.word8 _equal go sep (k, mv) = mconcat [ sep From 133e2e1532b2fa92a9c6939e50209ab571c61ae1 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jan 2024 00:08:24 +0100 Subject: [PATCH 12/14] refactored a bit from 'parseByteRanges' and used 'Data.Word8' in 'Header.hs' --- Network/HTTP/Types/Header.hs | 24 +++++++++++------------- http-types.cabal | 4 ++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Network/HTTP/Types/Header.hs b/Network/HTTP/Types/Header.hs index 4eae93f..57e7423 100644 --- a/Network/HTTP/Types/Header.hs +++ b/Network/HTTP/Types/Header.hs @@ -89,6 +89,7 @@ module Network.HTTP.Types.Header ( ) where +import Control.Monad (guard) import qualified Data.ByteString as B import qualified Data.ByteString.Builder as B import qualified Data.ByteString.Char8 as B8 @@ -100,6 +101,7 @@ import Data.List (intersperse) import Data.Monoid #endif import Data.Typeable (Typeable) +import Data.Word8 (_comma, _hyphen) import GHC.Generics (Generic) -- | A full HTTP header field with the name and value separated. @@ -480,9 +482,9 @@ data ByteRange -- -- @since 0.6.11 renderByteRangeBuilder :: ByteRange -> B.Builder -renderByteRangeBuilder (ByteRangeFrom from) = B.integerDec from `mappend` B.char7 '-' -renderByteRangeBuilder (ByteRangeFromTo from to) = B.integerDec from `mappend` B.char7 '-' `mappend` B.integerDec to -renderByteRangeBuilder (ByteRangeSuffix suffix) = B.char7 '-' `mappend` B.integerDec suffix +renderByteRangeBuilder (ByteRangeFrom from) = B.integerDec from `mappend` B.word8 _hyphen +renderByteRangeBuilder (ByteRangeFromTo from to) = B.integerDec from `mappend` B.word8 _hyphen `mappend` B.integerDec to +renderByteRangeBuilder (ByteRangeSuffix suffix) = B.word8 _hyphen `mappend` B.integerDec suffix -- | Renders a byte range into a 'B.ByteString'. -- @@ -504,7 +506,7 @@ type ByteRanges = [ByteRange] renderByteRangesBuilder :: ByteRanges -> B.Builder renderByteRangesBuilder xs = B.byteString "bytes=" - `mappend` mconcat (intersperse (B.char7 ',') $ map renderByteRangeBuilder xs) + `mappend` mconcat (intersperse (B.word8 _comma) $ map renderByteRangeBuilder xs) -- | Renders a list of byte ranges into a 'B.ByteString'. -- @@ -537,7 +539,7 @@ renderByteRanges = BL.toStrict . B.toLazyByteString . renderByteRangesBuilder -- @since 0.9.1 parseByteRanges :: B.ByteString -> Maybe ByteRanges parseByteRanges bs1 = do - bs2 <- stripPrefixB "bytes=" bs1 + bs2 <- B.stripPrefix "bytes=" bs1 (r, bs3) <- range bs2 ranges (r :) bs3 where @@ -546,19 +548,15 @@ parseByteRanges bs1 = do if i < 0 -- has prefix "-" ("-0" is not valid, but here treated as "0-") then Just (ByteRangeSuffix (negate i), bs3) else do - bs4 <- stripPrefixB "-" bs3 + (w8, bs4) <- B.uncons bs3 + guard $ w8 == _hyphen case B8.readInteger bs4 of Just (j, bs5) | j >= i -> Just (ByteRangeFromTo i j, bs5) _ -> Just (ByteRangeFrom i, bs4) ranges front bs3 | B.null bs3 = Just (front []) | otherwise = do - bs4 <- stripPrefixB "," bs3 + (w8, bs4) <- B.uncons bs3 + guard $ w8 == _comma (r, bs5) <- range bs4 ranges (front . (r :)) bs5 - - -- FIXME: Use 'stripPrefix' from the 'bytestring' package. - -- Might have to update the dependency constraints though. - stripPrefixB x y - | x `B.isPrefixOf` y = Just (B.drop (B.length x) y) - | otherwise = Nothing diff --git a/http-types.cabal b/http-types.cabal index 8758f72..87461b3 100644 --- a/http-types.cabal +++ b/http-types.cabal @@ -37,11 +37,11 @@ Library Network.HTTP.Types.Version GHC-Options: -Wall Build-depends: base >= 4 && < 5, - bytestring >=0.10.4.0 && <1.0, + bytestring >=0.10.8.0 && <1.0, array >=0.2 && <0.6, case-insensitive >=0.2 && <1.3, text >= 0.11.0.2, - word8 + word8 < 0.2 Default-language: Haskell2010 Test-suite spec From 5cc9f9f3c4d98bf540fede029e06f3cf30feb2a2 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Sun, 24 Mar 2024 19:28:15 +0100 Subject: [PATCH 13/14] bit of restructuring and added tests for HTTP version pattern synonyms --- Network/HTTP/Types/Method.hs | 15 ++++++--------- Network/HTTP/Types/Status.hs | 3 ++- Network/HTTP/Types/Version.hs | 22 ++++++++++++---------- test/Network/HTTP/Types/VersionSpec.hs | 11 +++++++++++ 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/Network/HTTP/Types/Method.hs b/Network/HTTP/Types/Method.hs index 3ff0e27..5c56e4f 100644 --- a/Network/HTTP/Types/Method.hs +++ b/Network/HTTP/Types/Method.hs @@ -33,7 +33,7 @@ module Network.HTTP.Types.Method ( where import Control.Arrow ((|||)) -import Data.Array (Array, Ix, assocs, listArray, (!)) +import Data.Array (Array, Ix, listArray, (!)) import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as B8 import Data.Data (Data) @@ -96,6 +96,9 @@ methodPatch = renderStdMethod PATCH -- -- @since 0.2.0 data StdMethod + -- These are ordered by suspected frequency. More popular methods should go first. + -- The reason is that 'methodList' is used with 'lookup'. + -- 'lookup' is probably faster for these few cases than setting up an elaborate data structure. = GET | POST | HEAD @@ -121,17 +124,11 @@ data StdMethod Data ) --- These are ordered by suspected frequency. More popular methods should go first. --- The reason is that methodList is used with lookup. --- lookup is probably faster for these few cases than setting up an elaborate data structure. - --- FIXME: listArray (minBound, maxBound) $ fmap fst methodList methodArray :: Array StdMethod Method -methodArray = listArray (minBound, maxBound) $ map (B8.pack . show) [minBound :: StdMethod .. maxBound] +methodArray = listArray (minBound, maxBound) $ fst <$> methodList --- FIXME: map (\m -> (B8.pack $ show m, m)) [minBound .. maxBound] methodList :: [(Method, StdMethod)] -methodList = map (\(a, b) -> (b, a)) (assocs methodArray) +methodList = map (\m -> (B8.pack $ show m, m)) [minBound :: StdMethod .. maxBound] -- | Convert a method 'ByteString' to a 'StdMethod' if possible. -- diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index fe8267f..d648ff6 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -176,7 +176,7 @@ instance Eq Status where instance Ord Status where compare = compare `on` statusCode --- | Be advised, that when using the \"enumFrom*\" family of methods or +-- | Be advised, that when using the @enumFrom*@ family of methods or -- ranges in lists, it will generate all possible status codes. -- -- E.g. @[status100 .. status200]@ generates 'Status'es of @100, 101, 102 .. 198, 199, 200@ @@ -845,3 +845,4 @@ statusIsServerError = statusIs 5 statusIs :: Int -> Status -> Bool statusIs i = (== i) . (`div` 100) . statusCode +{-# INLINE statusIs #-} diff --git a/Network/HTTP/Types/Version.hs b/Network/HTTP/Types/Version.hs index ec730bc..cc01a29 100644 --- a/Network/HTTP/Types/Version.hs +++ b/Network/HTTP/Types/Version.hs @@ -109,18 +109,20 @@ pattern Http30 <- HttpVersion 3 0 -- -- @since 0.13 parseHttpVersion :: ByteString -> Either String HttpVersion --- This order is from most likely to be checked with this function. --- HTTP/2 and /3 don't use this format as the version string, they use "h2" and "h3". -parseHttpVersion "HTTP/1.1" = Right http11 -parseHttpVersion "HTTP/1.0" = Right http10 -parseHttpVersion "HTTP/2.0" = Right http20 -parseHttpVersion "HTTP/3.0" = Right http30 -parseHttpVersion "HTTP/0.9" = Right http09 parseHttpVersion bs = do rest <- note "Not an HTTP protocol version" $ B.stripPrefix "HTTP/" bs - (ds, more) <- withDigits "major" rest - let majV = unsafeDigitsToInt ds - HttpVersion majV <$> getMinorVersion more + case rest of + -- This order is from most likely to be checked with this function. + -- HTTP/2 and /3 don't use this format as the version string, they use "h2" and "h3". + "1.1" -> Right http11 + "1.0" -> Right http10 + "2.0" -> Right http20 + "3.0" -> Right http30 + "0.9" -> Right http09 + _ -> do + (ds, more) <- withDigits "major" rest + let majV = unsafeDigitsToInt ds + HttpVersion majV <$> getMinorVersion more where note s = maybe (Left s) Right withDigits s rest = diff --git a/test/Network/HTTP/Types/VersionSpec.hs b/test/Network/HTTP/Types/VersionSpec.hs index cddf1f7..3ced369 100644 --- a/test/Network/HTTP/Types/VersionSpec.hs +++ b/test/Network/HTTP/Types/VersionSpec.hs @@ -52,8 +52,19 @@ checkVersion (str, v1, v2) = show v1 `shouldBe` str renderHttpVersion v1 `shouldBe` bsStr parseHttpVersion bsStr `shouldBe` Right v1 + patternCheck str v1 where bsStr = fromString str instance Arbitrary HttpVersion where arbitrary = HttpVersion <$> arbitrary <*> arbitrary + +patternCheck :: String -> HttpVersion -> Expectation +patternCheck s v = + case v of + Http09 -> s `shouldBe` "HTTP/0.9" + Http10 -> s `shouldBe` "HTTP/1.0" + Http11 -> s `shouldBe` "HTTP/1.1" + Http20 -> s `shouldBe` "HTTP/2.0" + Http30 -> s `shouldBe` "HTTP/3.0" + _ -> expectationFailure $ s ++ " does not have a Pattern Synonym" From 0b475d55af403489c9145c0e34853525b5c95078 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Sun, 24 Mar 2024 19:38:02 +0100 Subject: [PATCH 14/14] README: small update --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e6bd06e..e9b5ddd 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,15 @@ The goal of this library is to have one location for any library, package or project to base their general HTTP types on for better interoperability. +This library also provides some utility functions for parsing and rendering HTTP types. + ### This library provides basic types for the following: -* HTTP versions (e.g. `HTTP/1.1`) -* HTTP methods (e.g. `GET`) -* HTTP headers (e.g. `Content-Type`) -* HTTP statusses (e.g. `404`) +* HTTP versions (e.g. `HTTP/1.1`) in [`Network.HTTP.Types.Version`](https://hackage.haskell.org/package/http-types/docs/Network-HTTP-Types-Version.html) +* HTTP methods (e.g. `GET`) in [`Network.HTTP.Types.Method`](https://hackage.haskell.org/package/http-types/docs/Network-HTTP-Types-Method.html) +* HTTP headers (e.g. `Content-Type`) in [`Network.HTTP.Types.Header`](https://hackage.haskell.org/package/http-types/docs/Network-HTTP-Types-Header.html) +* HTTP statusses (e.g. `404`) in [`Network.HTTP.Types.Status`](https://hackage.haskell.org/package/http-types/docs/Network-HTTP-Types-Status.html) +* HTTP URIs (e.g. paths, query parameters, etc.) in [`Network.HTTP.Types.URI`](https://hackage.haskell.org/package/http-types/docs/Network-HTTP-Types-URI.html) -This library also contains some utility functions, e.g. related to URI handling, -that are not necessarily restricted in use to HTTP, but the scope is restricted -to things that are useful inside HTTP, i.e. no FTP URI parsing. +The main module [`Network.HTTP.Types`](https://hackage.haskell.org/package/http-types/docs/Network-HTTP-Types.html) +exports everything as well, so you don't have to import the modules separately if you don't want to.