Skip to content

Instantly share code, notes, and snippets.

@bshepherdson
Created May 25, 2016 18:05
Show Gist options
  • Save bshepherdson/c14f5338185f021da432a233e7f7347a to your computer and use it in GitHub Desktop.
Save bshepherdson/c14f5338185f021da432a233e7f7347a to your computer and use it in GitHub Desktop.
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