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
## Unpublished
- Separate `Headers` into `RequestHeaders` and `ResponseHeaders`
- Allow passing a record to `headers` to make `ResponseHeaders` easier
## 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.
## 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
* 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.Maybe (Maybe(..))
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 Routing.Duplex as RD
import Routing.Duplex.Generic as RG
@ -23,7 +23,7 @@ route = RD.root $ RG.sum
filePath :: String
filePath = "./docs/Examples/BinaryResponse/circle.png"
responseHeaders :: Headers
responseHeaders :: ResponseHeaders
responseHeaders = header "Content-Type" "image/png"
-- | Respond with image data when run

View File

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

View File

@ -12,8 +12,7 @@ module HTTPurple.Headers
, read
, toResponseHeaders
, write
)
where
) where
import Prelude
@ -36,8 +35,9 @@ import Prim.RowList (class RowToList, Cons, Nil)
import Record as Record
import Type.Proxy (Proxy(..))
-- | The `RequestHeaders` type is just sugar for a `Object` of `Strings`
-- | that represents the set of headers in an HTTP request or response.
-- | The `RequestHeaders` type is a wrapper for a map
-- | 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)
derive instance Newtype RequestHeaders _
@ -47,14 +47,14 @@ derive instance Newtype RequestHeaders _
instance Lookup RequestHeaders String String where
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.
instance Show RequestHeaders where
show (RequestHeaders headers') = foldMapWithIndex showField headers' <> "\n"
where
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
eq (RequestHeaders a) (RequestHeaders b) = eq a b
@ -62,11 +62,12 @@ instance Eq RequestHeaders where
instance Semigroup RequestHeaders where
append (RequestHeaders a) (RequestHeaders b) = RequestHeaders $ union b a
-- | The `RequestHeaders` type is just sugar for a `Object` of `Strings`
-- | that represents the set of headers in an HTTP request or response.
-- | The `ResponseHeaders` type is a wrapper for a map
-- | 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))
-- | Allow one `ResponseHeaders` objects to be appended to another.
instance Semigroup ResponseHeaders where
append (ResponseHeaders a) (ResponseHeaders b) = ResponseHeaders $ union b a
@ -82,7 +83,7 @@ instance Show ResponseHeaders where
instance Eq ResponseHeaders where
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 = requestHeaders >>> fold insertField Map.empty >>> RequestHeaders
where
@ -95,18 +96,19 @@ write response (ResponseHeaders headers') = void $ traverseWithIndex writeField
where
writeField key values = setHeaders response (unwrap key) values
-- | Return a `ResponseHeaders` containing nothing.
-- | Return a `ResponseHeaders` containing no headers.
empty :: ResponseHeaders
empty = ResponseHeaders Map.empty
-- -- | Convert an `Array` of `Tuples` of 2 `Strings` to a `Headers` object.
-- | Convert an `Array` of `Tuples` of 2 `Strings` to a `RequestHeaders` object.
-- | This is intended mainly for internal use.
mkRequestHeaders :: Array (Tuple String String) -> RequestHeaders
mkRequestHeaders = foldl insertField Map.empty >>> RequestHeaders
where
insertField x (Tuple key value) = insert (CaseInsensitiveString key) value x
-- | Create a singleton header from a key-value pair.
-- | This is intended mainly for internal use.
mkRequestHeader :: String -> String -> RequestHeaders
mkRequestHeader key = singleton (CaseInsensitiveString key) >>> RequestHeaders
@ -114,6 +116,8 @@ mkRequestHeader key = singleton (CaseInsensitiveString key) >>> RequestHeaders
header :: String -> String -> 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 = un RequestHeaders >>> map (Array.singleton) >>> ResponseHeaders
@ -128,7 +132,7 @@ else instance
, RowToList r rl
, RowToList tail tailRL
, Row.Cons sym String tail r
, Row.Lacks sym tail
, Row.Lacks sym tail
, ToHeadersHelper tail tailRL
) =>
ToHeadersHelper r (Cons sym String tailRL) where
@ -143,7 +147,7 @@ else instance
, RowToList r rl
, RowToList tail tailRL
, Row.Cons sym (Array String) tail r
, Row.Lacks sym tail
, Row.Lacks sym tail
, ToHeadersHelper tail tailRL
) =>
ToHeadersHelper r (Cons sym (Array String) tailRL) where
@ -156,7 +160,16 @@ else instance
tail = Record.delete (Proxy :: Proxy sym) rec
class ToHeaders r where
-- | Create `ResponseHeaders` from a record, an `Array (Tuple String String)` or an `Array (Tuple String (Array String))`
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)

View File

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

View File

@ -5,7 +5,7 @@ import Prelude
import Data.Maybe (Maybe(Nothing, Just))
import Data.Tuple (Tuple(Tuple))
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 Test.HTTPurple.TestHelpers ((?=))
import Test.HTTPurple.TestHelpers as TestHelpers
@ -17,20 +17,20 @@ lookupSpec =
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 "test"
mkRequestHeader "x-test" "test" !! "x-test" ?= Just "test"
describe "when searching with uppercase" 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 searching with lowercase" 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
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
it "is Nothing" do
((empty !! "X-Test") :: Maybe String) ?= Nothing
((mkRequestHeaders [] !! "X-Test") :: Maybe String) ?= Nothing
showSpec :: TestHelpers.Test
showSpec =
@ -83,12 +83,12 @@ readSpec =
describe "with no headers" do
it "is an empty Map" do
request <- TestHelpers.mockRequest "" "" "" "" []
read request ?= empty
read request ?= (mkRequestHeaders [])
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
read request ?= mkRequestHeaders testHeader
writeSpec :: TestHelpers.Test
writeSpec =
@ -98,7 +98,7 @@ writeSpec =
mock <- TestHelpers.mockResponse
write mock $ header "X-Test" "test"
pure $ TestHelpers.getResponseHeader "X-Test" mock
header ?= "test"
header ?= [ "test" ]
emptySpec :: TestHelpers.Test
emptySpec =

View File

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

View File

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