refactor(headers): errors for parse_header

Header::parse_header() returns now a hyper Result instead of an option
this will enable more precise Error messages in the future, currently
most failures are reported as ::Error::Header.

BREAKING CHANGE: parse_header returns Result instead of Option, related
code did also change
This commit is contained in:
Pyfisch
2015-06-06 22:04:01 +02:00
parent 763746153e
commit 195a89fa91
19 changed files with 140 additions and 132 deletions

View File

@@ -58,8 +58,8 @@ impl hyper::header::Header for Foo {
fn header_name() -> &'static str {
"x-foo"
}
fn parse_header(_: &[Vec<u8>]) -> Option<Foo> {
None
fn parse_header(_: &[Vec<u8>]) -> hyper::Result<Foo> {
Err(hyper::Error::Header)
}
}
@@ -104,4 +104,3 @@ fn bench_mock_hyper(b: &mut test::Bencher) {
.read_to_string(&mut s).unwrap()
});
}

View File

@@ -2,6 +2,7 @@
use std::error::Error as StdError;
use std::fmt;
use std::io::Error as IoError;
use std::str::Utf8Error;
use httparse;
use openssl::ssl::error::SslError;
@@ -18,6 +19,7 @@ use self::Error::{
Ssl,
TooLarge,
Http2,
Utf8
};
@@ -45,6 +47,8 @@ pub enum Error {
Ssl(SslError),
/// An HTTP/2-specific error, coming from the `solicit` library.
Http2(Http2Error),
/// Parsing a field as string failed
Utf8(Utf8Error),
#[doc(hidden)]
__Nonexhaustive(Void)
@@ -77,6 +81,7 @@ impl StdError for Error {
Io(ref e) => e.description(),
Ssl(ref e) => e.description(),
Http2(ref e) => e.description(),
Utf8(ref e) => e.description(),
Error::__Nonexhaustive(ref void) => match *void {}
}
}
@@ -113,6 +118,12 @@ impl From<SslError> for Error {
}
}
impl From<Utf8Error> for Error {
fn from(err: Utf8Error) -> Error {
Utf8(err)
}
}
impl From<httparse::Error> for Error {
fn from(err: httparse::Error) -> Error {
match err {

View File

@@ -52,8 +52,8 @@ pub enum RangeUnit {
impl FromStr for RangeUnit {
type Err = ();
fn from_str(s: &str) -> Result<Self, ()> {
type Err = ::Error;
fn from_str(s: &str) -> ::Result<Self> {
match s {
"bytes" => Ok(RangeUnit::Bytes),
"none" => Ok(RangeUnit::None),

View File

@@ -35,16 +35,14 @@ impl Header for AccessControlAllowOrigin {
"Access-Control-Allow-Origin"
}
fn parse_header(raw: &[Vec<u8>]) -> Option<AccessControlAllowOrigin> {
fn parse_header(raw: &[Vec<u8>]) -> ::Result<AccessControlAllowOrigin> {
if raw.len() == 1 {
match unsafe { &raw.get_unchecked(0)[..] } {
b"*" => Some(AccessControlAllowOrigin::Any),
b"null" => Some(AccessControlAllowOrigin::Null),
r => if let Ok(s) = str::from_utf8(r) {
Url::parse(s).ok().map(AccessControlAllowOrigin::Value)
} else { None }
b"*" => Ok(AccessControlAllowOrigin::Any),
b"null" => Ok(AccessControlAllowOrigin::Null),
r => Ok(AccessControlAllowOrigin::Value(try!(Url::parse(try!(str::from_utf8(r))))))
}
} else { None }
} else { Err(::Error::Header) }
}
}

View File

@@ -42,18 +42,25 @@ impl<S: Scheme + Any> Header for Authorization<S> where <S as FromStr>::Err: 'st
"Authorization"
}
fn parse_header(raw: &[Vec<u8>]) -> Option<Authorization<S>> {
if raw.len() == 1 {
match (from_utf8(unsafe { &raw.get_unchecked(0)[..] }), <S as Scheme>::scheme()) {
(Ok(header), Some(scheme))
if header.starts_with(scheme) && header.len() > scheme.len() + 1 => {
header[scheme.len() + 1..].parse::<S>().map(Authorization).ok()
},
(Ok(header), None) => header.parse::<S>().map(Authorization).ok(),
_ => None
fn parse_header(raw: &[Vec<u8>]) -> ::Result<Authorization<S>> {
if raw.len() != 1 {
return Err(::Error::Header);
}
let header = try!(from_utf8(unsafe { &raw.get_unchecked(0)[..] }));
return if let Some(scheme) = <S as Scheme>::scheme() {
if header.starts_with(scheme) && header.len() > scheme.len() + 1 {
match header[scheme.len() + 1..].parse::<S>().map(Authorization) {
Ok(h) => Ok(h),
Err(_) => Err(::Error::Header)
}
} else {
Err(::Error::Header)
}
} else {
None
match header.parse::<S>().map(Authorization) {
Ok(h) => Ok(h),
Err(_) => Err(::Error::Header)
}
}
}
}
@@ -121,15 +128,15 @@ impl Scheme for Basic {
}
impl FromStr for Basic {
type Err = ();
fn from_str(s: &str) -> Result<Basic, ()> {
type Err = ::Error;
fn from_str(s: &str) -> ::Result<Basic> {
match s.from_base64() {
Ok(decoded) => match String::from_utf8(decoded) {
Ok(text) => {
let mut parts = &mut text.split(':');
let user = match parts.next() {
Some(part) => part.to_owned(),
None => return Err(())
None => return Err(::Error::Header)
};
let password = match parts.next() {
Some(part) => Some(part.to_owned()),
@@ -142,12 +149,12 @@ impl FromStr for Basic {
},
Err(e) => {
debug!("Basic::from_utf8 error={:?}", e);
Err(())
Err(::Error::Header)
}
},
Err(e) => {
debug!("Basic::from_base64 error={:?}", e);
Err(())
Err(::Error::Header)
}
}
}

View File

@@ -30,15 +30,15 @@ impl Header for CacheControl {
"Cache-Control"
}
fn parse_header(raw: &[Vec<u8>]) -> Option<CacheControl> {
fn parse_header(raw: &[Vec<u8>]) -> ::Result<CacheControl> {
let directives = raw.iter()
.filter_map(|line| from_one_comma_delimited(&line[..]))
.filter_map(|line| from_one_comma_delimited(&line[..]).ok())
.collect::<Vec<Vec<CacheDirective>>>()
.concat();
if !directives.is_empty() {
Some(CacheControl(directives))
Ok(CacheControl(directives))
} else {
None
Err(::Error::Header)
}
}
}
@@ -148,35 +148,35 @@ mod tests {
#[test]
fn test_parse_multiple_headers() {
let cache = Header::parse_header(&[b"no-cache".to_vec(), b"private".to_vec()]);
assert_eq!(cache, Some(CacheControl(vec![CacheDirective::NoCache,
assert_eq!(cache.ok(), Some(CacheControl(vec![CacheDirective::NoCache,
CacheDirective::Private])))
}
#[test]
fn test_parse_argument() {
let cache = Header::parse_header(&[b"max-age=100, private".to_vec()]);
assert_eq!(cache, Some(CacheControl(vec![CacheDirective::MaxAge(100),
assert_eq!(cache.ok(), Some(CacheControl(vec![CacheDirective::MaxAge(100),
CacheDirective::Private])))
}
#[test]
fn test_parse_quote_form() {
let cache = Header::parse_header(&[b"max-age=\"200\"".to_vec()]);
assert_eq!(cache, Some(CacheControl(vec![CacheDirective::MaxAge(200)])))
assert_eq!(cache.ok(), Some(CacheControl(vec![CacheDirective::MaxAge(200)])))
}
#[test]
fn test_parse_extension() {
let cache = Header::parse_header(&[b"foo, bar=baz".to_vec()]);
assert_eq!(cache, Some(CacheControl(vec![
assert_eq!(cache.ok(), Some(CacheControl(vec![
CacheDirective::Extension("foo".to_owned(), None),
CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned()))])))
}
#[test]
fn test_parse_bad_syntax() {
let cache: Option<CacheControl> = Header::parse_header(&[b"foo=".to_vec()]);
assert_eq!(cache, None)
let cache: ::Result<CacheControl> = Header::parse_header(&[b"foo=".to_vec()]);
assert_eq!(cache.ok(), None)
}
}

View File

@@ -27,26 +27,23 @@ impl Header for Cookie {
"Cookie"
}
fn parse_header(raw: &[Vec<u8>]) -> Option<Cookie> {
fn parse_header(raw: &[Vec<u8>]) -> ::Result<Cookie> {
let mut cookies = Vec::with_capacity(raw.len());
for cookies_raw in raw.iter() {
match from_utf8(&cookies_raw[..]) {
Ok(cookies_str) => {
for cookie_str in cookies_str.split(';') {
match cookie_str.trim().parse() {
Ok(cookie) => cookies.push(cookie),
Err(_) => return None
}
}
},
Err(_) => return None
};
let cookies_str = try!(from_utf8(&cookies_raw[..]));
for cookie_str in cookies_str.split(';') {
if let Ok(cookie) = cookie_str.trim().parse() {
cookies.push(cookie);
} else {
return Err(::Error::Header);
}
}
}
if !cookies.is_empty() {
Some(Cookie(cookies))
Ok(Cookie(cookies))
} else {
None
Err(::Error::Header)
}
}
}
@@ -88,7 +85,7 @@ fn test_parse() {
let h = Header::parse_header(&[b"foo=bar; baz=quux".to_vec()][..]);
let c1 = CookiePair::new("foo".to_owned(), "bar".to_owned());
let c2 = CookiePair::new("baz".to_owned(), "quux".to_owned());
assert_eq!(h, Some(Cookie(vec![c1, c2])));
assert_eq!(h.ok(), Some(Cookie(vec![c1, c2])));
}
#[test]

View File

@@ -26,7 +26,7 @@ impl Header for Expect {
"Expect"
}
fn parse_header(raw: &[Vec<u8>]) -> Option<Expect> {
fn parse_header(raw: &[Vec<u8>]) -> ::Result<Expect> {
if raw.len() == 1 {
let text = unsafe {
// safe because:
@@ -38,12 +38,12 @@ impl Header for Expect {
str::from_utf8_unchecked(raw.get_unchecked(0))
};
if UniCase(text) == EXPECT_CONTINUE {
Some(Expect::Continue)
Ok(Expect::Continue)
} else {
None
Err(::Error::Header)
}
} else {
None
Err(::Error::Header)
}
}
}

View File

@@ -22,7 +22,7 @@ impl Header for Host {
"Host"
}
fn parse_header(raw: &[Vec<u8>]) -> Option<Host> {
fn parse_header(raw: &[Vec<u8>]) -> ::Result<Host> {
from_one_raw_str(raw).and_then(|mut s: String| {
// FIXME: use rust-url to parse this
// https://github.com/servo/rust-url/issues/42
@@ -39,7 +39,7 @@ impl Header for Host {
None
}
}
None => return None // this is a bad ipv6 address...
None => return Err(::Error::Header) // this is a bad ipv6 address...
}
} else {
slice.rfind(':')
@@ -56,7 +56,7 @@ impl Header for Host {
None => ()
}
Some(Host {
Ok(Host {
hostname: s,
port: port
})
@@ -82,14 +82,14 @@ mod tests {
#[test]
fn test_host() {
let host = Header::parse_header([b"foo.com".to_vec()].as_ref());
assert_eq!(host, Some(Host {
assert_eq!(host.ok(), Some(Host {
hostname: "foo.com".to_owned(),
port: None
}));
let host = Header::parse_header([b"foo.com:8080".to_vec()].as_ref());
assert_eq!(host, Some(Host {
assert_eq!(host.ok(), Some(Host {
hostname: "foo.com".to_owned(),
port: Some(8080)
}));

View File

@@ -45,10 +45,10 @@ mod tests {
#[test]
fn test_if_none_match() {
let mut if_none_match: Option<IfNoneMatch>;
let mut if_none_match: ::Result<IfNoneMatch>;
if_none_match = Header::parse_header([b"*".to_vec()].as_ref());
assert_eq!(if_none_match, Some(IfNoneMatch::Any));
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any));
if_none_match = Header::parse_header([b"\"foobar\", W/\"weak-etag\"".to_vec()].as_ref());
let mut entities: Vec<EntityTag> = Vec::new();
@@ -56,7 +56,7 @@ mod tests {
let weak_etag = EntityTag::new(true, "weak-etag".to_owned());
entities.push(foobar_etag);
entities.push(weak_etag);
assert_eq!(if_none_match, Some(IfNoneMatch::Items(entities)));
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities)));
}
}

View File

@@ -36,16 +36,16 @@ impl Header for IfRange {
fn header_name() -> &'static str {
"If-Range"
}
fn parse_header(raw: &[Vec<u8>]) -> Option<IfRange> {
let etag: Option<EntityTag> = header::parsing::from_one_raw_str(raw);
if etag != None {
return Some(IfRange::EntityTag(etag.unwrap()));
fn parse_header(raw: &[Vec<u8>]) -> ::Result<IfRange> {
let etag: ::Result<EntityTag> = header::parsing::from_one_raw_str(raw);
if etag.is_ok() {
return Ok(IfRange::EntityTag(etag.unwrap()));
}
let date: Option<HttpDate> = header::parsing::from_one_raw_str(raw);
if date != None {
return Some(IfRange::Date(date.unwrap()));
let date: ::Result<HttpDate> = header::parsing::from_one_raw_str(raw);
if date.is_ok() {
return Ok(IfRange::Date(date.unwrap()));
}
None
Err(::Error::Header)
}
}

View File

@@ -144,7 +144,7 @@ macro_rules! test_header {
let a: Vec<Vec<u8>> = $raw.iter().map(|x| x.to_vec()).collect();
let val = HeaderField::parse_header(&a[..]);
// Test parsing
assert_eq!(val, $typed);
assert_eq!(val.ok(), $typed);
// Test formatting
if $typed != None {
let res: &str = str::from_utf8($raw[0]).unwrap();
@@ -171,7 +171,7 @@ macro_rules! header {
fn header_name() -> &'static str {
$n
}
fn parse_header(raw: &[Vec<u8>]) -> Option<Self> {
fn parse_header(raw: &[Vec<u8>]) -> $crate::Result<Self> {
$crate::header::parsing::from_comma_delimited(raw).map($id)
}
}
@@ -197,7 +197,7 @@ macro_rules! header {
fn header_name() -> &'static str {
$n
}
fn parse_header(raw: &[Vec<u8>]) -> Option<Self> {
fn parse_header(raw: &[Vec<u8>]) -> $crate::Result<Self> {
$crate::header::parsing::from_comma_delimited(raw).map($id)
}
}
@@ -223,7 +223,7 @@ macro_rules! header {
fn header_name() -> &'static str {
$n
}
fn parse_header(raw: &[Vec<u8>]) -> Option<Self> {
fn parse_header(raw: &[Vec<u8>]) -> $crate::Result<Self> {
$crate::header::parsing::from_one_raw_str(raw).map($id)
}
}
@@ -252,11 +252,11 @@ macro_rules! header {
fn header_name() -> &'static str {
$n
}
fn parse_header(raw: &[Vec<u8>]) -> Option<Self> {
fn parse_header(raw: &[Vec<u8>]) -> $crate::Result<Self> {
// FIXME: Return None if no item is in $id::Only
if raw.len() == 1 {
if raw[0] == b"*" {
return Some($id::Any)
return Ok($id::Any)
}
}
$crate::header::parsing::from_comma_delimited(raw).map(|vec| $id::Items(vec))

View File

@@ -29,12 +29,12 @@ impl Header for Pragma {
"Pragma"
}
fn parse_header(raw: &[Vec<u8>]) -> Option<Pragma> {
fn parse_header(raw: &[Vec<u8>]) -> ::Result<Pragma> {
parsing::from_one_raw_str(raw).and_then(|s: String| {
let slice = &s.to_ascii_lowercase()[..];
match slice {
"no-cache" => Some(Pragma::NoCache),
_ => Some(Pragma::Ext(s)),
"no-cache" => Ok(Pragma::NoCache),
_ => Ok(Pragma::Ext(s)),
}
})
}
@@ -57,6 +57,6 @@ fn test_parse_header() {
let c: Pragma = Header::parse_header([b"FoObar".to_vec()].as_ref()).unwrap();
let d = Pragma::Ext("FoObar".to_owned());
assert_eq!(c, d);
let e: Option<Pragma> = Header::parse_header([b"".to_vec()].as_ref());
assert_eq!(e, None);
let e: ::Result<Pragma> = Header::parse_header([b"".to_vec()].as_ref());
assert_eq!(e.ok(), None);
}

View File

@@ -64,7 +64,7 @@ impl Header for SetCookie {
"Set-Cookie"
}
fn parse_header(raw: &[Vec<u8>]) -> Option<SetCookie> {
fn parse_header(raw: &[Vec<u8>]) -> ::Result<SetCookie> {
let mut set_cookies = Vec::with_capacity(raw.len());
for set_cookies_raw in raw {
if let Ok(s) = from_utf8(&set_cookies_raw[..]) {
@@ -75,9 +75,9 @@ impl Header for SetCookie {
}
if !set_cookies.is_empty() {
Some(SetCookie(set_cookies))
Ok(SetCookie(set_cookies))
} else {
None
Err(::Error::Header)
}
}
@@ -120,7 +120,7 @@ fn test_parse() {
let mut c1 = Cookie::new("foo".to_owned(), "bar".to_owned());
c1.httponly = true;
assert_eq!(h, Some(SetCookie(vec![c1])));
assert_eq!(h.ok(), Some(SetCookie(vec![c1])));
}
#[test]

View File

@@ -45,8 +45,8 @@ header! {
Some(Upgrade(vec![Protocol::new(ProtocolName::WebSocket, None)])));
#[test]
fn test3() {
let x: Option<Upgrade> = Header::parse_header(&[b"WEbSOCKet".to_vec()]);
assert_eq!(x, Some(Upgrade(vec![Protocol::new(ProtocolName::WebSocket, None)])));
let x: ::Result<Upgrade> = Header::parse_header(&[b"WEbSOCKet".to_vec()]);
assert_eq!(x.ok(), Some(Upgrade(vec![Protocol::new(ProtocolName::WebSocket, None)])));
}
}
}

View File

@@ -24,15 +24,15 @@ header! {
#[test]
fn test2() {
let mut vary: Option<Vary>;
let mut vary: ::Result<Vary>;
vary = Header::parse_header([b"*".to_vec()].as_ref());
assert_eq!(vary, Some(Vary::Any));
assert_eq!(vary.ok(), Some(Vary::Any));
vary = Header::parse_header([b"etag,cookie,allow".to_vec()].as_ref());
assert_eq!(vary, Some(Vary::Items(vec!["eTag".parse().unwrap(),
"cookIE".parse().unwrap(),
"AlLOw".parse().unwrap(),])));
assert_eq!(vary.ok(), Some(Vary::Items(vec!["eTag".parse().unwrap(),
"cookIE".parse().unwrap(),
"AlLOw".parse().unwrap(),])));
}
}
}

View File

@@ -60,11 +60,11 @@ impl Item {
Some(val) => Some(val),
None => {
match parse::<H>(self.raw.as_ref().expect("item.raw must exist")) {
Some(typed) => {
Ok(typed) => {
unsafe { self.typed.insert(tid, typed); }
self.typed.get(tid)
},
None => None
Err(_) => None
}
}
}.map(|typed| unsafe { typed.downcast_ref_unchecked() })
@@ -74,10 +74,10 @@ impl Item {
let tid = TypeId::of::<H>();
if self.typed.get_mut(tid).is_none() {
match parse::<H>(self.raw.as_ref().expect("item.raw must exist")) {
Some(typed) => {
Ok(typed) => {
unsafe { self.typed.insert(tid, typed); }
},
None => ()
Err(_) => ()
}
}
self.typed.get_mut(tid).map(|typed| unsafe { typed.downcast_mut_unchecked() })
@@ -85,7 +85,7 @@ impl Item {
}
#[inline]
fn parse<H: Header + HeaderFormat>(raw: &Vec<Vec<u8>>) -> Option<Box<HeaderFormat + Send + Sync>> {
fn parse<H: Header + HeaderFormat>(raw: &Vec<Vec<u8>>) -> ::Result<Box<HeaderFormat + Send + Sync>> {
Header::parse_header(&raw[..]).map(|h: H| {
// FIXME: Use Type ascription
let h: Box<HeaderFormat + Send + Sync> = Box::new(h);

View File

@@ -44,7 +44,7 @@ pub trait Header: Clone + Any + Send + Sync {
/// it's not necessarily the case that a Header is *allowed* to have more
/// than one field value. If that's the case, you **should** return `None`
/// if `raw.len() > 1`.
fn parse_header(raw: &[Vec<u8>]) -> Option<Self>;
fn parse_header(raw: &[Vec<u8>]) -> ::Result<Self>;
}
@@ -420,7 +420,7 @@ mod tests {
#[test]
fn test_content_type() {
let content_type = Header::parse_header([b"text/plain".to_vec()].as_ref());
assert_eq!(content_type, Some(ContentType(Mime(Text, Plain, vec![]))));
assert_eq!(content_type.ok(), Some(ContentType(Mime(Text, Plain, vec![]))));
}
#[test]
@@ -429,10 +429,10 @@ mod tests {
let application_vendor = "application/vnd.github.v3.full+json; q=0.5".parse().unwrap();
let accept = Header::parse_header([b"text/plain".to_vec()].as_ref());
assert_eq!(accept, Some(Accept(vec![text_plain.clone()])));
assert_eq!(accept.ok(), Some(Accept(vec![text_plain.clone()])));
let accept = Header::parse_header([b"application/vnd.github.v3.full+json; q=0.5, text/plain".to_vec()].as_ref());
assert_eq!(accept, Some(Accept(vec![application_vendor, text_plain])));
assert_eq!(accept.ok(), Some(Accept(vec![application_vendor, text_plain])));
}
#[derive(Clone, PartialEq, Debug)]
@@ -442,18 +442,21 @@ mod tests {
fn header_name() -> &'static str {
"content-length"
}
fn parse_header(raw: &[Vec<u8>]) -> Option<CrazyLength> {
fn parse_header(raw: &[Vec<u8>]) -> ::Result<CrazyLength> {
use std::str::from_utf8;
use std::str::FromStr;
if raw.len() != 1 {
return None;
return Err(::Error::Header);
}
// we JUST checked that raw.len() == 1, so raw[0] WILL exist.
match from_utf8(unsafe { &raw.get_unchecked(0)[..] }) {
match match from_utf8(unsafe { &raw.get_unchecked(0)[..] }) {
Ok(s) => FromStr::from_str(s).ok(),
Err(_) => None
}.map(|u| CrazyLength(Some(false), u))
}.map(|u| CrazyLength(Some(false), u)) {
Some(x) => Ok(x),
None => Err(::Error::Header),
}
}
}

View File

@@ -4,44 +4,37 @@ use std::str;
use std::fmt::{self, Display};
/// Reads a single raw string when parsing a header
pub fn from_one_raw_str<T: str::FromStr>(raw: &[Vec<u8>]) -> Option<T> {
if raw.len() != 1 {
return None;
}
pub fn from_one_raw_str<T: str::FromStr>(raw: &[Vec<u8>]) -> ::Result<T> {
if raw.len() != 1 || unsafe { raw.get_unchecked(0) } == b"" { return Err(::Error::Header) }
// we JUST checked that raw.len() == 1, so raw[0] WILL exist.
if let Ok(s) = str::from_utf8(& unsafe { raw.get_unchecked(0) }[..]) {
if s != "" {
return str::FromStr::from_str(s).ok();
}
let s: &str = try!(str::from_utf8(& unsafe { raw.get_unchecked(0) }[..]));
if let Ok(x) = str::FromStr::from_str(s) {
Ok(x)
} else {
Err(::Error::Header)
}
None
}
/// Reads a comma-delimited raw header into a Vec.
#[inline]
pub fn from_comma_delimited<T: str::FromStr>(raw: &[Vec<u8>]) -> Option<Vec<T>> {
pub fn from_comma_delimited<T: str::FromStr>(raw: &[Vec<u8>]) -> ::Result<Vec<T>> {
if raw.len() != 1 {
return None;
return Err(::Error::Header);
}
// we JUST checked that raw.len() == 1, so raw[0] WILL exist.
from_one_comma_delimited(& unsafe { raw.get_unchecked(0) }[..])
}
/// Reads a comma-delimited raw string into a Vec.
pub fn from_one_comma_delimited<T: str::FromStr>(raw: &[u8]) -> Option<Vec<T>> {
match str::from_utf8(raw) {
Ok(s) => {
Some(s
.split(',')
.filter_map(|x| match x.trim() {
"" => None,
y => Some(y)
})
.filter_map(|x| x.parse().ok())
.collect())
}
Err(_) => None
}
pub fn from_one_comma_delimited<T: str::FromStr>(raw: &[u8]) -> ::Result<Vec<T>> {
let s = try!(str::from_utf8(raw));
Ok(s.split(',')
.filter_map(|x| match x.trim() {
"" => None,
y => Some(y)
})
.filter_map(|x| x.parse().ok())
.collect())
}
/// Format an array into a comma-delimited string.