use crate::tnetstring;
use std::fmt;
use std::io;
use std::str;
pub const IDS_MAX: usize = 128;
const HEADERS_MAX: usize = 32;
const EMPTY_BYTES: &[u8] = b"";
const EMPTY_ID: Id = Id {
id: EMPTY_BYTES,
seq: None,
};
pub const EMPTY_HEADER: Header = Header {
name: "",
value: EMPTY_BYTES,
};
const EMPTY_HEADERS: [Header; 0] = [EMPTY_HEADER; 0];
#[derive(Clone, Copy)]
pub struct Id<'a> {
pub id: &'a [u8],
pub seq: Option<u32>,
}
pub struct Header<'a> {
pub name: &'a str,
pub value: &'a [u8],
}
#[derive(Debug, PartialEq)]
pub enum ContentType {
Text,
Binary,
}
pub struct RequestData<'buf, 'headers> {
pub credits: u32,
pub more: bool,
pub stream: bool,
pub max_size: u32,
pub timeout: u32,
pub method: &'buf str,
pub uri: &'buf str,
pub headers: &'headers [Header<'buf>],
pub content_type: Option<ContentType>,
pub body: &'buf [u8],
pub peer_address: &'buf str,
pub peer_port: u16,
}
impl RequestData<'_, '_> {
pub fn new() -> Self {
Self {
credits: 0,
more: false,
stream: false,
max_size: 0,
timeout: 0,
method: "",
uri: "",
headers: &EMPTY_HEADERS,
body: EMPTY_BYTES,
content_type: None,
peer_address: "",
peer_port: 0,
}
}
}
pub struct ResponseData<'buf, 'headers> {
pub credits: u32,
pub more: bool,
pub code: u32,
pub reason: &'buf str,
pub headers: &'headers [Header<'buf>],
pub content_type: Option<ContentType>,
pub body: &'buf [u8],
}
pub struct RejectedInfo<'buf, 'headers> {
pub code: u32,
pub reason: &'buf str,
pub headers: &'headers [Header<'buf>],
pub body: &'buf [u8],
}
pub struct ErrorData<'buf, 'headers> {
pub condition: &'buf str,
pub rejected_info: Option<RejectedInfo<'buf, 'headers>>,
}
pub struct CreditData {
pub credits: u32,
}
pub struct CloseData<'a> {
pub status: Option<(u16, &'a str)>,
}
pub struct PingData<'a> {
pub body: &'a [u8],
}
pub struct PongData<'a> {
pub body: &'a [u8],
}
pub enum RequestPacket<'buf, 'headers> {
Data(RequestData<'buf, 'headers>),
Error(ErrorData<'buf, 'headers>),
Credit(CreditData),
KeepAlive,
Cancel,
HandoffStart,
HandoffProceed,
Close(CloseData<'buf>),
Ping(PingData<'buf>),
Pong(PongData<'buf>),
}
pub enum ResponsePacket<'buf, 'headers> {
Unknown,
Data(ResponseData<'buf, 'headers>),
Error(ErrorData<'buf, 'headers>),
Credit(CreditData),
KeepAlive,
Cancel,
HandoffStart,
HandoffProceed,
Close(CloseData<'buf>),
Ping(PingData<'buf>),
Pong(PongData<'buf>),
}
pub struct Request<'buf, 'ids, 'headers> {
pub from: &'buf [u8],
pub ids: &'ids [Id<'buf>],
pub multi: bool,
pub ptype: RequestPacket<'buf, 'headers>,
}
impl<'buf, 'ids, 'headers> Request<'buf, 'ids, 'headers> {
pub fn new_data(
from: &'buf [u8],
ids: &'ids [Id<'buf>],
data: RequestData<'buf, 'headers>,
) -> Self {
Self {
from: from,
ids: ids,
multi: false,
ptype: RequestPacket::Data(data),
}
}
pub fn new_error(from: &'buf [u8], ids: &'ids [Id<'buf>], condition: &'buf str) -> Self {
Self {
from: from,
ids: ids,
multi: false,
ptype: RequestPacket::Error(ErrorData {
condition,
rejected_info: None,
}),
}
}
pub fn new_credit(from: &'buf [u8], ids: &'ids [Id<'buf>], credits: u32) -> Self {
Self {
from: from,
ids: ids,
multi: false,
ptype: RequestPacket::Credit(CreditData { credits }),
}
}
pub fn new_keep_alive(from: &'buf [u8], ids: &'ids [Id<'buf>]) -> Self {
Self {
from: from,
ids: ids,
multi: false,
ptype: RequestPacket::KeepAlive,
}
}
pub fn new_cancel(from: &'buf [u8], ids: &'ids [Id<'buf>]) -> Self {
Self {
from: from,
ids: ids,
multi: false,
ptype: RequestPacket::Cancel,
}
}
pub fn new_handoff_start(from: &'buf [u8], ids: &'ids [Id<'buf>]) -> Self {
Self {
from: from,
ids: ids,
multi: false,
ptype: RequestPacket::HandoffStart,
}
}
pub fn new_handoff_proceed(from: &'buf [u8], ids: &'ids [Id<'buf>]) -> Self {
Self {
from: from,
ids: ids,
multi: false,
ptype: RequestPacket::HandoffProceed,
}
}
pub fn new_close(
from: &'buf [u8],
ids: &'ids [Id<'buf>],
status: Option<(u16, &'buf str)>,
) -> Self {
Self {
from: from,
ids: ids,
multi: false,
ptype: RequestPacket::Close(CloseData { status }),
}
}
pub fn new_ping(from: &'buf [u8], ids: &'ids [Id<'buf>], body: &'buf [u8]) -> Self {
Self {
from: from,
ids: ids,
multi: false,
ptype: RequestPacket::Ping(PingData { body }),
}
}
pub fn new_pong(from: &'buf [u8], ids: &'ids [Id<'buf>], body: &'buf [u8]) -> Self {
Self {
from: from,
ids: ids,
multi: false,
ptype: RequestPacket::Pong(PongData { body }),
}
}
pub fn serialize(&self, dest: &mut [u8]) -> Result<usize, io::Error> {
if dest.len() < 1 {
return Err(io::Error::from(io::ErrorKind::WriteZero));
}
dest[0] = b'T';
let mut cursor = io::Cursor::new(&mut dest[1..]);
let mut w = tnetstring::Writer::new(&mut cursor);
w.start_map()?;
if !self.from.is_empty() {
w.write_string(b"from")?;
w.write_string(self.from)?;
}
if self.ids.len() == 1 {
w.write_string(b"id")?;
w.write_string(self.ids[0].id)?;
if let Some(seq) = self.ids[0].seq {
w.write_string(b"seq")?;
w.write_int(seq as isize)?;
}
} else if self.ids.len() > 1 {
w.write_string(b"id")?;
w.start_array()?;
for id in self.ids.iter() {
w.start_map()?;
w.write_string(b"id")?;
w.write_string(id.id)?;
if let Some(seq) = id.seq {
w.write_string(b"seq")?;
w.write_int(seq as isize)?;
}
w.end_map()?;
}
w.end_array()?;
}
if self.multi {
w.write_string(b"ext")?;
w.start_map()?;
w.write_string(b"multi")?;
w.write_bool(true)?;
w.end_map()?;
}
match &self.ptype {
RequestPacket::Data(data) => {
if !data.method.is_empty() {
w.write_string(b"method")?;
w.write_string(data.method.as_bytes())?;
}
if !data.uri.is_empty() {
w.write_string(b"uri")?;
w.write_string(data.uri.as_bytes())?;
}
if !data.headers.is_empty() {
w.write_string(b"headers")?;
w.start_array()?;
for h in data.headers.iter() {
w.start_array()?;
w.write_string(h.name.as_bytes())?;
w.write_string(h.value)?;
w.end_array()?;
}
w.end_array()?;
}
if let Some(ctype) = &data.content_type {
w.write_string(b"content-type")?;
let s: &[u8] = match ctype {
ContentType::Text => b"text",
ContentType::Binary => b"binary",
};
w.write_string(s)?;
}
if !data.body.is_empty() {
w.write_string(b"body")?;
w.write_string(&data.body)?;
}
if data.credits > 0 {
w.write_string(b"credits")?;
w.write_int(data.credits as isize)?;
}
if data.more {
w.write_string(b"more")?;
w.write_bool(true)?;
}
if data.stream {
w.write_string(b"stream")?;
w.write_bool(true)?;
}
if data.max_size > 0 {
w.write_string(b"max_size")?;
w.write_int(data.max_size as isize)?;
}
if data.timeout > 0 {
w.write_string(b"timeout")?;
w.write_int(data.timeout as isize)?;
}
if !data.peer_address.is_empty() {
w.write_string(b"peer-address")?;
w.write_string(data.peer_address.as_bytes())?;
w.write_string(b"peer-port")?;
w.write_int(data.peer_port as isize)?;
}
}
RequestPacket::Error(data) => {
w.write_string(b"type")?;
w.write_string(b"error")?;
w.write_string(b"condition")?;
w.write_string(data.condition.as_bytes())?;
}
RequestPacket::Credit(data) => {
w.write_string(b"type")?;
w.write_string(b"credit")?;
w.write_string(b"credits")?;
w.write_int(data.credits as isize)?;
}
RequestPacket::KeepAlive => {
w.write_string(b"type")?;
w.write_string(b"keep-alive")?;
}
RequestPacket::Cancel => {
w.write_string(b"type")?;
w.write_string(b"cancel")?;
}
RequestPacket::HandoffStart => {
w.write_string(b"type")?;
w.write_string(b"handoff-start")?;
}
RequestPacket::HandoffProceed => {
w.write_string(b"type")?;
w.write_string(b"handoff-proceed")?;
}
RequestPacket::Close(data) => {
w.write_string(b"type")?;
w.write_string(b"close")?;
if let Some(status) = data.status {
w.write_string(b"code")?;
w.write_int(status.0 as isize)?;
if !status.1.is_empty() {
w.write_string(b"body")?;
w.write_string(status.1.as_bytes())?;
}
}
}
RequestPacket::Ping(data) => {
w.write_string(b"type")?;
w.write_string(b"ping")?;
if !data.body.is_empty() {
w.write_string(b"body")?;
w.write_string(&data.body)?;
}
}
RequestPacket::Pong(data) => {
w.write_string(b"type")?;
w.write_string(b"pong")?;
if !data.body.is_empty() {
w.write_string(b"body")?;
w.write_string(&data.body)?;
}
}
}
w.end_map()?;
w.flush()?;
Ok((cursor.position() as usize) + 1)
}
}
#[derive(Debug, PartialEq)]
pub enum ParseError {
Unrecognized,
TnetParse(tnetstring::ParseError),
WrongType(&'static str, tnetstring::FrameType),
NotMapOrString(&'static str),
NotUtf8(&'static str),
NegativeInt(&'static str),
TooManyIds,
TooManyHeaders,
InvalidHeader,
NoId,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unrecognized => write!(f, "unrecognized data format"),
Self::TnetParse(e) => e.fmt(f),
Self::WrongType(field, expected) => write!(f, "{} must be of type {}", field, expected),
Self::NotMapOrString(field) => write!(f, "{} must be of type map or string", field),
Self::NotUtf8(field) => write!(f, "{} must be a utf-8 string", field),
Self::NegativeInt(field) => write!(f, "{} must not be negative", field),
Self::TooManyIds => write!(f, "too many ids"),
Self::TooManyHeaders => write!(f, "too many headers"),
Self::InvalidHeader => write!(f, "header item must have size 2"),
Self::NoId => write!(f, "no id"),
}
}
}
impl From<tnetstring::ParseError> for ParseError {
fn from(e: tnetstring::ParseError) -> Self {
Self::TnetParse(e)
}
}
trait ErrorContext<T> {
fn field(self, field: &'static str) -> Result<T, ParseError>;
}
impl<T> ErrorContext<T> for Result<T, tnetstring::ParseError> {
fn field(self, field: &'static str) -> Result<T, ParseError> {
match self {
Ok(v) => Ok(v),
Err(tnetstring::ParseError::WrongType(_, expected)) => {
Err(ParseError::WrongType(field, expected))
}
Err(e) => Err(e.into()),
}
}
}
impl<T> ErrorContext<T> for Result<T, str::Utf8Error> {
fn field(self, field: &'static str) -> Result<T, ParseError> {
match self {
Ok(v) => Ok(v),
Err(_) => Err(ParseError::NotUtf8(field)),
}
}
}
pub struct ResponseScratch<'a> {
pub ids: [Id<'a>; IDS_MAX],
pub headers: [Header<'a>; HEADERS_MAX],
}
impl ResponseScratch<'_> {
pub fn new() -> Self {
Self {
ids: [EMPTY_ID; IDS_MAX],
headers: [EMPTY_HEADER; HEADERS_MAX],
}
}
}
pub struct Response<'buf, 'ids, 'headers> {
pub from: &'buf [u8],
pub ids: &'ids [Id<'buf>],
pub multi: bool,
pub ptype: ResponsePacket<'buf, 'headers>,
pub ptype_str: &'buf str,
}
impl<'buf, 'scratch> Response<'_, '_, '_> {
pub fn parse_ids(
src: &'buf [u8],
scratch: &'scratch mut ResponseScratch<'buf>,
) -> Result<&'scratch [Id<'buf>], ParseError> {
if src.len() < 1 || src[0] != b'T' {
return Err(ParseError::Unrecognized);
}
let root = tnetstring::parse_map(&src[1..]).field("root")?;
let mut ids_pos = 0;
for e in root.clone() {
let e = e?;
match e.key {
"id" => {
match e.ftype {
tnetstring::FrameType::Array => {
for idm in tnetstring::parse_array(e.data)? {
let idm = idm?;
if ids_pos >= scratch.ids.len() {
return Err(ParseError::TooManyIds);
}
for m in tnetstring::parse_map(idm.data)? {
let m = m?;
match m.key {
"id" => {
let s = tnetstring::parse_string(m.data).field("id")?;
scratch.ids[ids_pos].id = s;
}
_ => {}
}
}
ids_pos += 1;
}
}
tnetstring::FrameType::String => {
let s = tnetstring::parse_string(e.data)?;
scratch.ids[0].id = s;
ids_pos = 1;
}
_ => {
return Err(ParseError::NotMapOrString("id"));
}
}
return Ok(&scratch.ids[..ids_pos]);
}
_ => {}
}
}
Ok(&scratch.ids[..ids_pos])
}
pub fn parse(
src: &'buf [u8],
scratch: &'scratch mut ResponseScratch<'buf>,
) -> Result<Response<'buf, 'scratch, 'scratch>, ParseError> {
if src.len() < 1 || src[0] != b'T' {
return Err(ParseError::Unrecognized);
}
let root = tnetstring::parse_map(&src[1..]).field("root")?;
let mut from = EMPTY_BYTES;
let mut ids_pos = 0;
let mut multi = false;
let mut ptype_str = "";
for e in root.clone() {
let e = e?;
match e.key {
"from" => {
from = tnetstring::parse_string(e.data).field("from")?;
}
"id" => {
match e.ftype {
tnetstring::FrameType::Array => {
for idm in tnetstring::parse_array(e.data)? {
let idm = idm?;
if ids_pos >= scratch.ids.len() {
return Err(ParseError::TooManyIds);
}
for m in tnetstring::parse_map(idm.data)? {
let m = m?;
match m.key {
"id" => {
let s = tnetstring::parse_string(m.data).field("id")?;
scratch.ids[ids_pos].id = s;
}
"seq" => {
let x = tnetstring::parse_int(m.data).field("seq")?;
if x < 0 {
return Err(ParseError::NegativeInt("seq"));
}
scratch.ids[ids_pos].seq = Some(x as u32);
}
_ => {}
}
}
ids_pos += 1;
}
}
tnetstring::FrameType::String => {
let s = tnetstring::parse_string(e.data)?;
scratch.ids[0].id = s;
ids_pos = 1;
}
_ => {
return Err(ParseError::NotMapOrString("id"));
}
}
}
"seq" => {
let x = tnetstring::parse_int(e.data).field("seq")?;
if x < 0 {
return Err(ParseError::NegativeInt("seq"));
}
scratch.ids[0].seq = Some(x as u32);
}
"type" => {
let s = tnetstring::parse_string(e.data).field("type")?;
let s = str::from_utf8(s).field("type")?;
ptype_str = s;
}
"ext" => {
let ext = tnetstring::parse_map(e.data).field("ext")?;
for m in ext {
let m = m?;
match m.key {
"multi" => {
let b = tnetstring::parse_bool(m.data).field("multi")?;
multi = b;
}
_ => {}
}
}
}
_ => {}
}
}
let ptype = match ptype_str {
"" => {
let mut credits = 0;
let mut more = false;
let mut code = 0;
let mut reason = "";
let mut headers_pos = 0;
let mut content_type = None;
let mut body = EMPTY_BYTES;
for e in root {
let e = e?;
match e.key {
"credits" => {
let x = tnetstring::parse_int(e.data).field("credits")?;
if x < 0 {
return Err(ParseError::NegativeInt("credits"));
}
credits = x as u32;
}
"more" => {
let b = tnetstring::parse_bool(e.data).field("more")?;
more = b;
}
"code" => {
let x = tnetstring::parse_int(e.data).field("code")?;
if x < 0 {
return Err(ParseError::NegativeInt("code"));
}
code = x as u32;
}
"reason" => {
let s = tnetstring::parse_string(e.data).field("reason")?;
let s = str::from_utf8(s).field("reason")?;
reason = s;
}
"headers" => {
let headers = tnetstring::parse_array(e.data).field("headers")?;
for ha in headers {
let ha = ha?;
if headers_pos >= scratch.headers.len() {
return Err(ParseError::TooManyHeaders);
}
let mut hi =
tnetstring::parse_array(ha.data).field("header item")?;
let name = match hi.next() {
Some(Ok(name)) => name,
Some(Err(e)) => {
return Err(e.into());
}
None => {
return Err(ParseError::InvalidHeader);
}
};
let name =
tnetstring::parse_string(name.data).field("header name")?;
let name = str::from_utf8(name).field("header name")?;
let value = match hi.next() {
Some(Ok(name)) => name,
Some(Err(e)) => {
return Err(e.into());
}
None => {
return Err(ParseError::InvalidHeader);
}
};
let value =
tnetstring::parse_string(value.data).field("header value")?;
scratch.headers[headers_pos] = Header { name, value };
headers_pos += 1;
}
}
"content-type" => {
let s = tnetstring::parse_string(e.data).field("content-type")?;
content_type = Some(match s {
b"binary" => ContentType::Binary,
_ => ContentType::Text,
});
}
"body" => {
let s = tnetstring::parse_string(e.data).field("body")?;
body = s;
}
_ => {}
}
}
ResponsePacket::Data(ResponseData {
credits,
more,
code,
reason,
headers: &scratch.headers[..headers_pos],
content_type,
body,
})
}
"error" => {
let mut condition = "";
let mut code = 0;
let mut reason = "";
let mut headers_pos = 0;
let mut body = EMPTY_BYTES;
for e in root {
let e = e?;
match e.key {
"condition" => {
let s = tnetstring::parse_string(e.data).field("condition")?;
let s = str::from_utf8(s).field("condition")?;
condition = s;
}
"code" => {
let x = tnetstring::parse_int(e.data).field("code")?;
if x < 0 {
return Err(ParseError::NegativeInt("code"));
}
code = x as u32;
}
"reason" => {
let s = tnetstring::parse_string(e.data).field("reason")?;
let s = str::from_utf8(s).field("reason")?;
reason = s;
}
"headers" => {
let headers = tnetstring::parse_array(e.data).field("headers")?;
for ha in headers {
let ha = ha?;
if headers_pos >= scratch.headers.len() {
return Err(ParseError::TooManyHeaders);
}
let mut hi =
tnetstring::parse_array(ha.data).field("header item")?;
let name = match hi.next() {
Some(Ok(name)) => name,
Some(Err(e)) => {
return Err(e.into());
}
None => {
return Err(ParseError::InvalidHeader);
}
};
let name =
tnetstring::parse_string(name.data).field("header name")?;
let name = str::from_utf8(name).field("header name")?;
let value = match hi.next() {
Some(Ok(name)) => name,
Some(Err(e)) => {
return Err(e.into());
}
None => {
return Err(ParseError::InvalidHeader);
}
};
let value =
tnetstring::parse_string(value.data).field("header value")?;
scratch.headers[headers_pos] = Header { name, value };
headers_pos += 1;
}
}
"body" => {
let s = tnetstring::parse_string(e.data).field("body")?;
body = s;
}
_ => {}
}
}
let rejected_info = if condition == "rejected" {
Some(RejectedInfo {
code,
reason,
headers: &scratch.headers[..headers_pos],
body,
})
} else {
None
};
ResponsePacket::Error(ErrorData {
condition,
rejected_info,
})
}
"credit" => {
let mut credits = 0;
for e in root {
let e = e?;
match e.key {
"credits" => {
let x = tnetstring::parse_int(e.data).field("credits")?;
if x < 0 {
return Err(ParseError::NegativeInt("credits"));
}
credits = x as u32;
}
_ => {}
}
}
ResponsePacket::Credit(CreditData { credits })
}
"keep-alive" => ResponsePacket::KeepAlive,
"cancel" => ResponsePacket::Cancel,
"handoff-start" => ResponsePacket::HandoffStart,
"handoff-proceed" => ResponsePacket::HandoffProceed,
"close" => {
let mut code = None;
let mut reason = "";
for e in root {
let e = e?;
match e.key {
"code" => {
let x = tnetstring::parse_int(e.data).field("code")?;
if x < 0 {
return Err(ParseError::NegativeInt("code"));
}
code = Some(x as u16);
}
"body" => {
let s = tnetstring::parse_string(e.data).field("body")?;
let s = str::from_utf8(s).field("condition")?;
reason = s;
}
_ => {}
}
}
if let Some(code) = code {
ResponsePacket::Close(CloseData {
status: Some((code, reason)),
})
} else {
ResponsePacket::Close(CloseData { status: None })
}
}
"ping" => ResponsePacket::Ping(PingData { body: &b""[..] }),
"pong" => ResponsePacket::Pong(PongData { body: &b""[..] }),
_ => ResponsePacket::Unknown,
};
Ok(Response {
from,
ids: &scratch.ids[..ids_pos],
multi,
ptype,
ptype_str,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize() {
struct Test {
name: &'static str,
req: Request<'static, 'static, 'static>,
expected: &'static str,
}
let tests = [
Test {
name: "data",
req: Request {
from: b"client",
ids: &[Id {
id: b"1",
seq: Some(0),
}],
multi: false,
ptype: RequestPacket::Data(RequestData {
credits: 0,
more: true,
stream: false,
max_size: 0,
timeout: 0,
method: "POST",
uri: "http://example.com/path",
headers: &[Header {
name: "Content-Type",
value: b"text/plain",
}],
content_type: None,
body: b"hello",
peer_address: "",
peer_port: 0,
}),
},
expected: concat!(
"T161:4:from,6:client,2:id,1:1,3:seq,1:0#6:method,4:POST,3:uri",
",23:http://example.com/path,7:headers,34:30:12:Content-Type,1",
"0:text/plain,]]4:body,5:hello,4:more,4:true!}",
),
},
Test {
name: "error",
req: Request {
from: b"client",
ids: &[Id {
id: b"1",
seq: Some(0),
}],
multi: false,
ptype: RequestPacket::Error(ErrorData {
condition: "bad-request",
rejected_info: None,
}),
},
expected: concat!(
"T77:4:from,6:client,2:id,1:1,3:seq,1:0#4:type,5:error,9:condi",
"tion,11:bad-request,}",
),
},
];
for test in tests.iter() {
let mut data = [0; 1024];
let size = test.req.serialize(&mut data).unwrap();
assert_eq!(
str::from_utf8(&data[..size]).unwrap(),
test.expected,
"test={}",
test.name
);
}
}
#[test]
fn test_parse() {
let data = concat!(
"T208:4:more,4:true!7:headers,34:30:12:Content-Type,10:text/pl",
"ain,]]12:content-type,6:binary,4:from,6:server,2:id,1:1,6:rea",
"son,2:OK,7:credits,3:100#9:user-data,12:3:foo,3:bar,}3:seq,1:",
"0#4:code,3:200#4:body,5:hello,}"
)
.as_bytes();
let mut scratch = ResponseScratch::new();
let resp = Response::parse(&data, &mut scratch).unwrap();
assert_eq!(resp.from, b"server");
assert_eq!(resp.ids.len(), 1);
assert_eq!(resp.ids[0].id, b"1");
assert_eq!(resp.ids[0].seq, Some(0));
let rdata = match resp.ptype {
ResponsePacket::Data(data) => data,
_ => panic!("expected data packet"),
};
assert_eq!(rdata.credits, 100);
assert_eq!(rdata.more, true);
assert_eq!(rdata.code, 200);
assert_eq!(rdata.reason, "OK");
assert_eq!(rdata.headers.len(), 1);
assert_eq!(rdata.headers[0].name, "Content-Type");
assert_eq!(rdata.headers[0].value, b"text/plain");
assert_eq!(rdata.body, b"hello");
let ctype = rdata.content_type.unwrap();
assert_eq!(ctype, ContentType::Binary);
}
}