From da83034e11a05f75a333f704b90b1f32c9892662 Mon Sep 17 00:00:00 2001 From: Arthur Xavier Date: Fri, 4 Nov 2022 12:16:26 -0300 Subject: [PATCH 1/9] =?UTF-8?q?FBCM-5157=C2=A0Refactor=20Headers=20instanc?= =?UTF-8?q?es=20and=20use=20`show`=20only=20for=20debugging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/HTTPure/Headers.purs | 20 +++++++++----------- test/Test/HTTPure/HeadersSpec.purs | 20 +++++++------------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/HTTPure/Headers.purs b/src/HTTPure/Headers.purs index 049b323..b330661 100644 --- a/src/HTTPure/Headers.purs +++ b/src/HTTPure/Headers.purs @@ -10,10 +10,11 @@ module HTTPure.Headers import Prelude import Data.Foldable (foldl) -import Data.FoldableWithIndex (foldMapWithIndex) +import Data.Generic.Rep (class Generic) import Data.Map (Map, insert, singleton, union) import Data.Map (empty) as Map import Data.Newtype (class Newtype, unwrap) +import Data.Show.Generic (genericShow) import Data.String.CaseInsensitive (CaseInsensitiveString(CaseInsensitiveString)) import Data.TraversableWithIndex (traverseWithIndex) import Data.Tuple (Tuple(Tuple)) @@ -28,24 +29,21 @@ newtype Headers = Headers (Map CaseInsensitiveString String) derive instance newtypeHeaders :: Newtype Headers _ +derive instance genericHeaders :: Generic Headers _ + -- | Given a string, return a `Maybe` containing the value of the matching -- | header, if there is any. -instance lookup :: Lookup Headers String String where +instance lookupHeaders :: Lookup Headers String String where lookup (Headers headers') key = headers' !! key --- | Allow a `Headers` to be represented as a string. This string is formatted --- | in HTTP headers format. -instance show :: Show Headers where - show (Headers headers') = foldMapWithIndex showField headers' <> "\n" - where - showField key value = unwrap key <> ": " <> value <> "\n" +instance showHeaders :: Show Headers where + show = genericShow -- | Compare two `Headers` objects by comparing the underlying `Objects`. -instance eq :: Eq Headers where - eq (Headers a) (Headers b) = eq a b +derive newtype instance eqHeaders :: Eq Headers -- | Allow one `Headers` objects to be appended to another. -instance semigroup :: Semigroup Headers where +instance semigroupHeaders :: Semigroup Headers where append (Headers a) (Headers b) = Headers $ union b a -- | Get the headers out of a HTTP `Request` object. diff --git a/test/Test/HTTPure/HeadersSpec.purs b/test/Test/HTTPure/HeadersSpec.purs index 368457a..5745037 100644 --- a/test/Test/HTTPure/HeadersSpec.purs +++ b/test/Test/HTTPure/HeadersSpec.purs @@ -2,10 +2,12 @@ module Test.HTTPure.HeadersSpec where import Prelude -import Data.Maybe (Maybe(Nothing, Just)) -import Data.Tuple (Tuple(Tuple)) +import Data.Map as Data.Map +import Data.Maybe (Maybe(..)) +import Data.String.CaseInsensitive (CaseInsensitiveString(..)) +import Data.Tuple (Tuple(..)) import Effect.Class (liftEffect) -import HTTPure.Headers (empty, header, headers, read, write) +import HTTPure.Headers (Headers(..), empty, header, headers, read, write) import HTTPure.Lookup ((!!)) import Test.HTTPure.TestHelpers ((?=)) import Test.HTTPure.TestHelpers as TestHelpers @@ -32,13 +34,6 @@ lookupSpec = it "is Nothing" do ((empty !! "X-Test") :: Maybe String) ?= Nothing -showSpec :: TestHelpers.Test -showSpec = - describe "show" do - it "is a string representing the headers in HTTP format" do - let mock = header "Test1" "1" <> header "Test2" "2" - show mock ?= "Test1: 1\nTest2: 2\n\n" - eqSpec :: TestHelpers.Test eqSpec = describe "eq" do @@ -104,13 +99,13 @@ emptySpec :: TestHelpers.Test emptySpec = describe "empty" do it "is an empty Map in an empty Headers" do - show empty ?= "\n" + empty ?= Headers Data.Map.empty headerSpec :: TestHelpers.Test headerSpec = describe "header" do it "creates a singleton Headers" do - show (header "X-Test" "test") ?= "X-Test: test\n\n" + header "X-Test" "test" ?= Headers (Data.Map.singleton (CaseInsensitiveString "X-Test") "test") headersFunctionSpec :: TestHelpers.Test headersFunctionSpec = @@ -128,7 +123,6 @@ headersSpec :: TestHelpers.Test headersSpec = describe "Headers" do lookupSpec - showSpec eqSpec appendSpec readSpec From ad280c40719cee3d38590b9d2e2a474ac704971c Mon Sep 17 00:00:00 2001 From: Arthur Xavier Date: Mon, 7 Nov 2022 10:51:53 -0300 Subject: [PATCH 2/9] FBCM-5157 Add `HTTPure.MultiHeaders` This module contains the `MultiHeaders` type which encapsulates the concept of duplicated headers (or headers with multiple values, like "Set-Cookie"). --- src/HTTPure/MultiHeaders.purs | 107 +++++++++++++++++++ test/Test/HTTPure/MultiHeadersSpec.purs | 133 ++++++++++++++++++++++++ test/Test/Main.purs | 2 + 3 files changed, 242 insertions(+) create mode 100644 src/HTTPure/MultiHeaders.purs create mode 100644 test/Test/HTTPure/MultiHeadersSpec.purs diff --git a/src/HTTPure/MultiHeaders.purs b/src/HTTPure/MultiHeaders.purs new file mode 100644 index 0000000..bddbfc5 --- /dev/null +++ b/src/HTTPure/MultiHeaders.purs @@ -0,0 +1,107 @@ +module HTTPure.MultiHeaders + ( MultiHeaders(..) + , empty + , header + , header' + , headers + , headers' + , read + , write + ) where + +import Prelude + +import Data.Array.NonEmpty (NonEmptyArray) +import Data.Array.NonEmpty as Data.Array.NonEmpty +import Data.Foldable (foldl) +import Data.Generic.Rep (class Generic) +import Data.Map (Map) +import Data.Map as Data.Map +import Data.Maybe (Maybe(..)) +import Data.Newtype (class Newtype, unwrap) +import Data.Show.Generic (genericShow) +import Data.String.CaseInsensitive (CaseInsensitiveString(CaseInsensitiveString)) +import Data.TraversableWithIndex (traverseWithIndex) +import Data.Tuple (Tuple(Tuple)) +import Effect (Effect) +import Foreign.Object (Object, fold) +import HTTPure.Lookup (class Lookup, (!!)) +import Node.HTTP (Request, Response, setHeaders) +import Unsafe.Coerce (unsafeCoerce) + +-- | The `MultiHeaders` type represets the set of headers in a HTTP request or +-- | response read in a way such that every header name maps to a non-empty list +-- | of header values. This is useful for headers that may have multiple values, +-- | such as "Set-Cookie". +newtype MultiHeaders = MultiHeaders (Map CaseInsensitiveString (NonEmptyArray String)) + +derive instance newtypeMultiHeaders :: Newtype MultiHeaders _ + +derive instance genericMultiHeaders :: Generic MultiHeaders _ + +-- | Given a string, return a `Maybe` containing the values of the matching +-- | header, if there is any. +instance lookupMultiHeaders :: Lookup MultiHeaders String (NonEmptyArray String) where + lookup (MultiHeaders headersMap) key = headersMap !! key + +instance showMultiHeaders :: Show MultiHeaders where + show = genericShow + +-- | Compare two `MultiHeaders` objects by comparing the underlying `Objects`. +derive newtype instance eqMultiHeaders :: Eq MultiHeaders + +-- | Allow one `MultiHeaders` objects to be appended to another. +instance semigroupMultiHeaders :: Semigroup MultiHeaders where + append (MultiHeaders a) (MultiHeaders b) = + MultiHeaders $ Data.Map.unionWith append a b + +-- | Return a `MultiHeaders` containing nothing. +empty :: MultiHeaders +empty = MultiHeaders Data.Map.empty + +-- | Create a singleton header from a key-value pair. +header :: String -> String -> MultiHeaders +header key = header' key <<< Data.Array.NonEmpty.singleton + +-- | Create a singleton header from a key-values pair. +header' :: String -> NonEmptyArray String -> MultiHeaders +header' key = MultiHeaders <<< Data.Map.singleton (CaseInsensitiveString key) + +-- | Convert an `Array` of `Tuples` of 2 `Strings` to a `MultiHeaders` object. +headers :: Array (Tuple String String) -> MultiHeaders +headers = headers' <<< map (map pure) + +-- | Convert an `Array` of `Tuples` of 2 `Strings` to a `MultiHeaders` object. +headers' :: Array (Tuple String (NonEmptyArray String)) -> MultiHeaders +headers' = foldl insertField Data.Map.empty >>> MultiHeaders + where + insertField x (Tuple key values) = Data.Map.insertWith append (CaseInsensitiveString key) values x + +-- | Read the headers out of a HTTP `Request` object and parse duplicated +-- | headers as a list (instead of comma-separated values, as with +-- | `HTTPure.Headers.read`). +read :: Request -> MultiHeaders +read = requestHeadersDistinct >>> fold insertField Data.Map.empty >>> MultiHeaders + where + insertField :: + forall a. + Map CaseInsensitiveString (NonEmptyArray a) -> + String -> + Array a -> + Map CaseInsensitiveString (NonEmptyArray a) + insertField headersMap key array = case Data.Array.NonEmpty.fromArray array of + Nothing -> headersMap + Just nonEmptyArray -> Data.Map.insert (CaseInsensitiveString key) nonEmptyArray headersMap + + -- | Similar to `Node.HTTP.requestHeaders`, but there is no join logic and the + -- | values are always arrays of strings, even for headers received just once. + -- | See https://nodejs.org/api/http.html#messageheadersdistinct. + requestHeadersDistinct :: Request -> Object (Array String) + requestHeadersDistinct = _.headersDistinct <<< unsafeCoerce + +-- | Given an HTTP `Response` and a `MultiHeaders` object, return an effect that will +-- | write the `MultiHeaders` to the `Response`. +write :: Response -> MultiHeaders -> Effect Unit +write response (MultiHeaders headersMap) = void $ traverseWithIndex writeField headersMap + where + writeField key = setHeaders response (unwrap key) <<< Data.Array.NonEmpty.toArray diff --git a/test/Test/HTTPure/MultiHeadersSpec.purs b/test/Test/HTTPure/MultiHeadersSpec.purs new file mode 100644 index 0000000..892fb38 --- /dev/null +++ b/test/Test/HTTPure/MultiHeadersSpec.purs @@ -0,0 +1,133 @@ +module Test.HTTPure.MultiHeadersSpec where + +import Prelude + +import Data.Array.NonEmpty (NonEmptyArray) +import Data.Map as Data.Map +import Data.Maybe (Maybe(..)) +import Data.String.CaseInsensitive (CaseInsensitiveString(..)) +import Data.Tuple (Tuple(..)) +import Effect.Class (liftEffect) +import HTTPure.Lookup ((!!)) +import HTTPure.MultiHeaders (MultiHeaders(..), empty, header, header', headers, read, write) +import Test.HTTPure.TestHelpers ((?=)) +import Test.HTTPure.TestHelpers as TestHelpers +import Test.Spec (describe, it) + +lookupSpec :: TestHelpers.Test +lookupSpec = + describe "lookup" do + describe "when the string is in the header set" do + describe "when searching with lowercase" do + it "is Just the string" do + header "x-test" "test" !! "x-test" ?= Just (pure "test") + describe "when searching with uppercase" do + it "is Just the string" do + header "x-test" "test" !! "X-Test" ?= Just (pure "test") + describe "when the string is uppercase" do + describe "when searching with lowercase" do + it "is Just the string" do + header "X-Test" "test" !! "x-test" ?= Just (pure "test") + describe "when searching with uppercase" do + it "is Just the string" do + header "X-Test" "test" !! "X-Test" ?= Just (pure "test") + describe "when the string is not in the header set" do + it "is Nothing" do + ((empty !! "X-Test") :: Maybe (NonEmptyArray String)) ?= Nothing + +eqSpec :: TestHelpers.Test +eqSpec = + describe "eq" do + describe "when the two MultiHeaders contain the same keys and values" do + it "is true" do + header "Test1" "test1" == header "Test1" "test1" ?= true + describe "when the two MultiHeaders contain different keys and values" do + it "is false" do + header "Test1" "test1" == header "Test2" "test2" ?= false + describe "when the two MultiHeaders contain only different values" do + it "is false" do + header "Test1" "test1" == header "Test1" "test2" ?= false + describe "when the one MultiHeaders contains additional keys and values" do + it "is false" do + let mock = header "Test1" "1" <> header "Test2" "2" + header "Test1" "1" == mock ?= false + +appendSpec :: TestHelpers.Test +appendSpec = + describe "append" do + describe "when there are multiple keys" do + it "appends the headers correctly" do + let + mock1 = header "Test1" "1" <> header "Test2" "2" + mock2 = header "Test3" "3" <> header "Test4" "4" + mock3 = + headers + [ Tuple "Test1" "1" + , Tuple "Test2" "2" + , Tuple "Test3" "3" + , Tuple "Test4" "4" + ] + mock1 <> mock2 ?= mock3 + describe "when there is a duplicated key" do + it "appends the multiple values" do + let mock = header "Test" "foo" <> header "Test" "bar" + mock ?= header' "Test" (pure "foo" <> pure "bar") + +readSpec :: TestHelpers.Test +readSpec = + describe "read" do + describe "with no headers" do + it "is an empty Map" do + request <- TestHelpers.mockRequest "" "" "" "" [] + read request ?= empty + describe "with headers" do + it "is a Map with the contents of the headers" do + let testHeader = [ Tuple "X-Test" "test" ] + request <- TestHelpers.mockRequest "" "" "" "" testHeader + read request ?= headers testHeader + +writeSpec :: TestHelpers.Test +writeSpec = + describe "write" do + it "writes the headers to the response" do + header <- liftEffect do + mock <- TestHelpers.mockResponse + write mock $ headers [ Tuple "X-Test" "test1", Tuple "X-Test" "test2" ] + pure $ TestHelpers.getResponseMultiHeader "X-Test" mock + header ?= [ "test1", "test2" ] + +emptySpec :: TestHelpers.Test +emptySpec = + describe "empty" do + it "is an empty Map in an empty MultiHeaders" do + empty ?= MultiHeaders Data.Map.empty + +headerSpec :: TestHelpers.Test +headerSpec = + describe "header" do + it "creates a singleton MultiHeaders" do + header "X-Test" "test" ?= MultiHeaders (Data.Map.singleton (CaseInsensitiveString "X-Test") (pure "test")) + +headersFunctionSpec :: TestHelpers.Test +headersFunctionSpec = + describe "headers" do + it "is equivalent to using header with <>" do + let + expected = header "X-Test-1" "1" <> header "X-Test-2" "2" + test = headers + [ Tuple "X-Test-1" "1" + , Tuple "X-Test-2" "2" + ] + test ?= expected + +multiHeadersSpec :: TestHelpers.Test +multiHeadersSpec = + describe "MultiHeaders" do + lookupSpec + eqSpec + appendSpec + readSpec + writeSpec + emptySpec + headerSpec + headersFunctionSpec diff --git a/test/Test/Main.purs b/test/Test/Main.purs index 187d15a..1dc01e8 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -8,6 +8,7 @@ import Test.HTTPure.HeadersSpec (headersSpec) import Test.HTTPure.IntegrationSpec (integrationSpec) import Test.HTTPure.LookupSpec (lookupSpec) import Test.HTTPure.MethodSpec (methodSpec) +import Test.HTTPure.MultiHeadersSpec (multiHeadersSpec) import Test.HTTPure.PathSpec (pathSpec) import Test.HTTPure.QuerySpec (querySpec) import Test.HTTPure.RequestSpec (requestSpec) @@ -27,6 +28,7 @@ main = launchAff_ $ runSpec [ specReporter ] $ describe "HTTPure" do headersSpec lookupSpec methodSpec + multiHeadersSpec pathSpec querySpec requestSpec From ac1ee8efc9d1b0ec4fd7cb3f556d0160cc6e0cba Mon Sep 17 00:00:00 2001 From: Arthur Xavier Date: Mon, 7 Nov 2022 10:52:48 -0300 Subject: [PATCH 3/9] FBCM-5157 Allow duplicated headers in request and response We add `multiHeaders` fields to both `HTTPure.Request.Request` and `HTTPure.Response.Response`, so that headers with multiple values can be both read from requests and written to responses. We've chosen this incremental approach instead of changing `HTTPure.Headers` to avoid a large breaking change. --- src/HTTPure/Request.purs | 5 ++++- src/HTTPure/Response.purs | 7 ++++++- test/Test/HTTPure/RequestSpec.purs | 10 +++++++++- test/Test/HTTPure/ResponseSpec.purs | 11 +++++++++++ test/Test/HTTPure/TestHelpers.js | 3 ++- test/Test/HTTPure/TestHelpers.purs | 15 ++++++++++++--- 6 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/HTTPure/Request.purs b/src/HTTPure/Request.purs index ae5b245..22e9064 100644 --- a/src/HTTPure/Request.purs +++ b/src/HTTPure/Request.purs @@ -16,6 +16,8 @@ import HTTPure.Headers (Headers) import HTTPure.Headers (read) as Headers import HTTPure.Method (Method) import HTTPure.Method (read) as Method +import HTTPure.MultiHeaders (MultiHeaders) +import HTTPure.MultiHeaders as HTTPure.MultiHeaders import HTTPure.Path (Path) import HTTPure.Path (read) as Path import HTTPure.Query (Query) @@ -33,6 +35,7 @@ type Request = , path :: Path , query :: Query , headers :: Headers + , multiHeaders :: MultiHeaders , body :: RequestBody , httpVersion :: Version , url :: String @@ -60,8 +63,8 @@ fromHTTPRequest request = do , path: Path.read request , query: Query.read request , headers: Headers.read request + , multiHeaders: HTTPure.MultiHeaders.read request , body , httpVersion: Version.read request , url: requestURL request } - diff --git a/src/HTTPure/Response.purs b/src/HTTPure/Response.purs index 772561a..bac48ab 100644 --- a/src/HTTPure/Response.purs +++ b/src/HTTPure/Response.purs @@ -141,6 +141,8 @@ import Effect.Class (class MonadEffect, liftEffect) import HTTPure.Body (class Body, defaultHeaders, write) import HTTPure.Headers (Headers, empty) import HTTPure.Headers (write) as Headers +import HTTPure.MultiHeaders (MultiHeaders) +import HTTPure.MultiHeaders as HTTPure.MultiHeaders import HTTPure.Status (Status) import HTTPure.Status ( accepted @@ -216,6 +218,7 @@ type ResponseM = Aff Response type Response = { status :: Status , headers :: Headers + , multiHeaders :: MultiHeaders , writeBody :: HTTP.Response -> Aff Unit } @@ -223,9 +226,10 @@ type Response = -- | a monad encapsulating writing the HTTPure `Response` to the HTTP `Response` -- | and closing the HTTP `Response`. send :: forall m. MonadEffect m => MonadAff m => HTTP.Response -> Response -> m Unit -send httpresponse { status, headers, writeBody } = do +send httpresponse { status, headers, multiHeaders, writeBody } = do liftEffect $ Status.write httpresponse status liftEffect $ Headers.write httpresponse headers + liftEffect $ HTTPure.MultiHeaders.write httpresponse multiHeaders liftAff $ writeBody httpresponse -- | For custom response statuses or providing a body for response codes that @@ -247,6 +251,7 @@ response' status headers body = liftEffect do pure { status , headers: defaultHeaders' <> headers + , multiHeaders: HTTPure.MultiHeaders.empty , writeBody: write body } diff --git a/test/Test/HTTPure/RequestSpec.purs b/test/Test/HTTPure/RequestSpec.purs index 61683be..bb763d6 100644 --- a/test/Test/HTTPure/RequestSpec.purs +++ b/test/Test/HTTPure/RequestSpec.purs @@ -7,6 +7,7 @@ import Foreign.Object (singleton) import HTTPure.Body (toString) import HTTPure.Headers (headers) import HTTPure.Method (Method(Post)) +import HTTPure.MultiHeaders as HTTPure.MultiHeaders import HTTPure.Request (fromHTTPRequest, fullPath) import HTTPure.Version (Version(HTTP1_1)) import Test.HTTPure.TestHelpers (Test, mockRequest, (?=)) @@ -27,6 +28,9 @@ fromHTTPRequestSpec = it "contains the correct headers" do mock <- mockRequest' mock.headers ?= headers mockHeaders + it "contains the correct multi-headers" do + mock <- mockRequest' + mock.multiHeaders ?= HTTPure.MultiHeaders.headers mockHeaders it "contains the correct body" do mockBody <- mockRequest' >>= _.body >>> toString mockBody ?= "body" @@ -34,7 +38,11 @@ fromHTTPRequestSpec = mock <- mockRequest' mock.httpVersion ?= HTTP1_1 where - mockHeaders = [ Tuple "Test" "test" ] + mockHeaders = + [ Tuple "Test" "test" + , Tuple "TestMulti" "test1" + , Tuple "TestMulti" "test2" + ] mockHTTPRequest = mockRequest "1.1" "POST" "/test?a=b" "body" mockHeaders diff --git a/test/Test/HTTPure/ResponseSpec.purs b/test/Test/HTTPure/ResponseSpec.purs index 4269f65..069e638 100644 --- a/test/Test/HTTPure/ResponseSpec.purs +++ b/test/Test/HTTPure/ResponseSpec.purs @@ -7,6 +7,7 @@ import Effect.Aff (makeAff, nonCanceler) import Effect.Class (liftEffect) import HTTPure.Body (defaultHeaders) import HTTPure.Headers (header) +import HTTPure.MultiHeaders as HTTPure.MultiHeaders import HTTPure.Response (emptyResponse, emptyResponse', response, response', send) import Node.Encoding (Encoding(UTF8)) import Node.HTTP (responseAsStream) @@ -15,6 +16,7 @@ import Test.HTTPure.TestHelpers ( Test , getResponseBody , getResponseHeader + , getResponseMultiHeader , getResponseStatus , mockResponse , (?=) @@ -28,6 +30,9 @@ sendSpec = mockResponse' = { status: 123 , headers: header "Test" "test" + , multiHeaders: + HTTPure.MultiHeaders.header "Set-Cookie" "test1" + <> HTTPure.MultiHeaders.header "Set-Cookie" "test2" , writeBody: \response -> makeAff \done -> do stream <- pure $ responseAsStream response @@ -40,6 +45,12 @@ sendSpec = send httpResponse mockResponse' pure $ getResponseHeader "Test" httpResponse header ?= "test" + it "writes the multi-headers" do + header <- do + httpResponse <- liftEffect mockResponse + send httpResponse mockResponse' + pure $ getResponseMultiHeader "Set-Cookie" httpResponse + header ?= [ "test1", "test2" ] it "writes the status" do status <- do httpResponse <- liftEffect mockResponse diff --git a/test/Test/HTTPure/TestHelpers.js b/test/Test/HTTPure/TestHelpers.js index c7e5316..42f47f1 100644 --- a/test/Test/HTTPure/TestHelpers.js +++ b/test/Test/HTTPure/TestHelpers.js @@ -9,7 +9,8 @@ export const mockRequestImpl = httpVersion => method => url => body => headers = }); stream.method = method; stream.url = url; - stream.headers = headers; + stream.headers = Object.fromEntries(Object.entries(headers).map(([key, values]) => [key, values[values.length - 1]])); + stream.headersDistinct = headers; stream.httpVersion = httpVersion; return stream; diff --git a/test/Test/HTTPure/TestHelpers.purs b/test/Test/HTTPure/TestHelpers.purs index 01cdcbb..ebf72c3 100644 --- a/test/Test/HTTPure/TestHelpers.purs +++ b/test/Test/HTTPure/TestHelpers.purs @@ -3,6 +3,7 @@ module Test.HTTPure.TestHelpers where import Prelude import Data.Array (fromFoldable) as Array +import Data.Array.NonEmpty (NonEmptyArray) import Data.Either (Either(Right)) import Data.List (List(Nil, Cons), reverse) import Data.Maybe (fromMaybe) @@ -14,7 +15,7 @@ import Effect.Aff (Aff, makeAff, nonCanceler) import Effect.Class (liftEffect) import Effect.Ref (modify_, new, read) import Foreign.Object (Object, lookup) -import Foreign.Object (fromFoldable) as Object +import Foreign.Object as Object import Node.Buffer (Buffer, concat, create, fromString) import Node.Buffer (toString) as Buffer import Node.Encoding (Encoding(UTF8)) @@ -204,7 +205,7 @@ foreign import mockRequestImpl :: String -> String -> String -> - Object String -> + Object (NonEmptyArray String) -> Effect Request -- | Mock an HTTP Request object @@ -215,7 +216,11 @@ mockRequest :: String -> Array (Tuple String String) -> Aff Request -mockRequest httpVersion method url body = liftEffect <<< mockRequestImpl httpVersion method url body <<< Object.fromFoldable +mockRequest httpVersion method url body = + liftEffect + <<< mockRequestImpl httpVersion method url body + <<< Object.fromFoldableWith (flip append) + <<< map (map pure) -- | Mock an HTTP Response object foreign import mockResponse :: Effect HTTP.Response @@ -237,5 +242,9 @@ getResponseHeaders = unsafeCoerce <<< _.headers <<< unsafeCoerce getResponseHeader :: String -> HTTP.Response -> String getResponseHeader header = fromMaybe "" <<< lookup header <<< getResponseHeaders +-- | Get the current value for the multi-header on the HTTP Response object. +getResponseMultiHeader :: String -> HTTP.Response -> Array String +getResponseMultiHeader header = fromMaybe [] <<< lookup header <<< _.headers <<< unsafeCoerce + -- | Create a stream out of a string. foreign import stringToStream :: String -> Readable () From 4d2a92671420bab2aa476f1d850c83fc26860937 Mon Sep 17 00:00:00 2001 From: Arthur Xavier Date: Mon, 7 Nov 2022 17:18:32 -0300 Subject: [PATCH 4/9] FBCM-5157 Recover the old `show` functionality under `toString` Some people could have been relying on the old `show` functionality, so we bring it back under the `toString` name. --- src/HTTPure/Headers.purs | 10 ++++++++++ src/HTTPure/MultiHeaders.purs | 19 +++++++++++++++++-- test/Test/HTTPure/HeadersSpec.purs | 10 +++++++++- test/Test/HTTPure/MultiHeadersSpec.purs | 16 +++++++++++++++- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/HTTPure/Headers.purs b/src/HTTPure/Headers.purs index b330661..3dc6f5f 100644 --- a/src/HTTPure/Headers.purs +++ b/src/HTTPure/Headers.purs @@ -4,12 +4,14 @@ module HTTPure.Headers , headers , header , read + , toString , write ) where import Prelude import Data.Foldable (foldl) +import Data.FoldableWithIndex (foldMapWithIndex) import Data.Generic.Rep (class Generic) import Data.Map (Map, insert, singleton, union) import Data.Map (empty) as Map @@ -72,3 +74,11 @@ headers = foldl insertField Map.empty >>> Headers -- | Create a singleton header from a key-value pair. header :: String -> String -> Headers header key = singleton (CaseInsensitiveString key) >>> Headers + +-- | Allow a `Headers` to be represented as a string. This string is formatted +-- | in HTTP headers format. +toString :: Headers -> String +toString (Headers headersMap) = foldMapWithIndex showField headersMap <> "\n" + where + showField :: CaseInsensitiveString -> String -> String + showField key value = unwrap key <> ": " <> value <> "\n" diff --git a/src/HTTPure/MultiHeaders.purs b/src/HTTPure/MultiHeaders.purs index bddbfc5..2da2500 100644 --- a/src/HTTPure/MultiHeaders.purs +++ b/src/HTTPure/MultiHeaders.purs @@ -6,6 +6,7 @@ module HTTPure.MultiHeaders , headers , headers' , read + , toString , write ) where @@ -14,11 +15,12 @@ import Prelude import Data.Array.NonEmpty (NonEmptyArray) import Data.Array.NonEmpty as Data.Array.NonEmpty import Data.Foldable (foldl) +import Data.FoldableWithIndex (foldMapWithIndex) import Data.Generic.Rep (class Generic) import Data.Map (Map) import Data.Map as Data.Map import Data.Maybe (Maybe(..)) -import Data.Newtype (class Newtype, unwrap) +import Data.Newtype (class Newtype, un) import Data.Show.Generic (genericShow) import Data.String.CaseInsensitive (CaseInsensitiveString(CaseInsensitiveString)) import Data.TraversableWithIndex (traverseWithIndex) @@ -99,9 +101,22 @@ read = requestHeadersDistinct >>> fold insertField Data.Map.empty >>> MultiHeade requestHeadersDistinct :: Request -> Object (Array String) requestHeadersDistinct = _.headersDistinct <<< unsafeCoerce +-- | Allow a `MultiHeaders` to be represented as a string. This string is +-- | formatted in HTTP headers format. +toString :: MultiHeaders -> String +toString (MultiHeaders headersMap) = foldMapWithIndex showField headersMap <> "\n" + where + showField :: CaseInsensitiveString -> NonEmptyArray String -> String + showField key values = + let + separator :: String + separator = if key == CaseInsensitiveString "Set-Cookie" then "; " else ", " + in + un CaseInsensitiveString key <> ": " <> Data.Foldable.intercalate separator values <> "\n" + -- | Given an HTTP `Response` and a `MultiHeaders` object, return an effect that will -- | write the `MultiHeaders` to the `Response`. write :: Response -> MultiHeaders -> Effect Unit write response (MultiHeaders headersMap) = void $ traverseWithIndex writeField headersMap where - writeField key = setHeaders response (unwrap key) <<< Data.Array.NonEmpty.toArray + writeField key = setHeaders response (un CaseInsensitiveString key) <<< Data.Array.NonEmpty.toArray diff --git a/test/Test/HTTPure/HeadersSpec.purs b/test/Test/HTTPure/HeadersSpec.purs index 5745037..b34a1c8 100644 --- a/test/Test/HTTPure/HeadersSpec.purs +++ b/test/Test/HTTPure/HeadersSpec.purs @@ -7,7 +7,7 @@ import Data.Maybe (Maybe(..)) import Data.String.CaseInsensitive (CaseInsensitiveString(..)) import Data.Tuple (Tuple(..)) import Effect.Class (liftEffect) -import HTTPure.Headers (Headers(..), empty, header, headers, read, write) +import HTTPure.Headers (Headers(..), empty, header, headers, read, toString, write) import HTTPure.Lookup ((!!)) import Test.HTTPure.TestHelpers ((?=)) import Test.HTTPure.TestHelpers as TestHelpers @@ -119,6 +119,13 @@ headersFunctionSpec = ] test ?= expected +toStringSpec :: TestHelpers.Test +toStringSpec = + describe "toString" do + it "is a string representing the headers in HTTP format" do + let mock = header "Test1" "1" <> header "Test2" "2" + toString mock ?= "Test1: 1\nTest2: 2\n\n" + headersSpec :: TestHelpers.Test headersSpec = describe "Headers" do @@ -130,3 +137,4 @@ headersSpec = emptySpec headerSpec headersFunctionSpec + toStringSpec diff --git a/test/Test/HTTPure/MultiHeadersSpec.purs b/test/Test/HTTPure/MultiHeadersSpec.purs index 892fb38..374b104 100644 --- a/test/Test/HTTPure/MultiHeadersSpec.purs +++ b/test/Test/HTTPure/MultiHeadersSpec.purs @@ -9,7 +9,7 @@ import Data.String.CaseInsensitive (CaseInsensitiveString(..)) import Data.Tuple (Tuple(..)) import Effect.Class (liftEffect) import HTTPure.Lookup ((!!)) -import HTTPure.MultiHeaders (MultiHeaders(..), empty, header, header', headers, read, write) +import HTTPure.MultiHeaders (MultiHeaders(..), empty, header, header', headers, read, toString, write) import Test.HTTPure.TestHelpers ((?=)) import Test.HTTPure.TestHelpers as TestHelpers import Test.Spec (describe, it) @@ -120,6 +120,19 @@ headersFunctionSpec = ] test ?= expected +toStringSpec :: TestHelpers.Test +toStringSpec = + describe "toString" do + it "is a string representing the headers in HTTP format" do + let mock = header "Test1" "1" <> header "Test2" "2" + toString mock ?= "Test1: 1\nTest2: 2\n\n" + it "separates duplicate headers with a comma" do + let mock = header "Test1" "1" <> header "Test1" "2" <> header "Test2" "2" + toString mock ?= "Test1: 1, 2\nTest2: 2\n\n" + it "separates duplicate 'Set-Cookie' headers with a semicolon" do + let mock = header "Test1" "1" <> header "Set-Cookie" "1" <> header "Set-Cookie" "2" + toString mock ?= "Set-Cookie: 1; 2\nTest1: 1\n\n" + multiHeadersSpec :: TestHelpers.Test multiHeadersSpec = describe "MultiHeaders" do @@ -131,3 +144,4 @@ multiHeadersSpec = emptySpec headerSpec headersFunctionSpec + toStringSpec From f97741a7fb1be7cd33a921ec7f0b84bb178a45dd Mon Sep 17 00:00:00 2001 From: Arthur Xavier Date: Mon, 7 Nov 2022 17:19:17 -0300 Subject: [PATCH 5/9] FBCM-5157 Join headers in both `headers` and `multiHeaders` Before this commit, headers that existed in both `headers` and `multiHeaders` were written to the response only with their `multiHeaders` values. We fix this so that they're joined as if they were all added to `multiHeaders`. --- src/HTTPure/MultiHeaders.purs | 7 +++++++ src/HTTPure/Response.purs | 9 ++++++--- test/Test/HTTPure/ResponseSpec.purs | 15 ++++++++++----- test/Test/HTTPure/TestHelpers.purs | 8 ++------ 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/HTTPure/MultiHeaders.purs b/src/HTTPure/MultiHeaders.purs index 2da2500..42756c4 100644 --- a/src/HTTPure/MultiHeaders.purs +++ b/src/HTTPure/MultiHeaders.purs @@ -1,6 +1,7 @@ module HTTPure.MultiHeaders ( MultiHeaders(..) , empty + , fromHeaders , header , header' , headers @@ -15,6 +16,7 @@ import Prelude import Data.Array.NonEmpty (NonEmptyArray) import Data.Array.NonEmpty as Data.Array.NonEmpty import Data.Foldable (foldl) +import Data.Foldable as Data.Foldable import Data.FoldableWithIndex (foldMapWithIndex) import Data.Generic.Rep (class Generic) import Data.Map (Map) @@ -27,6 +29,7 @@ import Data.TraversableWithIndex (traverseWithIndex) import Data.Tuple (Tuple(Tuple)) import Effect (Effect) import Foreign.Object (Object, fold) +import HTTPure.Headers (Headers(..)) import HTTPure.Lookup (class Lookup, (!!)) import Node.HTTP (Request, Response, setHeaders) import Unsafe.Coerce (unsafeCoerce) @@ -61,6 +64,10 @@ instance semigroupMultiHeaders :: Semigroup MultiHeaders where empty :: MultiHeaders empty = MultiHeaders Data.Map.empty +-- | Create a `MultiHeaders` out of a `Headers` value. +fromHeaders :: Headers -> MultiHeaders +fromHeaders = MultiHeaders <<< map pure <<< Data.Map.fromFoldableWithIndex <<< un Headers + -- | Create a singleton header from a key-value pair. header :: String -> String -> MultiHeaders header key = header' key <<< Data.Array.NonEmpty.singleton diff --git a/src/HTTPure/Response.purs b/src/HTTPure/Response.purs index bac48ab..58dd465 100644 --- a/src/HTTPure/Response.purs +++ b/src/HTTPure/Response.purs @@ -140,7 +140,6 @@ import Effect.Aff.Class (class MonadAff, liftAff) import Effect.Class (class MonadEffect, liftEffect) import HTTPure.Body (class Body, defaultHeaders, write) import HTTPure.Headers (Headers, empty) -import HTTPure.Headers (write) as Headers import HTTPure.MultiHeaders (MultiHeaders) import HTTPure.MultiHeaders as HTTPure.MultiHeaders import HTTPure.Status (Status) @@ -225,11 +224,15 @@ type Response = -- | Given an HTTP `Response` and a HTTPure `Response`, this method will return -- | a monad encapsulating writing the HTTPure `Response` to the HTTP `Response` -- | and closing the HTTP `Response`. +-- | +-- | If a header exists in both `headers` and `multiHeaders`, the values will be +-- | joined as if they were all in `multiHeaders`. send :: forall m. MonadEffect m => MonadAff m => HTTP.Response -> Response -> m Unit send httpresponse { status, headers, multiHeaders, writeBody } = do liftEffect $ Status.write httpresponse status - liftEffect $ Headers.write httpresponse headers - liftEffect $ HTTPure.MultiHeaders.write httpresponse multiHeaders + liftEffect + $ HTTPure.MultiHeaders.write httpresponse + $ HTTPure.MultiHeaders.fromHeaders headers <> multiHeaders liftAff $ writeBody httpresponse -- | For custom response statuses or providing a body for response codes that diff --git a/test/Test/HTTPure/ResponseSpec.purs b/test/Test/HTTPure/ResponseSpec.purs index 069e638..63538dd 100644 --- a/test/Test/HTTPure/ResponseSpec.purs +++ b/test/Test/HTTPure/ResponseSpec.purs @@ -15,7 +15,6 @@ import Node.Stream (end, writeString) import Test.HTTPure.TestHelpers ( Test , getResponseBody - , getResponseHeader , getResponseMultiHeader , getResponseStatus , mockResponse @@ -39,18 +38,24 @@ sendSpec = void $ writeString stream UTF8 "test" $ const $ end stream $ const $ done $ Right unit pure nonCanceler } - it "writes the headers" do + it "writes the `headers`" do header <- do httpResponse <- liftEffect mockResponse send httpResponse mockResponse' - pure $ getResponseHeader "Test" httpResponse - header ?= "test" - it "writes the multi-headers" do + pure $ getResponseMultiHeader "Test" httpResponse + header ?= [ "test" ] + it "writes the `multiHeaders`" do header <- do httpResponse <- liftEffect mockResponse send httpResponse mockResponse' pure $ getResponseMultiHeader "Set-Cookie" httpResponse header ?= [ "test1", "test2" ] + it "joins headers that exist in both `headers` and `multiHeaders`" do + header <- do + httpResponse <- liftEffect mockResponse + send httpResponse mockResponse' { headers = header "Set-Cookie" "test0" } + pure $ getResponseMultiHeader "Set-Cookie" httpResponse + header ?= [ "test0", "test1", "test2" ] it "writes the status" do status <- do httpResponse <- liftEffect mockResponse diff --git a/test/Test/HTTPure/TestHelpers.purs b/test/Test/HTTPure/TestHelpers.purs index ebf72c3..75bcc2c 100644 --- a/test/Test/HTTPure/TestHelpers.purs +++ b/test/Test/HTTPure/TestHelpers.purs @@ -234,15 +234,11 @@ getResponseBody = _.body <<< unsafeCoerce getResponseStatus :: HTTP.Response -> Int getResponseStatus = _.statusCode <<< unsafeCoerce --- | Get all current headers on the HTTP Response object. -getResponseHeaders :: HTTP.Response -> Object String -getResponseHeaders = unsafeCoerce <<< _.headers <<< unsafeCoerce - -- | Get the current value for the header on the HTTP Response object. getResponseHeader :: String -> HTTP.Response -> String -getResponseHeader header = fromMaybe "" <<< lookup header <<< getResponseHeaders +getResponseHeader header = fromMaybe "" <<< lookup header <<< _.headers <<< unsafeCoerce --- | Get the current value for the multi-header on the HTTP Response object. +-- | Get the current values for the header on the HTTP Response object. getResponseMultiHeader :: String -> HTTP.Response -> Array String getResponseMultiHeader header = fromMaybe [] <<< lookup header <<< _.headers <<< unsafeCoerce From 81792a5a8e5f18583a2d25a519d063e8c1b3986e Mon Sep 17 00:00:00 2001 From: Arthur Xavier Date: Mon, 7 Nov 2022 17:27:17 -0300 Subject: [PATCH 6/9] FBCM-5157 Rewrite newly added modules following CitizenNet's style guide --- src/HTTPure/MultiHeaders.purs | 39 +++--- test/Test/HTTPure/MultiHeadersSpec.purs | 159 +++++++++++++----------- 2 files changed, 104 insertions(+), 94 deletions(-) diff --git a/src/HTTPure/MultiHeaders.purs b/src/HTTPure/MultiHeaders.purs index 42756c4..23b1994 100644 --- a/src/HTTPure/MultiHeaders.purs +++ b/src/HTTPure/MultiHeaders.purs @@ -15,24 +15,24 @@ import Prelude import Data.Array.NonEmpty (NonEmptyArray) import Data.Array.NonEmpty as Data.Array.NonEmpty -import Data.Foldable (foldl) import Data.Foldable as Data.Foldable -import Data.FoldableWithIndex (foldMapWithIndex) +import Data.FoldableWithIndex as Data.FoldableWithIndex import Data.Generic.Rep (class Generic) import Data.Map (Map) import Data.Map as Data.Map import Data.Maybe (Maybe(..)) import Data.Newtype (class Newtype, un) -import Data.Show.Generic (genericShow) -import Data.String.CaseInsensitive (CaseInsensitiveString(CaseInsensitiveString)) -import Data.TraversableWithIndex (traverseWithIndex) -import Data.Tuple (Tuple(Tuple)) +import Data.Show.Generic as Data.Show.Generic +import Data.String.CaseInsensitive (CaseInsensitiveString(..)) +import Data.TraversableWithIndex as Data.TraversableWithIndex +import Data.Tuple (Tuple(..)) import Effect (Effect) -import Foreign.Object (Object, fold) +import Foreign.Object (Object) +import Foreign.Object as Foreign.Object import HTTPure.Headers (Headers(..)) import HTTPure.Lookup (class Lookup, (!!)) -import Node.HTTP (Request, Response, setHeaders) -import Unsafe.Coerce (unsafeCoerce) +import Node.HTTP as Node.HTTP +import Unsafe.Coerce as Unsafe.Coerce -- | The `MultiHeaders` type represets the set of headers in a HTTP request or -- | response read in a way such that every header name maps to a non-empty list @@ -50,7 +50,7 @@ instance lookupMultiHeaders :: Lookup MultiHeaders String (NonEmptyArray String) lookup (MultiHeaders headersMap) key = headersMap !! key instance showMultiHeaders :: Show MultiHeaders where - show = genericShow + show = Data.Show.Generic.genericShow -- | Compare two `MultiHeaders` objects by comparing the underlying `Objects`. derive newtype instance eqMultiHeaders :: Eq MultiHeaders @@ -82,15 +82,15 @@ headers = headers' <<< map (map pure) -- | Convert an `Array` of `Tuples` of 2 `Strings` to a `MultiHeaders` object. headers' :: Array (Tuple String (NonEmptyArray String)) -> MultiHeaders -headers' = foldl insertField Data.Map.empty >>> MultiHeaders +headers' = MultiHeaders <<< Data.Foldable.foldl insertField Data.Map.empty where insertField x (Tuple key values) = Data.Map.insertWith append (CaseInsensitiveString key) values x -- | Read the headers out of a HTTP `Request` object and parse duplicated -- | headers as a list (instead of comma-separated values, as with -- | `HTTPure.Headers.read`). -read :: Request -> MultiHeaders -read = requestHeadersDistinct >>> fold insertField Data.Map.empty >>> MultiHeaders +read :: Node.HTTP.Request -> MultiHeaders +read = MultiHeaders <<< Foreign.Object.fold insertField Data.Map.empty <<< requestHeadersDistinct where insertField :: forall a. @@ -105,13 +105,13 @@ read = requestHeadersDistinct >>> fold insertField Data.Map.empty >>> MultiHeade -- | Similar to `Node.HTTP.requestHeaders`, but there is no join logic and the -- | values are always arrays of strings, even for headers received just once. -- | See https://nodejs.org/api/http.html#messageheadersdistinct. - requestHeadersDistinct :: Request -> Object (Array String) - requestHeadersDistinct = _.headersDistinct <<< unsafeCoerce + requestHeadersDistinct :: Node.HTTP.Request -> Object (Array String) + requestHeadersDistinct = _.headersDistinct <<< Unsafe.Coerce.unsafeCoerce -- | Allow a `MultiHeaders` to be represented as a string. This string is -- | formatted in HTTP headers format. toString :: MultiHeaders -> String -toString (MultiHeaders headersMap) = foldMapWithIndex showField headersMap <> "\n" +toString (MultiHeaders headersMap) = Data.FoldableWithIndex.foldMapWithIndex showField headersMap <> "\n" where showField :: CaseInsensitiveString -> NonEmptyArray String -> String showField key values = @@ -123,7 +123,8 @@ toString (MultiHeaders headersMap) = foldMapWithIndex showField headersMap <> "\ -- | Given an HTTP `Response` and a `MultiHeaders` object, return an effect that will -- | write the `MultiHeaders` to the `Response`. -write :: Response -> MultiHeaders -> Effect Unit -write response (MultiHeaders headersMap) = void $ traverseWithIndex writeField headersMap +write :: Node.HTTP.Response -> MultiHeaders -> Effect Unit +write response (MultiHeaders headersMap) = void $ Data.TraversableWithIndex.traverseWithIndex writeField headersMap where - writeField key = setHeaders response (un CaseInsensitiveString key) <<< Data.Array.NonEmpty.toArray + writeField :: CaseInsensitiveString -> NonEmptyArray String -> Effect Unit + writeField key = Node.HTTP.setHeaders response (un CaseInsensitiveString key) <<< Data.Array.NonEmpty.toArray diff --git a/test/Test/HTTPure/MultiHeadersSpec.purs b/test/Test/HTTPure/MultiHeadersSpec.purs index 374b104..76b8a15 100644 --- a/test/Test/HTTPure/MultiHeadersSpec.purs +++ b/test/Test/HTTPure/MultiHeadersSpec.purs @@ -9,112 +9,113 @@ import Data.String.CaseInsensitive (CaseInsensitiveString(..)) import Data.Tuple (Tuple(..)) import Effect.Class (liftEffect) import HTTPure.Lookup ((!!)) -import HTTPure.MultiHeaders (MultiHeaders(..), empty, header, header', headers, read, toString, write) +import HTTPure.MultiHeaders (MultiHeaders(..)) +import HTTPure.MultiHeaders as HTTPure.MultiHeaders import Test.HTTPure.TestHelpers ((?=)) import Test.HTTPure.TestHelpers as TestHelpers -import Test.Spec (describe, it) +import Test.Spec as Test.Spec lookupSpec :: TestHelpers.Test lookupSpec = - describe "lookup" do - describe "when the string is in the header set" do - describe "when searching with lowercase" do - it "is Just the string" do - header "x-test" "test" !! "x-test" ?= Just (pure "test") - describe "when searching with uppercase" do - it "is Just the string" do - header "x-test" "test" !! "X-Test" ?= Just (pure "test") - describe "when the string is uppercase" do - describe "when searching with lowercase" do - it "is Just the string" do - header "X-Test" "test" !! "x-test" ?= Just (pure "test") - describe "when searching with uppercase" do - it "is Just the string" do - header "X-Test" "test" !! "X-Test" ?= Just (pure "test") - describe "when the string is not in the header set" do - it "is Nothing" do - ((empty !! "X-Test") :: Maybe (NonEmptyArray String)) ?= Nothing + Test.Spec.describe "lookup" do + Test.Spec.describe "when the string is in the header set" do + Test.Spec.describe "when searching with lowercase" do + Test.Spec.it "is Just the string" do + HTTPure.MultiHeaders.header "x-test" "test" !! "x-test" ?= Just (pure "test") + Test.Spec.describe "when searching with uppercase" do + Test.Spec.it "is Just the string" do + HTTPure.MultiHeaders.header "x-test" "test" !! "X-Test" ?= Just (pure "test") + Test.Spec.describe "when the string is uppercase" do + Test.Spec.describe "when searching with lowercase" do + Test.Spec.it "is Just the string" do + HTTPure.MultiHeaders.header "X-Test" "test" !! "x-test" ?= Just (pure "test") + Test.Spec.describe "when searching with uppercase" do + Test.Spec.it "is Just the string" do + HTTPure.MultiHeaders.header "X-Test" "test" !! "X-Test" ?= Just (pure "test") + Test.Spec.describe "when the string is not in the header set" do + Test.Spec.it "is Nothing" do + ((HTTPure.MultiHeaders.empty !! "X-Test") :: Maybe (NonEmptyArray String)) ?= Nothing eqSpec :: TestHelpers.Test eqSpec = - describe "eq" do - describe "when the two MultiHeaders contain the same keys and values" do - it "is true" do - header "Test1" "test1" == header "Test1" "test1" ?= true - describe "when the two MultiHeaders contain different keys and values" do - it "is false" do - header "Test1" "test1" == header "Test2" "test2" ?= false - describe "when the two MultiHeaders contain only different values" do - it "is false" do - header "Test1" "test1" == header "Test1" "test2" ?= false - describe "when the one MultiHeaders contains additional keys and values" do - it "is false" do - let mock = header "Test1" "1" <> header "Test2" "2" - header "Test1" "1" == mock ?= false + Test.Spec.describe "eq" do + Test.Spec.describe "when the two MultiHeaders contain the same keys and values" do + Test.Spec.it "is true" do + HTTPure.MultiHeaders.header "Test1" "test1" == HTTPure.MultiHeaders.header "Test1" "test1" ?= true + Test.Spec.describe "when the two MultiHeaders contain different keys and values" do + Test.Spec.it "is false" do + HTTPure.MultiHeaders.header "Test1" "test1" == HTTPure.MultiHeaders.header "Test2" "test2" ?= false + Test.Spec.describe "when the two MultiHeaders contain only different values" do + Test.Spec.it "is false" do + HTTPure.MultiHeaders.header "Test1" "test1" == HTTPure.MultiHeaders.header "Test1" "test2" ?= false + Test.Spec.describe "when the one MultiHeaders contains additional keys and values" do + Test.Spec.it "is false" do + let mock = HTTPure.MultiHeaders.header "Test1" "1" <> HTTPure.MultiHeaders.header "Test2" "2" + HTTPure.MultiHeaders.header "Test1" "1" == mock ?= false appendSpec :: TestHelpers.Test appendSpec = - describe "append" do - describe "when there are multiple keys" do - it "appends the headers correctly" do + Test.Spec.describe "append" do + Test.Spec.describe "when there are multiple keys" do + Test.Spec.it "appends the headers correctly" do let - mock1 = header "Test1" "1" <> header "Test2" "2" - mock2 = header "Test3" "3" <> header "Test4" "4" + mock1 = HTTPure.MultiHeaders.header "Test1" "1" <> HTTPure.MultiHeaders.header "Test2" "2" + mock2 = HTTPure.MultiHeaders.header "Test3" "3" <> HTTPure.MultiHeaders.header "Test4" "4" mock3 = - headers + HTTPure.MultiHeaders.headers [ Tuple "Test1" "1" , Tuple "Test2" "2" , Tuple "Test3" "3" , Tuple "Test4" "4" ] mock1 <> mock2 ?= mock3 - describe "when there is a duplicated key" do - it "appends the multiple values" do - let mock = header "Test" "foo" <> header "Test" "bar" - mock ?= header' "Test" (pure "foo" <> pure "bar") + Test.Spec.describe "when there is a duplicated key" do + Test.Spec.it "appends the multiple values" do + let mock = HTTPure.MultiHeaders.header "Test" "foo" <> HTTPure.MultiHeaders.header "Test" "bar" + mock ?= HTTPure.MultiHeaders.header' "Test" (pure "foo" <> pure "bar") readSpec :: TestHelpers.Test readSpec = - describe "read" do - describe "with no headers" do - it "is an empty Map" do + Test.Spec.describe "read" do + Test.Spec.describe "with no headers" do + Test.Spec.it "is an empty Map" do request <- TestHelpers.mockRequest "" "" "" "" [] - read request ?= empty - describe "with headers" do - it "is a Map with the contents of the headers" do + HTTPure.MultiHeaders.read request ?= HTTPure.MultiHeaders.empty + Test.Spec.describe "with headers" do + Test.Spec.it "is a Map with the contents of the headers" do let testHeader = [ Tuple "X-Test" "test" ] request <- TestHelpers.mockRequest "" "" "" "" testHeader - read request ?= headers testHeader + HTTPure.MultiHeaders.read request ?= HTTPure.MultiHeaders.headers testHeader writeSpec :: TestHelpers.Test writeSpec = - describe "write" do - it "writes the headers to the response" do + Test.Spec.describe "write" do + Test.Spec.it "writes the headers to the response" do header <- liftEffect do mock <- TestHelpers.mockResponse - write mock $ headers [ Tuple "X-Test" "test1", Tuple "X-Test" "test2" ] + HTTPure.MultiHeaders.write mock $ HTTPure.MultiHeaders.headers [ Tuple "X-Test" "test1", Tuple "X-Test" "test2" ] pure $ TestHelpers.getResponseMultiHeader "X-Test" mock header ?= [ "test1", "test2" ] emptySpec :: TestHelpers.Test emptySpec = - describe "empty" do - it "is an empty Map in an empty MultiHeaders" do - empty ?= MultiHeaders Data.Map.empty + Test.Spec.describe "empty" do + Test.Spec.it "is an empty Map in an empty MultiHeaders" do + HTTPure.MultiHeaders.empty ?= MultiHeaders Data.Map.empty headerSpec :: TestHelpers.Test headerSpec = - describe "header" do - it "creates a singleton MultiHeaders" do - header "X-Test" "test" ?= MultiHeaders (Data.Map.singleton (CaseInsensitiveString "X-Test") (pure "test")) + Test.Spec.describe "header" do + Test.Spec.it "creates a singleton MultiHeaders" do + HTTPure.MultiHeaders.header "X-Test" "test" ?= MultiHeaders (Data.Map.singleton (CaseInsensitiveString "X-Test") (pure "test")) headersFunctionSpec :: TestHelpers.Test headersFunctionSpec = - describe "headers" do - it "is equivalent to using header with <>" do + Test.Spec.describe "headers" do + Test.Spec.it "is equivalent to using header with <>" do let - expected = header "X-Test-1" "1" <> header "X-Test-2" "2" - test = headers + expected = HTTPure.MultiHeaders.header "X-Test-1" "1" <> HTTPure.MultiHeaders.header "X-Test-2" "2" + test = HTTPure.MultiHeaders.headers [ Tuple "X-Test-1" "1" , Tuple "X-Test-2" "2" ] @@ -122,20 +123,28 @@ headersFunctionSpec = toStringSpec :: TestHelpers.Test toStringSpec = - describe "toString" do - it "is a string representing the headers in HTTP format" do - let mock = header "Test1" "1" <> header "Test2" "2" - toString mock ?= "Test1: 1\nTest2: 2\n\n" - it "separates duplicate headers with a comma" do - let mock = header "Test1" "1" <> header "Test1" "2" <> header "Test2" "2" - toString mock ?= "Test1: 1, 2\nTest2: 2\n\n" - it "separates duplicate 'Set-Cookie' headers with a semicolon" do - let mock = header "Test1" "1" <> header "Set-Cookie" "1" <> header "Set-Cookie" "2" - toString mock ?= "Set-Cookie: 1; 2\nTest1: 1\n\n" + Test.Spec.describe "toString" do + Test.Spec.it "is a string representing the headers in HTTP format" do + let mock = HTTPure.MultiHeaders.header "Test1" "1" <> HTTPure.MultiHeaders.header "Test2" "2" + HTTPure.MultiHeaders.toString mock ?= "Test1: 1\nTest2: 2\n\n" + Test.Spec.it "separates duplicate headers with a comma" do + let + mock = + HTTPure.MultiHeaders.header "Test1" "1" + <> HTTPure.MultiHeaders.header "Test1" "2" + <> HTTPure.MultiHeaders.header "Test2" "2" + HTTPure.MultiHeaders.toString mock ?= "Test1: 1, 2\nTest2: 2\n\n" + Test.Spec.it "separates duplicate 'Set-Cookie' headers with a semicolon" do + let + mock = + HTTPure.MultiHeaders.header "Test1" "1" + <> HTTPure.MultiHeaders.header "Set-Cookie" "1" + <> HTTPure.MultiHeaders.header "Set-Cookie" "2" + HTTPure.MultiHeaders.toString mock ?= "Set-Cookie: 1; 2\nTest1: 1\n\n" multiHeadersSpec :: TestHelpers.Test multiHeadersSpec = - describe "MultiHeaders" do + Test.Spec.describe "MultiHeaders" do lookupSpec eqSpec appendSpec From aa7e3c6f4b733c6df3dcbe62cd154233b81baa1f Mon Sep 17 00:00:00 2001 From: Arthur Xavier Date: Tue, 8 Nov 2022 13:58:00 -0300 Subject: [PATCH 7/9] FBCM-5157 Fix implementation of `HTTPure.MultiHeaders.read` When testing the old implementation of this function with an actual node.js request, the `request.headersDistinct` property did not exist, even in node.js versions greater than v16.17.0, which, according to https://nodejs.org/api/http.html#messageheadersdistinct is when the property was added. We fix this by using `request.rawHeaders` instead. --- src/HTTPure.purs | 2 ++ src/HTTPure/MultiHeaders.js | 22 ++++++++++++++ src/HTTPure/MultiHeaders.purs | 38 ++++++++++++------------- test/Test/HTTPure/MultiHeadersSpec.purs | 7 ++++- test/Test/HTTPure/TestHelpers.js | 6 ++-- test/Test/HTTPure/TestHelpers.purs | 17 +++++------ 6 files changed, 61 insertions(+), 31 deletions(-) create mode 100644 src/HTTPure/MultiHeaders.js diff --git a/src/HTTPure.purs b/src/HTTPure.purs index 01a90b1..b76504f 100644 --- a/src/HTTPure.purs +++ b/src/HTTPure.purs @@ -3,6 +3,7 @@ module HTTPure , module HTTPure.Headers , module HTTPure.Lookup , module HTTPure.Method + , module HTTPure.MultiHeaders , module HTTPure.Path , module HTTPure.Query , module HTTPure.Request @@ -15,6 +16,7 @@ import HTTPure.Body (toBuffer, toStream, toString) import HTTPure.Headers (Headers, empty, header, headers) import HTTPure.Lookup (at, has, lookup, (!!), (!?), (!@)) import HTTPure.Method (Method(..)) +import HTTPure.MultiHeaders (MultiHeaders) import HTTPure.Path (Path) import HTTPure.Query (Query) import HTTPure.Request (Request, fullPath) diff --git a/src/HTTPure/MultiHeaders.js b/src/HTTPure/MultiHeaders.js new file mode 100644 index 0000000..6168572 --- /dev/null +++ b/src/HTTPure/MultiHeaders.js @@ -0,0 +1,22 @@ +export const parseRawHeaders = f => headers => { + const result = []; + let key = null, value = null; + + for (const str of headers) { + if (key === null) { + key = str; + } else if (value === null) { + value = str; + } else { + result.push(f(key)(value)); + key = str; + value = null; + } + } + + if (key !== null && value !== null) { + result.push(f(key)(value)); + } + + return result; +}; diff --git a/src/HTTPure/MultiHeaders.purs b/src/HTTPure/MultiHeaders.purs index 23b1994..3c2814a 100644 --- a/src/HTTPure/MultiHeaders.purs +++ b/src/HTTPure/MultiHeaders.purs @@ -20,15 +20,12 @@ import Data.FoldableWithIndex as Data.FoldableWithIndex import Data.Generic.Rep (class Generic) import Data.Map (Map) import Data.Map as Data.Map -import Data.Maybe (Maybe(..)) import Data.Newtype (class Newtype, un) import Data.Show.Generic as Data.Show.Generic import Data.String.CaseInsensitive (CaseInsensitiveString(..)) import Data.TraversableWithIndex as Data.TraversableWithIndex import Data.Tuple (Tuple(..)) import Effect (Effect) -import Foreign.Object (Object) -import Foreign.Object as Foreign.Object import HTTPure.Headers (Headers(..)) import HTTPure.Lookup (class Lookup, (!!)) import Node.HTTP as Node.HTTP @@ -86,27 +83,30 @@ headers' = MultiHeaders <<< Data.Foldable.foldl insertField Data.Map.empty where insertField x (Tuple key values) = Data.Map.insertWith append (CaseInsensitiveString key) values x +-- | Parse a list of raw request headers, applying the given function to every +-- | key-value pair. +-- | See https://nodejs.org/api/http.html#messagerawheaders. +foreign import parseRawHeaders :: forall a. (String -> String -> a) -> Array String -> Array a + -- | Read the headers out of a HTTP `Request` object and parse duplicated -- | headers as a list (instead of comma-separated values, as with -- | `HTTPure.Headers.read`). read :: Node.HTTP.Request -> MultiHeaders -read = MultiHeaders <<< Foreign.Object.fold insertField Data.Map.empty <<< requestHeadersDistinct +read = + MultiHeaders + <<< Data.Map.fromFoldableWith (flip append) + <<< map (\(Tuple key value) -> Tuple (CaseInsensitiveString key) (pure value)) + <<< parseRawHeaders Tuple + <<< requestRawHeaders where - insertField :: - forall a. - Map CaseInsensitiveString (NonEmptyArray a) -> - String -> - Array a -> - Map CaseInsensitiveString (NonEmptyArray a) - insertField headersMap key array = case Data.Array.NonEmpty.fromArray array of - Nothing -> headersMap - Just nonEmptyArray -> Data.Map.insert (CaseInsensitiveString key) nonEmptyArray headersMap - - -- | Similar to `Node.HTTP.requestHeaders`, but there is no join logic and the - -- | values are always arrays of strings, even for headers received just once. - -- | See https://nodejs.org/api/http.html#messageheadersdistinct. - requestHeadersDistinct :: Node.HTTP.Request -> Object (Array String) - requestHeadersDistinct = _.headersDistinct <<< Unsafe.Coerce.unsafeCoerce + -- | The raw request/response headers list exactly as they were received. + -- | The keys and values are in the same list. It is not a list of tuples. + -- | So, the even-numbered offsets are key values, and the odd-numbered + -- | offsets are the associated values. Header names are not lowercased, and + -- | duplicates are not merged. + -- | See https://nodejs.org/api/http.html#messagerawheaders. + requestRawHeaders :: Node.HTTP.Request -> Array String + requestRawHeaders = _.rawHeaders <<< Unsafe.Coerce.unsafeCoerce -- | Allow a `MultiHeaders` to be represented as a string. This string is -- | formatted in HTTP headers format. diff --git a/test/Test/HTTPure/MultiHeadersSpec.purs b/test/Test/HTTPure/MultiHeadersSpec.purs index 76b8a15..64b2a14 100644 --- a/test/Test/HTTPure/MultiHeadersSpec.purs +++ b/test/Test/HTTPure/MultiHeadersSpec.purs @@ -83,7 +83,12 @@ readSpec = HTTPure.MultiHeaders.read request ?= HTTPure.MultiHeaders.empty Test.Spec.describe "with headers" do Test.Spec.it "is a Map with the contents of the headers" do - let testHeader = [ Tuple "X-Test" "test" ] + let testHeader = [ Tuple "X-Test" "test", Tuple "X-Foo" "bar" ] + request <- TestHelpers.mockRequest "" "" "" "" testHeader + HTTPure.MultiHeaders.read request ?= HTTPure.MultiHeaders.headers testHeader + Test.Spec.describe "with duplicate headers" do + Test.Spec.it "is a Map with the contents of the headers" do + let testHeader = [ Tuple "X-Test" "test1", Tuple "X-Test" "test2" ] request <- TestHelpers.mockRequest "" "" "" "" testHeader HTTPure.MultiHeaders.read request ?= HTTPure.MultiHeaders.headers testHeader diff --git a/test/Test/HTTPure/TestHelpers.js b/test/Test/HTTPure/TestHelpers.js index 42f47f1..e64d1a4 100644 --- a/test/Test/HTTPure/TestHelpers.js +++ b/test/Test/HTTPure/TestHelpers.js @@ -1,6 +1,6 @@ import { Readable } from "stream"; -export const mockRequestImpl = httpVersion => method => url => body => headers => () => { +export const mockRequestImpl = httpVersion => method => url => body => headers => rawHeaders => () => { const stream = new Readable({ read: function (size) { this.push(body); @@ -9,8 +9,8 @@ export const mockRequestImpl = httpVersion => method => url => body => headers = }); stream.method = method; stream.url = url; - stream.headers = Object.fromEntries(Object.entries(headers).map(([key, values]) => [key, values[values.length - 1]])); - stream.headersDistinct = headers; + stream.headers = headers; + stream.rawHeaders = rawHeaders; stream.httpVersion = httpVersion; return stream; diff --git a/test/Test/HTTPure/TestHelpers.purs b/test/Test/HTTPure/TestHelpers.purs index 75bcc2c..74e5059 100644 --- a/test/Test/HTTPure/TestHelpers.purs +++ b/test/Test/HTTPure/TestHelpers.purs @@ -3,13 +3,13 @@ module Test.HTTPure.TestHelpers where import Prelude import Data.Array (fromFoldable) as Array -import Data.Array.NonEmpty (NonEmptyArray) +import Data.Array as Data.Array import Data.Either (Either(Right)) import Data.List (List(Nil, Cons), reverse) import Data.Maybe (fromMaybe) import Data.Options ((:=)) import Data.String (toLower) -import Data.Tuple (Tuple) +import Data.Tuple (Tuple(..)) import Effect (Effect) import Effect.Aff (Aff, makeAff, nonCanceler) import Effect.Class (liftEffect) @@ -205,7 +205,8 @@ foreign import mockRequestImpl :: String -> String -> String -> - Object (NonEmptyArray String) -> + Object String -> + Array String -> Effect Request -- | Mock an HTTP Request object @@ -216,11 +217,11 @@ mockRequest :: String -> Array (Tuple String String) -> Aff Request -mockRequest httpVersion method url body = - liftEffect - <<< mockRequestImpl httpVersion method url body - <<< Object.fromFoldableWith (flip append) - <<< map (map pure) +mockRequest httpVersion method url body headers = + liftEffect $ mockRequestImpl httpVersion method url body (Object.fromFoldable headers) rawHeaders + where + rawHeaders :: Array String + rawHeaders = Data.Array.concatMap (\(Tuple key value) -> [ key, value ]) headers -- | Mock an HTTP Response object foreign import mockResponse :: Effect HTTP.Response From 1792860aada5a93cffbbffe5d5245a11e5a3252a Mon Sep 17 00:00:00 2001 From: Arthur Xavier Date: Tue, 8 Nov 2022 14:02:11 -0300 Subject: [PATCH 8/9] FBCM-5157 Add the `Examples.MultiHeaders` example --- docs/Examples/MultiHeaders/Main.purs | 43 ++++++++++++++++++++++++++++ docs/Examples/MultiHeaders/Readme.md | 22 ++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 docs/Examples/MultiHeaders/Main.purs create mode 100644 docs/Examples/MultiHeaders/Readme.md diff --git a/docs/Examples/MultiHeaders/Main.purs b/docs/Examples/MultiHeaders/Main.purs new file mode 100644 index 0000000..095ad04 --- /dev/null +++ b/docs/Examples/MultiHeaders/Main.purs @@ -0,0 +1,43 @@ +module Examples.MultiHeaders.Main where + +import Prelude + +import Data.Foldable as Data.Foldable +import Data.Maybe (maybe) +import Data.Tuple (Tuple(..)) +import Effect.Console (log) +import HTTPure (MultiHeaders, Request, ResponseM, ServerM, ok, serve, (!!)) +import HTTPure.MultiHeaders as HTTPure.MultiHeaders + +-- | The headers that will be included in every response. +responseHeaders :: MultiHeaders +responseHeaders = + HTTPure.MultiHeaders.headers + [ Tuple "Set-Cookie" "id=123456" + , Tuple "Set-Cookie" "domain=foo.example.com" + ] + +-- | Route to the correct handler +router :: Request -> ResponseM +router { multiHeaders } = ado + response <- + ok + $ maybe "" (Data.Foldable.intercalate ", ") + $ multiHeaders !! "X-Input" + in response { multiHeaders = responseHeaders } + +-- | Boot up the server +main :: ServerM +main = + serve 8080 router do + log " ┌───────────────────────────────────────────────────────────────────┐" + log " │ Server now up on port 8080 │" + log " │ │" + log " │ To test, run: │" + log " │ > curl -H 'X-Input: test1' -H 'X-Input: test2' -v localhost:8080 │" + log " │ # => ... │" + log " │ # => ...< Set-Cookie: id=123456 │" + log " │ # => ...< Set-Cookie: domain=foo.example.com │" + log " │ # => ... │" + log " │ # => test1, test2 │" + log " └───────────────────────────────────────────────────────────────────┘" diff --git a/docs/Examples/MultiHeaders/Readme.md b/docs/Examples/MultiHeaders/Readme.md new file mode 100644 index 0000000..adfb767 --- /dev/null +++ b/docs/Examples/MultiHeaders/Readme.md @@ -0,0 +1,22 @@ +# Multi-Headers Example + +This is a basic example of working with multi-headers. Unlike `HTTPure.Headers`, +the `HTTPure.MultiHeaders` module abstracts headers with potentially multiple +values. + +This example will respond to an HTTP GET on any url and will read the header +'X-Input' and return the contents in the response body. Try adding multiple, +duplicate 'X-Input' headers to see how it works. It will also return the +'Set-Cookie' response header with multiple values. + +To run the example server, run: + +```bash +nix-shell --run 'example MultiHeaders' +``` + +Or, without nix: + +```bash +spago -x test.dhall run --main Examples.MultiHeaders.Main +``` From ce72d1d91fa4199ba4717ec7dd4b6a51aadfe476 Mon Sep 17 00:00:00 2001 From: Arthur Xavier Date: Thu, 10 Nov 2022 09:51:00 -0300 Subject: [PATCH 9/9] FBCM-5157 Filter out `Set-Cookie` headers in `HTTPure.Headers.read` We must do this in order to avoid runtime type errors when trying to access `Set-Cookie` request headers. The reason is that node.js specializes the reading of `Set-Cookie` and makes that one specific header an `Array String` instead of `String`. See https://nodejs.org/api/http.html#messageheaders for more details. --- docs/Examples/Headers/Readme.md | 5 +++++ src/HTTPure/Headers.purs | 28 +++++++++++++++++++--------- src/HTTPure/MultiHeaders.purs | 3 +++ test/Test/HTTPure/HeadersSpec.purs | 6 ++++++ 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/docs/Examples/Headers/Readme.md b/docs/Examples/Headers/Readme.md index 5d3921d..259a8d9 100644 --- a/docs/Examples/Headers/Readme.md +++ b/docs/Examples/Headers/Readme.md @@ -5,6 +5,11 @@ on any url and will read the header 'X-Input' and return the contents in the response body. It will also return the 'X-Example' response header with the value 'hello world!'. +Bear in mind that acessing `Set-Cookie` headers through the `headers` interface +will not work because of how node.js represents those headers specifically. For +`Set-Cookie` request headers, please use the `multiHeaders` property of +`HTTPure.Request`. + To run the example server, run: ```bash diff --git a/src/HTTPure/Headers.purs b/src/HTTPure/Headers.purs index 3dc6f5f..993a000 100644 --- a/src/HTTPure/Headers.purs +++ b/src/HTTPure/Headers.purs @@ -10,18 +10,19 @@ module HTTPure.Headers import Prelude -import Data.Foldable (foldl) +import Data.Foldable (foldMap) import Data.FoldableWithIndex (foldMapWithIndex) import Data.Generic.Rep (class Generic) -import Data.Map (Map, insert, singleton, union) +import Data.Map (Map, singleton, union) import Data.Map (empty) as Map import Data.Newtype (class Newtype, unwrap) import Data.Show.Generic (genericShow) +import Data.String as Data.String import Data.String.CaseInsensitive (CaseInsensitiveString(CaseInsensitiveString)) import Data.TraversableWithIndex (traverseWithIndex) -import Data.Tuple (Tuple(Tuple)) +import Data.Tuple (Tuple, uncurry) import Effect (Effect) -import Foreign.Object (fold) +import Foreign.Object as Foreign.Object import HTTPure.Lookup (class Lookup, (!!)) import Node.HTTP (Request, Response, requestHeaders, setHeader) @@ -48,11 +49,22 @@ derive newtype instance eqHeaders :: Eq Headers instance semigroupHeaders :: Semigroup Headers where append (Headers a) (Headers b) = Headers $ union b a +instance monoidHeaders :: Monoid Headers where + mempty = Headers Map.empty + -- | Get the headers out of a HTTP `Request` object. +-- | +-- | We intentionally filter out "Set-Cookie" headers here as according to the +-- | node.js docs, the "set-cookie" header is always represented as an array, +-- | and trying to read it as `String` would cause a runtime type error. +-- | See https://nodejs.org/api/http.html#messageheaders. read :: Request -> Headers -read = requestHeaders >>> fold insertField Map.empty >>> Headers +read = Foreign.Object.foldMap header' <<< requestHeaders where - insertField x key value = insert (CaseInsensitiveString key) value x + header' :: String -> String -> Headers + header' key + | Data.String.toLower key == "set-cookie" = const mempty + | otherwise = header key -- | Given an HTTP `Response` and a `Headers` object, return an effect that will -- | write the `Headers` to the `Response`. @@ -67,9 +79,7 @@ empty = Headers Map.empty -- | Convert an `Array` of `Tuples` of 2 `Strings` to a `Headers` object. headers :: Array (Tuple String String) -> Headers -headers = foldl insertField Map.empty >>> Headers - where - insertField x (Tuple key value) = insert (CaseInsensitiveString key) value x +headers = foldMap (uncurry header) -- | Create a singleton header from a key-value pair. header :: String -> String -> Headers diff --git a/src/HTTPure/MultiHeaders.purs b/src/HTTPure/MultiHeaders.purs index 3c2814a..0cf4aa8 100644 --- a/src/HTTPure/MultiHeaders.purs +++ b/src/HTTPure/MultiHeaders.purs @@ -57,6 +57,9 @@ instance semigroupMultiHeaders :: Semigroup MultiHeaders where append (MultiHeaders a) (MultiHeaders b) = MultiHeaders $ Data.Map.unionWith append a b +instance monoidMultiHeaders :: Monoid MultiHeaders where + mempty = MultiHeaders Data.Map.empty + -- | Return a `MultiHeaders` containing nothing. empty :: MultiHeaders empty = MultiHeaders Data.Map.empty diff --git a/test/Test/HTTPure/HeadersSpec.purs b/test/Test/HTTPure/HeadersSpec.purs index b34a1c8..62494ec 100644 --- a/test/Test/HTTPure/HeadersSpec.purs +++ b/test/Test/HTTPure/HeadersSpec.purs @@ -84,6 +84,12 @@ readSpec = let testHeader = [ Tuple "X-Test" "test" ] request <- TestHelpers.mockRequest "" "" "" "" testHeader read request ?= headers testHeader + describe "with 'Set-Cookie' headers" do + it "is a Map with the contents of the headers without any 'Set-Cookie' headers" do + let testHeader = [ Tuple "X-Test" "test", Tuple "Set-Cookie" "foo", Tuple "set-cookie" "bar" ] + let headers' = Headers $ Data.Map.singleton (CaseInsensitiveString "X-Test") "test" + request <- TestHelpers.mockRequest "" "" "" "" testHeader + read request ?= headers' writeSpec :: TestHelpers.Test writeSpec =