Created
May 25, 2016 18:05
-
-
Save bshepherdson/c14f5338185f021da432a233e7f7347a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use "net" | |
primitive _PayloadStart | |
primitive _PayloadHeaders | |
primitive _PayloadContentLength | |
primitive _PayloadChunkStart | |
primitive _PayloadChunk | |
primitive _PayloadChunkEnd | |
primitive _PayloadBody | |
primitive _PayloadReady | |
primitive _PayloadError | |
type _PayloadState is | |
( _PayloadStart | |
| _PayloadHeaders | |
| _PayloadContentLength | |
| _PayloadChunkStart | |
| _PayloadChunk | |
| _PayloadChunkEnd | |
| _PayloadBody | |
| _PayloadReady | |
| _PayloadError | |
) | |
// The only difference between a request and a response is the first line. | |
// So we use a Strategy pattern to handle parsing that first line. | |
// If the apply succeeds, we have our Request created and move on the | |
// _PayloadHeaders. If it fails, _PayloadError. | |
interface _PayloadParser[A: Payload iso] | |
fun apply(buffer: ReadBuffer): A^ ? | |
primitive _RequestParser is _PayloadParser[Request iso] | |
fun apply(buffer: ReadBuffer): Request iso^ ? => | |
""" | |
Look for: "<Method> <URL> <Proto>". | |
""" | |
let line = buffer.line() | |
let method_end = line.find(" ") | |
let method = recover val line.substring(0, method_end) end | |
let url_end = line.find(" ", method_end + 1) | |
let url = URL.valid(line.substring(method_end + 1, url_end)) | |
var req = recover Request(url, method) end | |
req.proto = line.substring(url_end + 1) | |
req | |
primitive _ResponseParser is _PayloadParser[Response iso] | |
fun apply(buffer: ReadBuffer): Response iso^ ? => | |
""" | |
Look for: "<Proto> <Code> <Description>". | |
""" | |
let line = buffer.line() | |
let proto_end = line.find(" ") | |
let proto = line.substring(0, proto_end) | |
let status = line.read_int[U16](proto_end + 1)._1 | |
let status_end = line.find(" ", proto_end + 1) | |
let description = line.substring(status_end + 1) | |
recover Response(status, consume description) end | |
class _PayloadBuilder[A: Payload iso, Parser: _PayloadParser[A]] | |
""" | |
This builds a Payload using received chunks of data. | |
""" | |
var _state: _PayloadState = _PayloadStart | |
var _payload: (A | None) = None | |
var _content_length: USize = 0 | |
var _chunked: Bool = false | |
fun ref parse(buffer: ReadBuffer) => | |
""" | |
Parse available data based on our state. _ResponseBody is not listed here. | |
In that state, we wait for the connection to close and treat all pending | |
data as the response body. | |
""" | |
match _state | |
| _PayloadStart => _parse_top(buffer) | |
| _PayloadHeaders => _parse_headers(buffer) | |
| _PayloadChunkStart => _parse_chunk_start(buffer) | |
| _PayloadChunk => _parse_chunk(buffer) | |
| _PayloadChunkEnd => _parse_chunk_end(buffer) | |
| _PayloadContentLength => _parse_content_length(buffer) | |
end | |
fun ref done(): A^ => | |
""" | |
Finish parsing. Returns the payload if it is ready, otherwise an empty | |
payload. | |
Leaves the PayloadBuilder ready to reuse afterward. | |
""" | |
let result = _state = _PayloadStart | |
try | |
match _payload = None | |
| None => error | |
| let p: A => | |
_content_length = 0 | |
_chunked = false | |
if result isnt _PayloadReady then | |
error | |
end | |
p | |
else | |
error | |
end | |
else | |
recover A._client_fail() end | |
end | |
fun ref closed(buffer: ReadBuffer) => | |
""" | |
The connection has closed, which may signal that all remaining data is the | |
payload body. | |
""" | |
if _state is _PayloadBody then | |
_content_length = buffer.size() | |
try | |
let chunk = buffer.block(_content_length) | |
(_payload as A).add_chunk(consume chunk) | |
_state = _PayloadReady | |
end | |
end | |
fun ref _parse_top(buffer: ReadBuffer) => | |
""" | |
Use the strategy (Parser) to parse the top line depending on whether this is | |
a request or response we're receiving now. | |
""" | |
try | |
_payload = Parser(buffer) | |
_state = _PayloadHeaders | |
else | |
_payload = None | |
_state = _PayloadError | |
end | |
fun ref _parse_headers(buffer: ReadBuffer) => | |
""" | |
Look for: "<Key>:<Value>" or an empty line. | |
""" | |
try | |
while true do | |
let line = buffer.line() | |
if line.size() > 0 then | |
try | |
let i = line.find(":") | |
let key = recover val line.substring(0, i).strip() end | |
let value = recover val line.substring(i + 1).strip() end | |
_payload(key) = value | |
match key.lower() | |
| "content-length" => | |
_content_length = value.read_int[USize]()._1 | |
| "transfer-encoding" => | |
try | |
value.find("chunked") | |
_chunked = true | |
end | |
| "host" => | |
// TODO: set url host and service | |
None | |
| "authorization" => | |
// TODO: set url username and password | |
None | |
end | |
else | |
_state = _PayloadError | |
end | |
else | |
if | |
(_payload.status == 204) or | |
(_payload.status == 304) or | |
((_payload.status > 0) and (_payload.status < 200)) | |
then | |
_state = _PayloadReady | |
elseif _chunked then | |
_content_length = 0 | |
_state = _PayloadChunkStart | |
parse(buffer) | |
elseif _content_length > 0 then | |
_state = _PayloadContentLength | |
parse(buffer) | |
else | |
if _client then | |
_state = _PayloadBody | |
parse(buffer) | |
else | |
_state = _PayloadReady | |
end | |
end | |
return | |
end | |
end | |
end | |
fun ref _parse_content_length(buffer: ReadBuffer) => | |
""" | |
Look for _content_length available bytes. | |
""" | |
try | |
let body = buffer.block(_content_length) | |
_payload.add_chunk(consume body) | |
_state = _PayloadReady | |
end | |
fun ref _parse_chunk_start(buffer: ReadBuffer) => | |
""" | |
Look for the beginning of a chunk. | |
""" | |
try | |
let line = buffer.line() | |
if line.size() > 0 then | |
_content_length = line.read_int[USize](0, 16)._1 | |
if _content_length > 0 then | |
_state = _PayloadChunk | |
else | |
_state = _PayloadChunkEnd | |
end | |
parse(buffer) | |
else | |
_content_length = 0 | |
_state = _PayloadError | |
end | |
end | |
fun ref _parse_chunk(buffer: ReadBuffer) => | |
""" | |
Look for a chunk. | |
""" | |
try | |
let chunk = buffer.block(_content_length) | |
_payload.add_chunk(consume chunk) | |
_state = _PayloadChunkEnd | |
parse(buffer) | |
end | |
fun ref _parse_chunk_end(buffer: ReadBuffer) => | |
""" | |
Look for a blank line. | |
""" | |
try | |
let line = buffer.line() | |
if line.size() == 0 then | |
if _content_length > 0 then | |
_state = _PayloadChunkStart | |
parse(buffer) | |
else | |
_state = _PayloadReady | |
end | |
else | |
_state = _PayloadError | |
end | |
end | |
type _RequestBuilder is _PayloadBuilder[Request iso, _RequestParser] | |
type _ResponseBuilder is _PayloadBuilder[Response iso, _ResponseParser] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment