From 342882565088c5ec958e49e13b7a21ac2611502b Mon Sep 17 00:00:00 2001 From: Carl Lerche Date: Thu, 16 Mar 2017 15:52:49 -0700 Subject: [PATCH 1/6] Initial implementation of Method --- Cargo.toml | 1 + src/method.rs | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/str.rs | 57 +++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 src/method.rs create mode 100644 src/str.rs diff --git a/Cargo.toml b/Cargo.toml index fb470d23..769cefd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ A set of types for representing HTTP requests and responses. """ [dependencies] +bytes = "0.4" diff --git a/src/method.rs b/src/method.rs new file mode 100644 index 00000000..e8180911 --- /dev/null +++ b/src/method.rs @@ -0,0 +1,152 @@ +//! The HTTP request method + +use self::Inner::*; +use str::Str; + +use std::fmt; +use std::convert::AsRef; + +/// The Request Method (VERB) +/// +/// Currently includes 8 variants representing the 8 methods defined in +/// [RFC 7230](https://tools.ietf.org/html/rfc7231#section-4.1), plus PATCH, +/// and an Extension variant for all extensions. +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct Method(Inner); + +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +enum Inner { + Options, + Get, + Post, + Put, + Delete, + Head, + Trace, + Connect, + Patch, + Extension(Str) +} + +/// GET +pub const GET: Method = Method(Get); + +/// POST +pub const POST: Method = Method(Post); + +/// PUT +pub const PUT: Method = Method(Put); + +/// DELETE +pub const DELETE: Method = Method(Delete); + +/// HEAD +pub const HEAD: Method = Method(Head); + +/// OPTIONS +pub const OPTIONS: Method = Method(Options); + +/// CONNECT +pub const CONNECT: Method = Method(Connect); + +/// PATCH +pub const PATCH: Method = Method(Patch); + +/// TRACE +pub const TRACE: Method = Method(Trace); + +impl Method { + /// Return a `Method` instance representing a non-standard HTTP method. + /// + /// # Examples + /// + /// ``` + /// use http::Method; + /// + /// let method = Method::extension("FOO"); + /// assert_eq!(method, "FOO"); + /// ``` + pub fn extension(s: &str) -> Method { + Method(Extension(s.into())) + } + + /// Whether a method is considered "safe", meaning the request is + /// essentially read-only. + /// + /// See [the spec](https://tools.ietf.org/html/rfc7231#section-4.2.1) + /// for more words. + pub fn is_safe(&self) -> bool { + match self.0 { + Get | Head | Options | Trace => true, + _ => false + } + } + + /// Whether a method is considered "idempotent", meaning the request has + /// the same result if executed multiple times. + /// + /// See [the spec](https://tools.ietf.org/html/rfc7231#section-4.2.2) for + /// more words. + pub fn is_idempotent(&self) -> bool { + if self.is_safe() { + true + } else { + match self.0 { + Put | Delete => true, + _ => false + } + } + } +} + +impl AsRef for Method { + fn as_ref(&self) -> &str { + match self.0 { + Options => "OPTIONS", + Get => "GET", + Post => "POST", + Put => "PUT", + Delete => "DELETE", + Head => "HEAD", + Trace => "TRACE", + Connect => "CONNECT", + Patch => "PATCH", + Extension(ref s) => s.as_ref() + } + } +} + +impl PartialEq for Method { + fn eq(&self, other: &str) -> bool { + self.as_ref() == other + } +} + +impl<'a> PartialEq<&'a str> for Method { + fn eq(&self, other: &&'a str) -> bool { + self.as_ref() == *other + } +} + +impl fmt::Display for Method { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str(match self.0 { + Options => "OPTIONS", + Get => "GET", + Post => "POST", + Put => "PUT", + Delete => "DELETE", + Head => "HEAD", + Trace => "TRACE", + Connect => "CONNECT", + Patch => "PATCH", + Extension(ref s) => s.as_ref() + }) + } +} + +impl Default for Method { + fn default() -> Method { + GET + } +} diff --git a/src/str.rs b/src/str.rs new file mode 100644 index 00000000..ef901449 --- /dev/null +++ b/src/str.rs @@ -0,0 +1,57 @@ +use bytes::Bytes; + +use std::{ops, str}; + +// TODO: Move this into `bytes` +#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] +pub struct Str { + bytes: Bytes, +} + +impl Str { + /* + pub fn new() -> Str { + Str { bytes: Bytes::new() } + } + + pub fn from_static(val: &'static str) -> Str { + Str { bytes: Bytes::from_static(val.as_bytes()) } + } + + pub fn from_utf8(bytes: Bytes) -> Result { + if let Err(e) = str::from_utf8(&bytes[..]) { + return Err(FromUtf8Error { + err: e, + val: bytes, + }); + } + + Ok(Str { bytes: bytes }) + } + + pub unsafe fn from_utf8_unchecked(bytes: Bytes) -> Str { + Str { bytes: bytes } + } + */ +} + +impl From for Str { + fn from(src: String) -> Str { + Str { bytes: Bytes::from(src) } + } +} + +impl<'a> From<&'a str> for Str { + fn from(src: &'a str) -> Str { + Str { bytes: Bytes::from(src) } + } +} + +impl ops::Deref for Str { + type Target = str; + + fn deref(&self) -> &str { + let b: &[u8] = self.bytes.as_ref(); + unsafe { str::from_utf8_unchecked(b) } + } +} From b7d41a57e0eed0c965429320b9d13cbdad2551e9 Mon Sep 17 00:00:00 2001 From: Carl Lerche Date: Thu, 16 Mar 2017 22:35:00 -0700 Subject: [PATCH 2/6] Add method extensions --- Cargo.toml | 1 - src/lib.rs | 3 + src/method.rs | 202 +++++++++++++++++++++++++++++++++++++++++++------- src/str.rs | 57 -------------- 4 files changed, 179 insertions(+), 84 deletions(-) delete mode 100644 src/str.rs diff --git a/Cargo.toml b/Cargo.toml index 769cefd3..fb470d23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,3 @@ A set of types for representing HTTP requests and responses. """ [dependencies] -bytes = "0.4" diff --git a/src/lib.rs b/src/lib.rs index e69de29b..ef3c3def 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod method; + +pub use method::Method; diff --git a/src/method.rs b/src/method.rs index e8180911..51df55eb 100644 --- a/src/method.rs +++ b/src/method.rs @@ -1,10 +1,10 @@ //! The HTTP request method use self::Inner::*; -use str::Str; -use std::fmt; +use std::{fmt, str}; use std::convert::AsRef; +use std::error::Error; /// The Request Method (VERB) /// @@ -14,6 +14,9 @@ use std::convert::AsRef; #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub struct Method(Inner); +#[derive(Debug)] +pub struct FromBytesError; + #[derive(Clone, PartialEq, Eq, Hash, Debug)] enum Inner { Options, @@ -25,9 +28,56 @@ enum Inner { Trace, Connect, Patch, - Extension(Str) + // If the extension is short enough, store it inline + ExtensionInline([u8; MAX_INLINE], u8), + // Otherwise, allocate it + ExtensionAllocated(Box<[u8]>), } +const MAX_INLINE: usize = 15; + +// From the HTTP spec section 5.1.1, the HTTP method is case-sensitive and can +// contain the following characters: +// +// ``` +// method = token +// token = 1*tchar +// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / +// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA +// ``` +// +// https://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01#Method +// +const METHOD_CHARS: [u8; 256] = [ + // 0 1 2 3 4 5 6 7 8 9 + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 1x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 2x + b'\0', b'\0', b'\0', b'!', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 3x + b'\0', b'\0', b'*', b'+', b'\0', b'-', b'.', b'\0', b'0', b'1', // 4x + b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'\0', b'\0', // 5x + b'\0', b'\0', b'\0', b'\0', b'\0', b'A', b'B', b'C', b'D', b'E', // 6x + b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', // 7x + b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', // 8x + b'Z', b'\0', b'\0', b'\0', b'^', b'_', b'`', b'a', b'b', b'c', // 9x + b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', // 10x + b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', // 11x + b'x', b'y', b'z', b'\0', b'|', b'\0', b'~', b'\0', b'\0', b'\0', // 12x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 13x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 14x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 15x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 16x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 17x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 18x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 19x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 20x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 21x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 22x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 23x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 24x + b'\0', b'\0', b'\0', b'\0', b'\0', b'\0' // 25x +]; + /// GET pub const GET: Method = Method(Get); @@ -56,18 +106,90 @@ pub const PATCH: Method = Method(Patch); pub const TRACE: Method = Method(Trace); impl Method { - /// Return a `Method` instance representing a non-standard HTTP method. - /// - /// # Examples + /// Converts a slice of bytes to an HTTP method. + pub fn from_bytes(src: &[u8]) -> Result { + match src.len() { + 3 => { + match src { + b"GET" => Ok(Method(Get)), + b"PUT" => Ok(Method(Put)), + _ => Method::extension_inline_checked(src), + } + } + 4 => { + match src { + b"POST" => Ok(Method(Post)), + b"HEAD" => Ok(Method(Head)), + _ => Method::extension_inline_checked(src), + } + } + 5 => { + match src { + b"PATCH" => Ok(Method(Patch)), + b"TRACE" => Ok(Method(Trace)), + _ => Method::extension_inline_checked(src), + } + } + 6 => { + match src { + b"DELETE" => Ok(Method(Delete)), + _ => Method::extension_inline_checked(src), + } + } + 7 => { + match src { + b"OPTIONS" => Ok(Method(Options)), + b"CONNECT" => Ok(Method(Connect)), + _ => Method::extension_inline_checked(src), + } + } + _ => { + if src.len() < MAX_INLINE { + Method::extension_inline_checked(src) + } else { + Method::extension_allocated_checked(src) + } + } + } + } + + /// Converts a slice of bytes to an HTTP method without validating the input + /// data. /// - /// ``` - /// use http::Method; + /// The caller must ensure that the input is a valid HTTP method (see HTTP + /// spec section 5.1.1) and that the method is **not** a standard HTTP + /// method, i.e. one that is defined as a constant in this module. /// - /// let method = Method::extension("FOO"); - /// assert_eq!(method, "FOO"); - /// ``` - pub fn extension(s: &str) -> Method { - Method(Extension(s.into())) + /// The function is unsafe as the input is not checked as valid UTF-8. + pub unsafe fn from_bytes_unchecked(src: &[u8]) -> Method { + if src.len() < MAX_INLINE { + let mut data: [u8; MAX_INLINE] = Default::default(); + + data[0..src.len()].copy_from_slice(src); + + Method(ExtensionInline(data, src.len() as u8)) + } else { + let mut data = vec![]; + data.extend(src); + + Method(ExtensionAllocated(data.into_boxed_slice())) + } + } + + fn extension_inline_checked(src: &[u8]) -> Result { + let mut data: [u8; MAX_INLINE] = Default::default(); + + try!(write_checked(src, &mut data)); + + Ok(Method(ExtensionInline(data, src.len() as u8))) + } + + fn extension_allocated_checked(src: &[u8]) -> Result { + let mut data: Vec = vec![0; src.len()]; + + try!(write_checked(src, &mut data)); + + Ok(Method(ExtensionAllocated(data.into_boxed_slice()))) } /// Whether a method is considered "safe", meaning the request is @@ -99,6 +221,20 @@ impl Method { } } +fn write_checked(src: &[u8], dst: &mut [u8]) -> Result<(), FromBytesError> { + for (i, &b) in src.iter().enumerate() { + let b = METHOD_CHARS[b as usize]; + + if b == 0 { + return Err(FromBytesError); + } + + dst[i] = b; + } + + Ok(()) +} + impl AsRef for Method { fn as_ref(&self) -> &str { match self.0 { @@ -111,7 +247,16 @@ impl AsRef for Method { Trace => "TRACE", Connect => "CONNECT", Patch => "PATCH", - Extension(ref s) => s.as_ref() + ExtensionInline(ref data, len) => { + unsafe { + str::from_utf8_unchecked(&data[..len as usize]) + } + } + ExtensionAllocated(ref data) => { + unsafe { + str::from_utf8_unchecked(data) + } + } } } } @@ -130,18 +275,7 @@ impl<'a> PartialEq<&'a str> for Method { impl fmt::Display for Method { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - fmt.write_str(match self.0 { - Options => "OPTIONS", - Get => "GET", - Post => "POST", - Put => "PUT", - Delete => "DELETE", - Head => "HEAD", - Trace => "TRACE", - Connect => "CONNECT", - Patch => "PATCH", - Extension(ref s) => s.as_ref() - }) + fmt.write_str(self.as_ref()) } } @@ -150,3 +284,19 @@ impl Default for Method { GET } } + +impl fmt::Display for FromBytesError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.description()) + } +} + +impl Error for FromBytesError { + fn description(&self) -> &str { + "invalid HTTP method" + } + + fn cause(&self) -> Option<&Error> { + None + } +} diff --git a/src/str.rs b/src/str.rs deleted file mode 100644 index ef901449..00000000 --- a/src/str.rs +++ /dev/null @@ -1,57 +0,0 @@ -use bytes::Bytes; - -use std::{ops, str}; - -// TODO: Move this into `bytes` -#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] -pub struct Str { - bytes: Bytes, -} - -impl Str { - /* - pub fn new() -> Str { - Str { bytes: Bytes::new() } - } - - pub fn from_static(val: &'static str) -> Str { - Str { bytes: Bytes::from_static(val.as_bytes()) } - } - - pub fn from_utf8(bytes: Bytes) -> Result { - if let Err(e) = str::from_utf8(&bytes[..]) { - return Err(FromUtf8Error { - err: e, - val: bytes, - }); - } - - Ok(Str { bytes: bytes }) - } - - pub unsafe fn from_utf8_unchecked(bytes: Bytes) -> Str { - Str { bytes: bytes } - } - */ -} - -impl From for Str { - fn from(src: String) -> Str { - Str { bytes: Bytes::from(src) } - } -} - -impl<'a> From<&'a str> for Str { - fn from(src: &'a str) -> Str { - Str { bytes: Bytes::from(src) } - } -} - -impl ops::Deref for Str { - type Target = str; - - fn deref(&self) -> &str { - let b: &[u8] = self.bytes.as_ref(); - unsafe { str::from_utf8_unchecked(b) } - } -} From f4795fd0b8fa794ec465a5d2188580f2a3de0348 Mon Sep 17 00:00:00 2001 From: Carl Lerche Date: Fri, 17 Mar 2017 08:23:03 -0700 Subject: [PATCH 3/6] Remove Method::from_bytes_unchecked --- src/method.rs | 51 ++++++++++++--------------------------------------- 1 file changed, 12 insertions(+), 39 deletions(-) diff --git a/src/method.rs b/src/method.rs index 51df55eb..839e7697 100644 --- a/src/method.rs +++ b/src/method.rs @@ -113,70 +113,51 @@ impl Method { match src { b"GET" => Ok(Method(Get)), b"PUT" => Ok(Method(Put)), - _ => Method::extension_inline_checked(src), + _ => Method::extension_inline(src), } } 4 => { match src { b"POST" => Ok(Method(Post)), b"HEAD" => Ok(Method(Head)), - _ => Method::extension_inline_checked(src), + _ => Method::extension_inline(src), } } 5 => { match src { b"PATCH" => Ok(Method(Patch)), b"TRACE" => Ok(Method(Trace)), - _ => Method::extension_inline_checked(src), + _ => Method::extension_inline(src), } } 6 => { match src { b"DELETE" => Ok(Method(Delete)), - _ => Method::extension_inline_checked(src), + _ => Method::extension_inline(src), } } 7 => { match src { b"OPTIONS" => Ok(Method(Options)), b"CONNECT" => Ok(Method(Connect)), - _ => Method::extension_inline_checked(src), + _ => Method::extension_inline(src), } } _ => { if src.len() < MAX_INLINE { - Method::extension_inline_checked(src) + Method::extension_inline(src) } else { - Method::extension_allocated_checked(src) - } - } - } - } - - /// Converts a slice of bytes to an HTTP method without validating the input - /// data. - /// - /// The caller must ensure that the input is a valid HTTP method (see HTTP - /// spec section 5.1.1) and that the method is **not** a standard HTTP - /// method, i.e. one that is defined as a constant in this module. - /// - /// The function is unsafe as the input is not checked as valid UTF-8. - pub unsafe fn from_bytes_unchecked(src: &[u8]) -> Method { - if src.len() < MAX_INLINE { - let mut data: [u8; MAX_INLINE] = Default::default(); - - data[0..src.len()].copy_from_slice(src); + let mut data: Vec = vec![0; src.len()]; - Method(ExtensionInline(data, src.len() as u8)) - } else { - let mut data = vec![]; - data.extend(src); + try!(write_checked(src, &mut data)); - Method(ExtensionAllocated(data.into_boxed_slice())) + Ok(Method(ExtensionAllocated(data.into_boxed_slice()))) + } + } } } - fn extension_inline_checked(src: &[u8]) -> Result { + fn extension_inline(src: &[u8]) -> Result { let mut data: [u8; MAX_INLINE] = Default::default(); try!(write_checked(src, &mut data)); @@ -184,14 +165,6 @@ impl Method { Ok(Method(ExtensionInline(data, src.len() as u8))) } - fn extension_allocated_checked(src: &[u8]) -> Result { - let mut data: Vec = vec![0; src.len()]; - - try!(write_checked(src, &mut data)); - - Ok(Method(ExtensionAllocated(data.into_boxed_slice()))) - } - /// Whether a method is considered "safe", meaning the request is /// essentially read-only. /// From bdadf2b6cc80492a0ee4d02dfde82882d6eceb41 Mon Sep 17 00:00:00 2001 From: Carl Lerche Date: Fri, 17 Mar 2017 08:25:05 -0700 Subject: [PATCH 4/6] Don't reimplement default fn --- src/method.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/method.rs b/src/method.rs index 839e7697..fb0ca848 100644 --- a/src/method.rs +++ b/src/method.rs @@ -268,8 +268,4 @@ impl Error for FromBytesError { fn description(&self) -> &str { "invalid HTTP method" } - - fn cause(&self) -> Option<&Error> { - None - } } From 9611ecbf15824f70b99e283266918b4399afe586 Mon Sep 17 00:00:00 2001 From: Carl Lerche Date: Fri, 17 Mar 2017 08:27:10 -0700 Subject: [PATCH 5/6] Don't have FromBytesError be a unit struct --- src/method.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/method.rs b/src/method.rs index fb0ca848..4519f8e8 100644 --- a/src/method.rs +++ b/src/method.rs @@ -15,7 +15,9 @@ use std::error::Error; pub struct Method(Inner); #[derive(Debug)] -pub struct FromBytesError; +pub struct FromBytesError { + _priv: (), +} #[derive(Clone, PartialEq, Eq, Hash, Debug)] enum Inner { @@ -199,7 +201,7 @@ fn write_checked(src: &[u8], dst: &mut [u8]) -> Result<(), FromBytesError> { let b = METHOD_CHARS[b as usize]; if b == 0 { - return Err(FromBytesError); + return Err(FromBytesError::new()); } dst[i] = b; @@ -258,6 +260,14 @@ impl Default for Method { } } +impl FromBytesError { + fn new() -> FromBytesError { + FromBytesError { + _priv: (), + } + } +} + impl fmt::Display for FromBytesError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.description()) From 611f338f283a337fef3d09d67c146d042ac1d8b0 Mon Sep 17 00:00:00 2001 From: Carl Lerche Date: Fri, 17 Mar 2017 09:14:26 -0700 Subject: [PATCH 6/6] Add Method::as_str and impl as_ref in terms of it --- src/method.rs | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/method.rs b/src/method.rs index 4519f8e8..4dfa8e54 100644 --- a/src/method.rs +++ b/src/method.rs @@ -194,24 +194,9 @@ impl Method { } } } -} - -fn write_checked(src: &[u8], dst: &mut [u8]) -> Result<(), FromBytesError> { - for (i, &b) in src.iter().enumerate() { - let b = METHOD_CHARS[b as usize]; - - if b == 0 { - return Err(FromBytesError::new()); - } - dst[i] = b; - } - - Ok(()) -} - -impl AsRef for Method { - fn as_ref(&self) -> &str { + /// Return a &str representation of the HTTP method + pub fn as_str(&self) -> &str { match self.0 { Options => "OPTIONS", Get => "GET", @@ -236,6 +221,26 @@ impl AsRef for Method { } } +fn write_checked(src: &[u8], dst: &mut [u8]) -> Result<(), FromBytesError> { + for (i, &b) in src.iter().enumerate() { + let b = METHOD_CHARS[b as usize]; + + if b == 0 { + return Err(FromBytesError::new()); + } + + dst[i] = b; + } + + Ok(()) +} + +impl AsRef for Method { + fn as_ref(&self) -> &str { + self.as_str() + } +} + impl PartialEq for Method { fn eq(&self, other: &str) -> bool { self.as_ref() == other