Separate headers into request and response headers

- Add response header construction using records
- Update tests and examples
- Update doc
This commit is contained in:
sigma-andex 2022-06-16 19:58:33 +01:00
parent 8733799cb0
commit 88169cd299
9 changed files with 69 additions and 44 deletions

View File

@ -1,6 +1,8 @@
# Changelog # Changelog
## Unpublished ## Unpublished
- Separate `Headers` into `RequestHeaders` and `ResponseHeaders`
- Allow passing a record to `headers` to make `ResponseHeaders` easier
## v1.3.0 ## v1.3.0

View File

@ -119,6 +119,11 @@ main =
HTTPurple 🪁 has some helpers to make json parsing and validation very simple. See the [requests guide](./Requests.md) for more information. HTTPurple 🪁 has some helpers to make json parsing and validation very simple. See the [requests guide](./Requests.md) for more information.
## Headers
HTTPurple 🪁 has two separate types for headers, namely `RequestHeader` and `ResponseHeader`. `ResponseHeader` wraps `Map CaseInsensitiveString (Array String)` and therefore allows setting multiple response headers. This is useful if you e.g. want to set multiple `Set-Cookie` headers.
Also you can create the headers by passing a record. See the [responses documentation](./Differences.md) for more information.
## Other improvmenets ## Other improvmenets
* Default closing handler - A default closing handler is provided so you can just stop your server using `ctrl+x` without having to worry about anything. You can deactivate it by setting `closingHandler: NoClosingHandler` in the listen options. * Default closing handler - A default closing handler is provided so you can just stop your server using `ctrl+x` without having to worry about anything. You can deactivate it by setting `closingHandler: NoClosingHandler` in the listen options.

View File

@ -5,7 +5,7 @@ import Prelude
import Data.Generic.Rep (class Generic) import Data.Generic.Rep (class Generic)
import Data.Maybe (Maybe(..)) import Data.Maybe (Maybe(..))
import Effect.Console (log) import Effect.Console (log)
import HTTPurple (Headers, Request, ResponseM, ServerM, header, ok', serve) import HTTPurple (Request, ResponseHeaders, ResponseM, ServerM, header, ok', serve)
import Node.FS.Aff (readFile) import Node.FS.Aff (readFile)
import Routing.Duplex as RD import Routing.Duplex as RD
import Routing.Duplex.Generic as RG import Routing.Duplex.Generic as RG
@ -23,7 +23,7 @@ route = RD.root $ RG.sum
filePath :: String filePath :: String
filePath = "./docs/Examples/BinaryResponse/circle.png" filePath = "./docs/Examples/BinaryResponse/circle.png"
responseHeaders :: Headers responseHeaders :: ResponseHeaders
responseHeaders = header "Content-Type" "image/png" responseHeaders = header "Content-Type" "image/png"
-- | Respond with image data when run -- | Respond with image data when run

View File

@ -3,9 +3,9 @@ module Examples.Headers.Main where
import Prelude import Prelude
import Data.Generic.Rep (class Generic) import Data.Generic.Rep (class Generic)
import Data.Maybe (Maybe(..))
import Effect.Console (log) import Effect.Console (log)
import HTTPurple (Headers, Request, ResponseM, ServerM, header, ok', serve, (!@)) import HTTPurple (Request, ResponseHeaders, ResponseM, ServerM, ok', serve, (!@))
import HTTPurple.Headers (headers)
import Routing.Duplex as RD import Routing.Duplex as RD
import Routing.Duplex.Generic as RG import Routing.Duplex.Generic as RG
@ -19,8 +19,11 @@ route = RD.root $ RG.sum
} }
-- | The headers that will be included in every response. -- | The headers that will be included in every response.
responseHeaders :: Headers responseHeaders :: ResponseHeaders
responseHeaders = header "X-Example" "hello world!" responseHeaders = headers
{ "X-Example": "hello world!"
, "X-Example2": "hello world!"
}
-- | Route to the correct handler -- | Route to the correct handler
router :: Request Route -> ResponseM router :: Request Route -> ResponseM

View File

@ -12,8 +12,7 @@ module HTTPurple.Headers
, read , read
, toResponseHeaders , toResponseHeaders
, write , write
) ) where
where
import Prelude import Prelude
@ -36,8 +35,9 @@ import Prim.RowList (class RowToList, Cons, Nil)
import Record as Record import Record as Record
import Type.Proxy (Proxy(..)) import Type.Proxy (Proxy(..))
-- | The `RequestHeaders` type is just sugar for a `Object` of `Strings` -- | The `RequestHeaders` type is a wrapper for a map
-- | that represents the set of headers in an HTTP request or response. -- | that represents the set of headers in an HTTP request.
-- | A request header contains maximum one value per key.
newtype RequestHeaders = RequestHeaders (Map CaseInsensitiveString String) newtype RequestHeaders = RequestHeaders (Map CaseInsensitiveString String)
derive instance Newtype RequestHeaders _ derive instance Newtype RequestHeaders _
@ -47,14 +47,14 @@ derive instance Newtype RequestHeaders _
instance Lookup RequestHeaders String String where instance Lookup RequestHeaders String String where
lookup (RequestHeaders headers') key = headers' !! key lookup (RequestHeaders headers') key = headers' !! key
-- | Allow a `Headers` to be represented as a string. This string is formatted -- | Allow a `RequestHeaders` to be represented as a string. This string is formatted
-- | in HTTP headers format. -- | in HTTP headers format.
instance Show RequestHeaders where instance Show RequestHeaders where
show (RequestHeaders headers') = foldMapWithIndex showField headers' <> "\n" show (RequestHeaders headers') = foldMapWithIndex showField headers' <> "\n"
where where
showField key value = unwrap key <> ": " <> value <> "\n" showField key value = unwrap key <> ": " <> value <> "\n"
-- | Compare two `Headers` objects by comparing the underlying `Objects`. -- | Compare two `RequestHeaders` objects by comparing the underlying `Objects`.
instance Eq RequestHeaders where instance Eq RequestHeaders where
eq (RequestHeaders a) (RequestHeaders b) = eq a b eq (RequestHeaders a) (RequestHeaders b) = eq a b
@ -62,11 +62,12 @@ instance Eq RequestHeaders where
instance Semigroup RequestHeaders where instance Semigroup RequestHeaders where
append (RequestHeaders a) (RequestHeaders b) = RequestHeaders $ union b a append (RequestHeaders a) (RequestHeaders b) = RequestHeaders $ union b a
-- | The `RequestHeaders` type is just sugar for a `Object` of `Strings` -- | The `ResponseHeaders` type is a wrapper for a map
-- | that represents the set of headers in an HTTP request or response. -- | that represents the set of headers in an HTTP response.
-- | A response header can contain multiple values per key,
-- | e.g. in the case of multiple Set-Cookie directives.
newtype ResponseHeaders = ResponseHeaders (Map CaseInsensitiveString (Array String)) newtype ResponseHeaders = ResponseHeaders (Map CaseInsensitiveString (Array String))
-- | Allow one `ResponseHeaders` objects to be appended to another. -- | Allow one `ResponseHeaders` objects to be appended to another.
instance Semigroup ResponseHeaders where instance Semigroup ResponseHeaders where
append (ResponseHeaders a) (ResponseHeaders b) = ResponseHeaders $ union b a append (ResponseHeaders a) (ResponseHeaders b) = ResponseHeaders $ union b a
@ -82,7 +83,7 @@ instance Show ResponseHeaders where
instance Eq ResponseHeaders where instance Eq ResponseHeaders where
eq (ResponseHeaders a) (ResponseHeaders b) = eq a b eq (ResponseHeaders a) (ResponseHeaders b) = eq a b
-- | Get the headers out of a HTTP `Request` object. -- | Get the headers out of a HTTP `RequestHeaders` object.
read :: Request -> RequestHeaders read :: Request -> RequestHeaders
read = requestHeaders >>> fold insertField Map.empty >>> RequestHeaders read = requestHeaders >>> fold insertField Map.empty >>> RequestHeaders
where where
@ -95,18 +96,19 @@ write response (ResponseHeaders headers') = void $ traverseWithIndex writeField
where where
writeField key values = setHeaders response (unwrap key) values writeField key values = setHeaders response (unwrap key) values
-- | Return a `ResponseHeaders` containing nothing. -- | Return a `ResponseHeaders` containing no headers.
empty :: ResponseHeaders empty :: ResponseHeaders
empty = ResponseHeaders Map.empty empty = ResponseHeaders Map.empty
-- | Convert an `Array` of `Tuples` of 2 `Strings` to a `RequestHeaders` object.
-- -- | Convert an `Array` of `Tuples` of 2 `Strings` to a `Headers` object. -- | This is intended mainly for internal use.
mkRequestHeaders :: Array (Tuple String String) -> RequestHeaders mkRequestHeaders :: Array (Tuple String String) -> RequestHeaders
mkRequestHeaders = foldl insertField Map.empty >>> RequestHeaders mkRequestHeaders = foldl insertField Map.empty >>> RequestHeaders
where where
insertField x (Tuple key value) = insert (CaseInsensitiveString key) value x insertField x (Tuple key value) = insert (CaseInsensitiveString key) value x
-- | Create a singleton header from a key-value pair. -- | Create a singleton header from a key-value pair.
-- | This is intended mainly for internal use.
mkRequestHeader :: String -> String -> RequestHeaders mkRequestHeader :: String -> String -> RequestHeaders
mkRequestHeader key = singleton (CaseInsensitiveString key) >>> RequestHeaders mkRequestHeader key = singleton (CaseInsensitiveString key) >>> RequestHeaders
@ -114,6 +116,8 @@ mkRequestHeader key = singleton (CaseInsensitiveString key) >>> RequestHeaders
header :: String -> String -> ResponseHeaders header :: String -> String -> ResponseHeaders
header key = Array.singleton >>> singleton (CaseInsensitiveString key) >>> ResponseHeaders header key = Array.singleton >>> singleton (CaseInsensitiveString key) >>> ResponseHeaders
-- | Copy the request headers to the response headers
-- | This is intended mainly for internal use.
toResponseHeaders :: RequestHeaders -> ResponseHeaders toResponseHeaders :: RequestHeaders -> ResponseHeaders
toResponseHeaders = un RequestHeaders >>> map (Array.singleton) >>> ResponseHeaders toResponseHeaders = un RequestHeaders >>> map (Array.singleton) >>> ResponseHeaders
@ -156,7 +160,16 @@ else instance
tail = Record.delete (Proxy :: Proxy sym) rec tail = Record.delete (Proxy :: Proxy sym) rec
class ToHeaders r where class ToHeaders r where
-- | Create `ResponseHeaders` from a record, an `Array (Tuple String String)` or an `Array (Tuple String (Array String))`
headers :: r -> ResponseHeaders headers :: r -> ResponseHeaders
instance (RowToList r rl, ToHeadersHelper r rl) => ToHeaders (Record r) where instance ToHeaders (Array (Tuple String String)) where
headers = foldl insertField Map.empty >>> ResponseHeaders
where
insertField x (Tuple key value) = insert (CaseInsensitiveString key) (Array.singleton value) x
else instance ToHeaders (Array (Tuple String (Array String))) where
headers = foldl insertField Map.empty >>> ResponseHeaders
where
insertField x (Tuple key value) = insert (CaseInsensitiveString key) value x
else instance (RowToList r rl, ToHeadersHelper r rl) => ToHeaders (Record r) where
headers = headersImpl (Proxy :: Proxy rl) headers = headersImpl (Proxy :: Proxy rl)

View File

@ -7,7 +7,7 @@ import Effect.Aff (Aff)
import Effect.Class (liftEffect) import Effect.Class (liftEffect)
import Effect.Ref (new) as Ref import Effect.Ref (new) as Ref
import HTTPurple.Body (RequestBody, defaultHeaders, read, toBuffer, toStream, toString, write) import HTTPurple.Body (RequestBody, defaultHeaders, read, toBuffer, toStream, toString, write)
import HTTPurple.Headers (header) import HTTPurple.Headers (mkRequestHeader)
import Node.Buffer (Buffer, fromString) import Node.Buffer (Buffer, fromString)
import Node.Buffer (toString) as Buffer import Node.Buffer (toString) as Buffer
import Node.Encoding (Encoding(UTF8)) import Node.Encoding (Encoding(UTF8))
@ -72,20 +72,20 @@ defaultHeadersSpec =
describe "with an ASCII string" do describe "with an ASCII string" do
it "has the correct Content-Length header" do it "has the correct Content-Length header" do
headers <- liftEffect $ defaultHeaders "ascii" headers <- liftEffect $ defaultHeaders "ascii"
headers ?= header "Content-Length" "5" headers ?= (mkRequestHeader "Content-Length" "5")
describe "with a UTF-8 string" do describe "with a UTF-8 string" do
it "has the correct Content-Length header" do it "has the correct Content-Length header" do
headers <- liftEffect $ defaultHeaders "\x2603" headers <- liftEffect $ defaultHeaders "\x2603"
headers ?= header "Content-Length" "3" headers ?= (mkRequestHeader "Content-Length" "3")
describe "Buffer" do describe "Buffer" do
it "has the correct Content-Length header" do it "has the correct Content-Length header" do
buf :: Buffer <- liftEffect $ fromString "foobar" UTF8 buf :: Buffer <- liftEffect $ fromString "foobar" UTF8
headers <- liftEffect $ defaultHeaders buf headers <- liftEffect $ defaultHeaders buf
headers ?= header "Content-Length" "6" headers ?= (mkRequestHeader "Content-Length" "6")
describe "Readable" do describe "Readable" do
it "specifies the Transfer-Encoding header" do it "specifies the Transfer-Encoding header" do
headers <- liftEffect $ defaultHeaders $ stringToStream "test" headers <- liftEffect $ defaultHeaders $ stringToStream "test"
headers ?= header "Transfer-Encoding" "chunked" headers ?= (mkRequestHeader "Transfer-Encoding" "chunked")
writeSpec :: Test writeSpec :: Test
writeSpec = writeSpec =

View File

@ -5,7 +5,7 @@ import Prelude
import Data.Maybe (Maybe(Nothing, Just)) import Data.Maybe (Maybe(Nothing, Just))
import Data.Tuple (Tuple(Tuple)) import Data.Tuple (Tuple(Tuple))
import Effect.Class (liftEffect) import Effect.Class (liftEffect)
import HTTPurple.Headers (empty, header, headers, read, write) import HTTPurple.Headers (empty, header, headers, mkRequestHeader, mkRequestHeaders, read, write)
import HTTPurple.Lookup ((!!)) import HTTPurple.Lookup ((!!))
import Test.HTTPurple.TestHelpers ((?=)) import Test.HTTPurple.TestHelpers ((?=))
import Test.HTTPurple.TestHelpers as TestHelpers import Test.HTTPurple.TestHelpers as TestHelpers
@ -17,20 +17,20 @@ lookupSpec =
describe "when the string is in the header set" do describe "when the string is in the header set" do
describe "when searching with lowercase" do describe "when searching with lowercase" do
it "is Just the string" do it "is Just the string" do
header "x-test" "test" !! "x-test" ?= Just "test" mkRequestHeader "x-test" "test" !! "x-test" ?= Just "test"
describe "when searching with uppercase" do describe "when searching with uppercase" do
it "is Just the string" do it "is Just the string" do
header "x-test" "test" !! "X-Test" ?= Just "test" mkRequestHeader "x-test" "test" !! "X-Test" ?= Just "test"
describe "when the string is uppercase" do describe "when the string is uppercase" do
describe "when searching with lowercase" do describe "when searching with lowercase" do
it "is Just the string" do it "is Just the string" do
header "X-Test" "test" !! "x-test" ?= Just "test" mkRequestHeader "X-Test" "test" !! "x-test" ?= Just "test"
describe "when searching with uppercase" do describe "when searching with uppercase" do
it "is Just the string" do it "is Just the string" do
header "X-Test" "test" !! "X-Test" ?= Just "test" mkRequestHeader "X-Test" "test" !! "X-Test" ?= Just "test"
describe "when the string is not in the header set" do describe "when the string is not in the header set" do
it "is Nothing" do it "is Nothing" do
((empty !! "X-Test") :: Maybe String) ?= Nothing ((mkRequestHeaders [] !! "X-Test") :: Maybe String) ?= Nothing
showSpec :: TestHelpers.Test showSpec :: TestHelpers.Test
showSpec = showSpec =
@ -83,12 +83,12 @@ readSpec =
describe "with no headers" do describe "with no headers" do
it "is an empty Map" do it "is an empty Map" do
request <- TestHelpers.mockRequest "" "" "" "" [] request <- TestHelpers.mockRequest "" "" "" "" []
read request ?= empty read request ?= (mkRequestHeaders [])
describe "with headers" do describe "with headers" do
it "is a Map with the contents of the headers" do it "is a Map with the contents of the headers" do
let testHeader = [ Tuple "X-Test" "test" ] let testHeader = [ Tuple "X-Test" "test" ]
request <- TestHelpers.mockRequest "" "" "" "" testHeader request <- TestHelpers.mockRequest "" "" "" "" testHeader
read request ?= headers testHeader read request ?= mkRequestHeaders testHeader
writeSpec :: TestHelpers.Test writeSpec :: TestHelpers.Test
writeSpec = writeSpec =
@ -98,7 +98,7 @@ writeSpec =
mock <- TestHelpers.mockResponse mock <- TestHelpers.mockResponse
write mock $ header "X-Test" "test" write mock $ header "X-Test" "test"
pure $ TestHelpers.getResponseHeader "X-Test" mock pure $ TestHelpers.getResponseHeader "X-Test" mock
header ?= "test" header ?= [ "test" ]
emptySpec :: TestHelpers.Test emptySpec :: TestHelpers.Test
emptySpec = emptySpec =

View File

@ -3,10 +3,12 @@ module Test.HTTPurple.ResponseSpec where
import Prelude import Prelude
import Data.Either (Either(Right)) import Data.Either (Either(Right))
import Debug (spy)
import Effect.Aff (makeAff, nonCanceler) import Effect.Aff (makeAff, nonCanceler)
import Effect.Class (liftEffect) import Effect.Class (liftEffect)
import HTTPurple.Body (defaultHeaders) import HTTPurple.Body (defaultHeaders)
import HTTPurple.Headers (header, toResponseHeaders) import HTTPurple.Headers (toResponseHeaders)
import HTTPurple.Headers as Headers
import HTTPurple.Response (emptyResponse, emptyResponse', response, response', send) import HTTPurple.Response (emptyResponse, emptyResponse', response, response', send)
import Node.Encoding (Encoding(UTF8)) import Node.Encoding (Encoding(UTF8))
import Node.HTTP (responseAsStream) import Node.HTTP (responseAsStream)
@ -20,7 +22,7 @@ sendSpec =
let let
mockResponse' = mockResponse' =
{ status: 123 { status: 123
, headers: header "Test" "test" , headers: Headers.header "Test" "test"
, writeBody: , writeBody:
\response -> makeAff \done -> do \response -> makeAff \done -> do
stream <- pure $ responseAsStream response stream <- pure $ responseAsStream response
@ -32,7 +34,7 @@ sendSpec =
httpResponse <- liftEffect mockResponse httpResponse <- liftEffect mockResponse
send httpResponse mockResponse' send httpResponse mockResponse'
pure $ getResponseHeader "Test" httpResponse pure $ getResponseHeader "Test" httpResponse
header ?= "test" header ?= [ "test" ]
it "writes the status" do it "writes the status" do
status <- do status <- do
httpResponse <- liftEffect mockResponse httpResponse <- liftEffect mockResponse
@ -68,7 +70,7 @@ response'Spec :: Test
response'Spec = response'Spec =
describe "response'" do describe "response'" do
let let
mockHeaders = header "Test" "test" mockHeaders = Headers.header "Test" "test"
mockResponse' = response' 123 mockHeaders "test" mockResponse' = response' 123 mockHeaders "test"
it "has the right status" do it "has the right status" do
resp <- mockResponse' resp <- mockResponse'
@ -107,7 +109,7 @@ emptyResponse'Spec :: Test
emptyResponse'Spec = emptyResponse'Spec =
describe "emptyResponse'" do describe "emptyResponse'" do
let let
mockHeaders = header "Test" "test" mockHeaders = Headers.header "Test" "test"
mockResponse' = emptyResponse' 123 mockHeaders mockResponse' = emptyResponse' 123 mockHeaders
it "has the right status" do it "has the right status" do
resp <- mockResponse' resp <- mockResponse'

View File

@ -230,12 +230,12 @@ getResponseStatus :: HTTP.Response -> Int
getResponseStatus = _.statusCode <<< unsafeCoerce getResponseStatus = _.statusCode <<< unsafeCoerce
-- | Get all current headers on the HTTP Response object. -- | Get all current headers on the HTTP Response object.
getResponseHeaders :: HTTP.Response -> Object String getResponseHeaders :: HTTP.Response -> Object (Array String)
getResponseHeaders = unsafeCoerce <<< _.headers <<< unsafeCoerce getResponseHeaders = unsafeCoerce <<< _.headers <<< unsafeCoerce
-- | Get the current value for the header on the HTTP Response object. -- | Get the current value for the header on the HTTP Response object.
getResponseHeader :: String -> HTTP.Response -> String getResponseHeader :: String -> HTTP.Response -> Array String
getResponseHeader header = fromMaybe "" <<< lookup header <<< getResponseHeaders getResponseHeader header = fromMaybe [ "" ] <<< lookup header <<< getResponseHeaders
-- | Create a stream out of a string. -- | Create a stream out of a string.
foreign import stringToStream :: String -> Readable () foreign import stringToStream :: String -> Readable ()