Add HTTP version (#137)
* v0.8.1 * Add the HTTP version to `Request` The `node-http` `Request` has the HTTP version on it. We can make it available in our `Request` for consumers. We went the naive approach first and typed it as a string. There is some structure to the version, so we could attempt to parse it. It's unclear what we would do if parsing failed though. Maybe we want something like: ```PureScript data Version = Known { major :: Int, minor :: Int } | Unknown String ``` That would allow us to parse the known format, and fallback to accepting anything else. It's definitely something to discuss anyway. * Make HTTP version its own data type There are only a handful of HTTP versions that are commonly used. We can talk about those explicitly and fallback to any arbitrary version. The changes here try to follow the patterns elsewhere in the code base. Hopefully, it's not too far off.
This commit is contained in:
parent
e64565e497
commit
c208dffb7b
@ -17,6 +17,7 @@ import HTTPure.Method as Method
|
||||
import HTTPure.Path as Path
|
||||
import HTTPure.Query as Query
|
||||
import HTTPure.Utils (encodeURIComponent)
|
||||
import HTTPure.Version as Version
|
||||
|
||||
-- | The `Request` type is a `Record` type that includes fields for accessing
|
||||
-- | the different parts of the HTTP request.
|
||||
@ -26,6 +27,7 @@ type Request =
|
||||
, query :: Query.Query
|
||||
, headers :: Headers.Headers
|
||||
, body :: String
|
||||
, httpVersion :: Version.Version
|
||||
}
|
||||
|
||||
-- | Return the full resolved path, including query parameters. This may not
|
||||
@ -51,4 +53,5 @@ fromHTTPRequest request = do
|
||||
, query: Query.read request
|
||||
, headers: Headers.read request
|
||||
, body
|
||||
, httpVersion: Version.read request
|
||||
}
|
||||
|
41
src/HTTPure/Version.purs
Normal file
41
src/HTTPure/Version.purs
Normal file
@ -0,0 +1,41 @@
|
||||
module HTTPure.Version
|
||||
( Version(..)
|
||||
, read
|
||||
) where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Node.HTTP as HTTP
|
||||
|
||||
-- | These are the HTTP versions that HTTPure understands. There are five
|
||||
-- | commonly known versions which are explicitly named.
|
||||
data Version
|
||||
= HTTP0_9
|
||||
| HTTP1_0
|
||||
| HTTP1_1
|
||||
| HTTP2_0
|
||||
| HTTP3_0
|
||||
| Other String
|
||||
|
||||
-- | If two `Versions` are the same constructor, they are equal.
|
||||
derive instance eqVersion :: Eq Version
|
||||
|
||||
-- | Allow a `Version` to be represented as a string. This string is formatted
|
||||
-- | as it would be in an HTTP request/response.
|
||||
instance showVersion :: Show Version where
|
||||
show HTTP0_9 = "HTTP/0.9"
|
||||
show HTTP1_0 = "HTTP/1.0"
|
||||
show HTTP1_1 = "HTTP/1.1"
|
||||
show HTTP2_0 = "HTTP/2.0"
|
||||
show HTTP3_0 = "HTTP/3.0"
|
||||
show (Other version) = "HTTP/" <> version
|
||||
|
||||
-- | Take an HTTP `Request` and extract the `Version` for that request.
|
||||
read :: HTTP.Request -> Version
|
||||
read request = case HTTP.httpVersion request of
|
||||
"0.9" -> HTTP0_9
|
||||
"1.0" -> HTTP1_0
|
||||
"1.1" -> HTTP1_1
|
||||
"2.0" -> HTTP2_0
|
||||
"3.0" -> HTTP3_0
|
||||
version -> Other version
|
@ -16,7 +16,7 @@ import Test.HTTPure.TestHelpers ((?=))
|
||||
readSpec :: TestHelpers.Test
|
||||
readSpec = Spec.describe "read" do
|
||||
Spec.it "is the body of the Request" do
|
||||
request <- TestHelpers.mockRequest "GET" "" "test" []
|
||||
request <- TestHelpers.mockRequest "" "GET" "" "test" []
|
||||
body <- Body.read request
|
||||
body ?= "test"
|
||||
|
||||
|
@ -70,12 +70,12 @@ readSpec :: TestHelpers.Test
|
||||
readSpec = Spec.describe "read" do
|
||||
Spec.describe "with no headers" do
|
||||
Spec.it "is an empty Map" do
|
||||
request <- TestHelpers.mockRequest "" "" "" []
|
||||
request <- TestHelpers.mockRequest "" "" "" "" []
|
||||
Headers.read request ?= Headers.empty
|
||||
Spec.describe "with headers" do
|
||||
Spec.it "is a Map with the contents of the headers" do
|
||||
let testHeader = [Tuple.Tuple "X-Test" "test"]
|
||||
request <- TestHelpers.mockRequest "" "" "" testHeader
|
||||
request <- TestHelpers.mockRequest "" "" "" "" testHeader
|
||||
Headers.read request ?= Headers.headers testHeader
|
||||
|
||||
writeSpec :: TestHelpers.Test
|
||||
|
@ -43,7 +43,7 @@ readSpec :: TestHelpers.Test
|
||||
readSpec = Spec.describe "read" do
|
||||
Spec.describe "with a 'GET' Request" do
|
||||
Spec.it "is Get" do
|
||||
request <- TestHelpers.mockRequest "GET" "" "" []
|
||||
request <- TestHelpers.mockRequest "" "GET" "" "" []
|
||||
Method.read request ?= Method.Get
|
||||
|
||||
methodSpec :: TestHelpers.Test
|
||||
|
@ -13,26 +13,26 @@ readSpec :: TestHelpers.Test
|
||||
readSpec = Spec.describe "read" do
|
||||
Spec.describe "with a query string" do
|
||||
Spec.it "is just the path" do
|
||||
request <- TestHelpers.mockRequest "GET" "test/path?blabla" "" []
|
||||
request <- TestHelpers.mockRequest "" "GET" "test/path?blabla" "" []
|
||||
Path.read request ?= [ "test", "path" ]
|
||||
Spec.describe "with no query string" do
|
||||
Spec.it "is the path" do
|
||||
request <- TestHelpers.mockRequest "GET" "test/path" "" []
|
||||
request <- TestHelpers.mockRequest "" "GET" "test/path" "" []
|
||||
Path.read request ?= [ "test", "path" ]
|
||||
Spec.describe "with no segments" do
|
||||
Spec.it "is an empty array" do
|
||||
request <- TestHelpers.mockRequest "GET" "" "" []
|
||||
request <- TestHelpers.mockRequest "" "GET" "" "" []
|
||||
Path.read request ?= []
|
||||
Spec.describe "with empty segments" do
|
||||
Spec.it "strips the empty segments" do
|
||||
request <- TestHelpers.mockRequest "GET" "//test//path///?query" "" []
|
||||
request <- TestHelpers.mockRequest "" "GET" "//test//path///?query" "" []
|
||||
Path.read request ?= [ "test", "path" ]
|
||||
Spec.describe "with percent encoded segments" do
|
||||
Spec.it "decodes percent encoding" do
|
||||
request <- TestHelpers.mockRequest "GET" "/test%20path/%2Fthis" "" []
|
||||
request <- TestHelpers.mockRequest "" "GET" "/test%20path/%2Fthis" "" []
|
||||
Path.read request ?= [ "test path", "/this" ]
|
||||
Spec.it "does not decode a plus sign" do
|
||||
request <- TestHelpers.mockRequest "GET" "/test+path/this" "" []
|
||||
request <- TestHelpers.mockRequest "" "GET" "/test+path/this" "" []
|
||||
Path.read request ?= [ "test+path", "this" ]
|
||||
|
||||
pathSpec :: TestHelpers.Test
|
||||
|
@ -15,41 +15,41 @@ readSpec :: TestHelpers.Test
|
||||
readSpec = Spec.describe "read" do
|
||||
Spec.describe "with no query string" do
|
||||
Spec.it "is an empty Map" do
|
||||
req <- TestHelpers.mockRequest "" "/test" "" []
|
||||
req <- TestHelpers.mockRequest "" "" "/test" "" []
|
||||
Query.read req ?= Object.empty
|
||||
Spec.describe "with an empty query string" do
|
||||
Spec.it "is an empty Map" do
|
||||
req <- TestHelpers.mockRequest "" "/test?" "" []
|
||||
req <- TestHelpers.mockRequest "" "" "/test?" "" []
|
||||
Query.read req ?= Object.empty
|
||||
Spec.describe "with a query parameter in the query string" do
|
||||
Spec.it "is a correct Map" do
|
||||
req <- TestHelpers.mockRequest "" "/test?a=b" "" []
|
||||
req <- TestHelpers.mockRequest "" "" "/test?a=b" "" []
|
||||
Query.read req ?= Object.singleton "a" "b"
|
||||
Spec.describe "with empty fields in the query string" do
|
||||
Spec.it "ignores the empty fields" do
|
||||
req <- TestHelpers.mockRequest "" "/test?&&a=b&&" "" []
|
||||
req <- TestHelpers.mockRequest "" "" "/test?&&a=b&&" "" []
|
||||
Query.read req ?= Object.singleton "a" "b"
|
||||
Spec.describe "with duplicated params" do
|
||||
Spec.it "takes the last param value" do
|
||||
req <- TestHelpers.mockRequest "" "/test?a=b&a=c" "" []
|
||||
req <- TestHelpers.mockRequest "" "" "/test?a=b&a=c" "" []
|
||||
Query.read req ?= Object.singleton "a" "c"
|
||||
Spec.describe "with empty params" do
|
||||
Spec.it "uses '' as the value" do
|
||||
req <- TestHelpers.mockRequest "" "/test?a" "" []
|
||||
req <- TestHelpers.mockRequest "" "" "/test?a" "" []
|
||||
Query.read req ?= Object.singleton "a" ""
|
||||
Spec.describe "with complex params" do
|
||||
Spec.it "is the correct Map" do
|
||||
req <- TestHelpers.mockRequest "" "/test?&&a&b=c&b=d&&&e=f&g=&" "" []
|
||||
req <- TestHelpers.mockRequest "" "" "/test?&&a&b=c&b=d&&&e=f&g=&" "" []
|
||||
Query.read req ?= expectedComplexResult
|
||||
Spec.describe "with urlencoded params" do
|
||||
Spec.it "decodes valid keys and values" do
|
||||
req <- TestHelpers.mockRequest "" "/test?foo%20bar=%3Fx%3Dtest" "" []
|
||||
req <- TestHelpers.mockRequest "" "" "/test?foo%20bar=%3Fx%3Dtest" "" []
|
||||
Query.read req ?= Object.singleton "foo bar" "?x=test"
|
||||
Spec.it "passes invalid keys and values through" do
|
||||
req <- TestHelpers.mockRequest "" "/test?%%=%C3" "" []
|
||||
req <- TestHelpers.mockRequest "" "" "/test?%%=%C3" "" []
|
||||
Query.read req ?= Object.singleton "%%" "%C3"
|
||||
Spec.it "converts + to a space" do
|
||||
req <- TestHelpers.mockRequest "" "/test?foo=bar+baz" "" []
|
||||
req <- TestHelpers.mockRequest "" "" "/test?foo=bar+baz" "" []
|
||||
Query.read req ?= Object.singleton "foo" "bar baz"
|
||||
where
|
||||
expectedComplexResult =
|
||||
|
@ -9,6 +9,7 @@ import Test.Spec as Spec
|
||||
import HTTPure.Headers as Headers
|
||||
import HTTPure.Method as Method
|
||||
import HTTPure.Request as Request
|
||||
import HTTPure.Version as Version
|
||||
|
||||
import Test.HTTPure.TestHelpers as TestHelpers
|
||||
import Test.HTTPure.TestHelpers ((?=))
|
||||
@ -30,10 +31,13 @@ fromHTTPRequestSpec = Spec.describe "fromHTTPRequest" do
|
||||
Spec.it "contains the correct body" do
|
||||
mock <- mockRequest
|
||||
mock.body ?= "body"
|
||||
Spec.it "contains the correct httpVersion" do
|
||||
mock <- mockRequest
|
||||
mock.httpVersion ?= Version.HTTP1_1
|
||||
where
|
||||
mockHeaders = [ Tuple.Tuple "Test" "test" ]
|
||||
mockHTTPRequest =
|
||||
TestHelpers.mockRequest "POST" "/test?a=b" "body" mockHeaders
|
||||
TestHelpers.mockRequest "1.1" "POST" "/test?a=b" "body" mockHeaders
|
||||
mockRequest = mockHTTPRequest >>= Request.fromHTTPRequest
|
||||
|
||||
fullPathSpec :: TestHelpers.Test
|
||||
@ -67,7 +71,7 @@ fullPathSpec = Spec.describe "fullPath" do
|
||||
mock <- mockRequest "/foo///bar/?&a=b&&c"
|
||||
Request.fullPath mock ?= "/foo/bar?a=b&c="
|
||||
where
|
||||
mockHTTPRequest path = TestHelpers.mockRequest "POST" path "body" []
|
||||
mockHTTPRequest path = TestHelpers.mockRequest "" "POST" path "body" []
|
||||
mockRequest path = mockHTTPRequest path >>= Request.fromHTTPRequest
|
||||
|
||||
requestSpec :: TestHelpers.Test
|
||||
|
@ -1,21 +1,24 @@
|
||||
"use strict";
|
||||
|
||||
exports.mockRequestImpl = function(method) {
|
||||
return function(url) {
|
||||
return function(body) {
|
||||
return function(headers) {
|
||||
return function() {
|
||||
var stream = new require('stream').Readable({
|
||||
read: function(size) {
|
||||
this.push(body);
|
||||
this.push(null);
|
||||
}
|
||||
});
|
||||
stream.method = method;
|
||||
stream.url = url;
|
||||
stream.headers = headers;
|
||||
exports.mockRequestImpl = function(httpVersion) {
|
||||
return function(method) {
|
||||
return function(url) {
|
||||
return function(body) {
|
||||
return function(headers) {
|
||||
return function() {
|
||||
var stream = new require('stream').Readable({
|
||||
read: function(size) {
|
||||
this.push(body);
|
||||
this.push(null);
|
||||
}
|
||||
});
|
||||
stream.method = method;
|
||||
stream.url = url;
|
||||
stream.headers = headers;
|
||||
stream.httpVersion = httpVersion;
|
||||
|
||||
return stream;
|
||||
return stream;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -137,17 +137,19 @@ foreign import mockRequestImpl ::
|
||||
String ->
|
||||
String ->
|
||||
String ->
|
||||
String ->
|
||||
Object.Object String ->
|
||||
Effect.Effect HTTP.Request
|
||||
|
||||
-- | Mock an HTTP Request object
|
||||
mockRequest :: String ->
|
||||
String ->
|
||||
String ->
|
||||
String ->
|
||||
Array (Tuple.Tuple String String) ->
|
||||
Aff.Aff HTTP.Request
|
||||
mockRequest method url body =
|
||||
EffectClass.liftEffect <<< mockRequestImpl method url body <<< Object.fromFoldable
|
||||
mockRequest httpVersion method url body =
|
||||
EffectClass.liftEffect <<< mockRequestImpl httpVersion method url body <<< Object.fromFoldable
|
||||
|
||||
-- | Mock an HTTP Response object
|
||||
foreign import mockResponse :: Effect.Effect HTTP.Response
|
||||
|
63
test/Test/HTTPure/VersionSpec.purs
Normal file
63
test/Test/HTTPure/VersionSpec.purs
Normal file
@ -0,0 +1,63 @@
|
||||
module Test.HTTPure.VersionSpec where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Test.Spec as Spec
|
||||
|
||||
import HTTPure.Version as Version
|
||||
|
||||
import Test.HTTPure.TestHelpers as TestHelpers
|
||||
import Test.HTTPure.TestHelpers ((?=))
|
||||
|
||||
showSpec :: TestHelpers.Test
|
||||
showSpec = Spec.describe "show" do
|
||||
Spec.describe "with an HTTP0_9" do
|
||||
Spec.it "is 'HTTP0_9'" do
|
||||
show Version.HTTP0_9 ?= "HTTP/0.9"
|
||||
Spec.describe "with an HTTP1_0" do
|
||||
Spec.it "is 'HTTP1_0'" do
|
||||
show Version.HTTP1_0 ?= "HTTP/1.0"
|
||||
Spec.describe "with an HTTP1_1" do
|
||||
Spec.it "is 'HTTP1_1'" do
|
||||
show Version.HTTP1_1 ?= "HTTP/1.1"
|
||||
Spec.describe "with an HTTP2_0" do
|
||||
Spec.it "is 'HTTP2_0'" do
|
||||
show Version.HTTP2_0 ?= "HTTP/2.0"
|
||||
Spec.describe "with an HTTP3_0" do
|
||||
Spec.it "is 'HTTP3_0'" do
|
||||
show Version.HTTP3_0 ?= "HTTP/3.0"
|
||||
Spec.describe "with an Other" do
|
||||
Spec.it "is 'Other'" do
|
||||
show (Version.Other "version") ?= "HTTP/version"
|
||||
|
||||
readSpec :: TestHelpers.Test
|
||||
readSpec = Spec.describe "read" do
|
||||
Spec.describe "with an 'HTTP0_9' Request" do
|
||||
Spec.it "is HTTP0_9" do
|
||||
request <- TestHelpers.mockRequest "0.9" "" "" "" []
|
||||
Version.read request ?= Version.HTTP0_9
|
||||
Spec.describe "with an 'HTTP1_0' Request" do
|
||||
Spec.it "is HTTP1_0" do
|
||||
request <- TestHelpers.mockRequest "1.0" "" "" "" []
|
||||
Version.read request ?= Version.HTTP1_0
|
||||
Spec.describe "with an 'HTTP1_1' Request" do
|
||||
Spec.it "is HTTP1_1" do
|
||||
request <- TestHelpers.mockRequest "1.1" "" "" "" []
|
||||
Version.read request ?= Version.HTTP1_1
|
||||
Spec.describe "with an 'HTTP2_0' Request" do
|
||||
Spec.it "is HTTP2_0" do
|
||||
request <- TestHelpers.mockRequest "2.0" "" "" "" []
|
||||
Version.read request ?= Version.HTTP2_0
|
||||
Spec.describe "with an 'HTTP3_0' Request" do
|
||||
Spec.it "is HTTP3_0" do
|
||||
request <- TestHelpers.mockRequest "3.0" "" "" "" []
|
||||
Version.read request ?= Version.HTTP3_0
|
||||
Spec.describe "with an 'Other' Request" do
|
||||
Spec.it "is Other" do
|
||||
request <- TestHelpers.mockRequest "version" "" "" "" []
|
||||
Version.read request ?= Version.Other "version"
|
||||
|
||||
versionSpec :: TestHelpers.Test
|
||||
versionSpec = Spec.describe "Version" do
|
||||
showSpec
|
||||
readSpec
|
@ -16,6 +16,7 @@ import Test.HTTPure.RequestSpec as RequestSpec
|
||||
import Test.HTTPure.ResponseSpec as ResponseSpec
|
||||
import Test.HTTPure.ServerSpec as ServerSpec
|
||||
import Test.HTTPure.StatusSpec as StatusSpec
|
||||
import Test.HTTPure.VersionSpec as VersionSpec
|
||||
import Test.HTTPure.IntegrationSpec as IntegrationSpec
|
||||
|
||||
import Test.HTTPure.TestHelpers as TestHelpers
|
||||
@ -32,4 +33,5 @@ main = Runner.run [ Reporter.specReporter ] $ Spec.describe "HTTPure" do
|
||||
ResponseSpec.responseSpec
|
||||
ServerSpec.serverSpec
|
||||
StatusSpec.statusSpec
|
||||
VersionSpec.versionSpec
|
||||
IntegrationSpec.integrationSpec
|
||||
|
Loading…
Reference in New Issue
Block a user