parent
cf0fddf2b4
commit
2f706a777f
@ -0,0 +1,632 @@ |
||||
use std::error::Error; |
||||
use std::fmt; |
||||
|
||||
#[derive(Eq, PartialEq, Debug, Clone, Hash)] |
||||
pub struct Iri { |
||||
iri: String, |
||||
positions: IriElementsPositions, |
||||
} |
||||
|
||||
impl Iri { |
||||
pub fn parse(iri: String) -> Result<Self, IriParseError> { |
||||
let base_positions = |
||||
parse_iri(iri.as_bytes(), 0).map_err(|position| IriParseError { position })?; |
||||
Ok(Self { |
||||
iri, |
||||
positions: base_positions, |
||||
}) |
||||
} |
||||
|
||||
pub fn resolve(&self, iri: &str) -> Result<Iri, IriParseError> { |
||||
let mut target_buffer = String::with_capacity(self.iri.len() + iri.len()); |
||||
let positions = resolve_relative_iri(iri, &self.iri, &self.positions, &mut target_buffer) |
||||
.map_err(|position| IriParseError { position })?; |
||||
Ok(Self { |
||||
iri: target_buffer, |
||||
positions, |
||||
}) |
||||
} |
||||
|
||||
pub fn into_string(self) -> String { |
||||
self.iri |
||||
} |
||||
} |
||||
|
||||
#[derive(Debug)] |
||||
pub struct IriParseError { |
||||
position: usize, |
||||
} |
||||
|
||||
impl fmt::Display for IriParseError { |
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
||||
write!(f, "Invalid IRI at char {}", self.position) |
||||
} |
||||
} |
||||
|
||||
impl Error for IriParseError {} |
||||
|
||||
type IriState = Result<usize, usize>; // usize = the end position
|
||||
|
||||
#[derive(Eq, PartialEq, Debug, Clone, Hash)] |
||||
struct IriElementsPositions { |
||||
scheme_end: usize, |
||||
authority_end: usize, |
||||
path_end: usize, |
||||
query_end: usize, |
||||
fragment_end: usize, |
||||
} |
||||
|
||||
// RFC 3986 5.2 Relative Resolution algorithm
|
||||
fn resolve_relative_iri( |
||||
reference_iri: &str, |
||||
base_iri: &str, |
||||
base_positions: &IriElementsPositions, |
||||
target_buffer: &mut String, |
||||
) -> Result<IriElementsPositions, usize> { |
||||
let base_scheme = &base_iri[0..base_positions.scheme_end]; |
||||
let base_authority = &base_iri[base_positions.scheme_end..base_positions.authority_end]; |
||||
let base_path = &base_iri[base_positions.authority_end..base_positions.path_end]; |
||||
let base_query = &base_iri[base_positions.path_end..base_positions.query_end]; |
||||
|
||||
let reference_positions = parse_iri_reference(reference_iri.as_bytes(), 0)?; |
||||
let r_scheme = &reference_iri[0..reference_positions.scheme_end]; |
||||
let r_authority = |
||||
&reference_iri[reference_positions.scheme_end..reference_positions.authority_end]; |
||||
let r_path = &reference_iri[reference_positions.authority_end..reference_positions.path_end]; |
||||
let r_query = &reference_iri[reference_positions.path_end..reference_positions.query_end]; |
||||
let r_fragment = &reference_iri[reference_positions.query_end..]; |
||||
|
||||
let scheme_end; |
||||
let authority_end; |
||||
let path_end; |
||||
let query_end; |
||||
let fragment_end; |
||||
|
||||
// if defined(R.scheme) then
|
||||
if !r_scheme.is_empty() { |
||||
// T.scheme = R.scheme;
|
||||
target_buffer.push_str(r_scheme); |
||||
scheme_end = target_buffer.len(); |
||||
|
||||
// T.authority = R.authority;
|
||||
target_buffer.push_str(r_authority); |
||||
authority_end = target_buffer.len(); |
||||
|
||||
// T.path = remove_dot_segments(R.path);
|
||||
append_and_remove_dot_segments(r_path, target_buffer, target_buffer.len()); |
||||
path_end = target_buffer.len(); |
||||
|
||||
// T.query = R.query;
|
||||
target_buffer.push_str(r_query); |
||||
query_end = target_buffer.len(); |
||||
|
||||
// T.fragment = R.fragment;
|
||||
target_buffer.push_str(r_fragment); |
||||
fragment_end = target_buffer.len(); |
||||
} else { |
||||
// T.scheme = Base.scheme;
|
||||
target_buffer.push_str(base_scheme); |
||||
scheme_end = target_buffer.len(); |
||||
|
||||
// if defined(R.authority) then
|
||||
if !r_authority.is_empty() { |
||||
// T.authority = R.authority;
|
||||
target_buffer.push_str(r_authority); |
||||
authority_end = target_buffer.len(); |
||||
|
||||
// T.path = remove_dot_segments(R.path);
|
||||
append_and_remove_dot_segments(r_path, target_buffer, target_buffer.len()); |
||||
path_end = target_buffer.len(); |
||||
|
||||
// T.query = R.query;
|
||||
target_buffer.push_str(r_query); |
||||
query_end = target_buffer.len(); |
||||
|
||||
// T.fragment = R.fragment;
|
||||
target_buffer.push_str(r_fragment); |
||||
fragment_end = target_buffer.len(); |
||||
} else { |
||||
// T.authority = Base.authority;
|
||||
target_buffer.push_str(base_authority); |
||||
authority_end = target_buffer.len(); |
||||
|
||||
// if (R.path == "") then
|
||||
if r_path == "" { |
||||
// T.path = Base.path;
|
||||
target_buffer.push_str(base_path); |
||||
path_end = target_buffer.len(); |
||||
|
||||
// if defined(R.query) then
|
||||
if !r_query.is_empty() { |
||||
// T.query = R.query;
|
||||
target_buffer.push_str(r_query); |
||||
} else { |
||||
// T.query = Base.query;
|
||||
target_buffer.push_str(base_query); |
||||
} |
||||
query_end = target_buffer.len(); |
||||
} else { |
||||
// if (R.path starts-with "/") then
|
||||
if r_path.starts_with('/') { |
||||
// T.path = remove_dot_segments(R.path);
|
||||
append_and_remove_dot_segments(r_path, target_buffer, target_buffer.len()); |
||||
} else { |
||||
let path_start_in_target = target_buffer.len(); |
||||
// T.path = merge(Base.path, R.path);
|
||||
// T.path = remove_dot_segments(T.path);
|
||||
if base_positions.authority_end > base_positions.scheme_end |
||||
&& base_positions.path_end == base_positions.authority_end |
||||
{ |
||||
append_and_remove_dot_segments_with_extra_slash( |
||||
r_path, |
||||
target_buffer, |
||||
path_start_in_target, |
||||
); |
||||
} else { |
||||
let last_base_slash = base_path |
||||
.char_indices() |
||||
.rev() |
||||
.find(|(_, c)| *c == '/') |
||||
.map_or(0, |(i, _)| i) |
||||
+ base_positions.authority_end; |
||||
append_and_remove_dot_segments( |
||||
&base_iri[base_positions.authority_end..=last_base_slash], |
||||
target_buffer, |
||||
path_start_in_target, |
||||
); |
||||
if target_buffer.ends_with('/') { |
||||
target_buffer.pop(); |
||||
append_and_remove_dot_segments_with_extra_slash( |
||||
r_path, |
||||
target_buffer, |
||||
path_start_in_target, |
||||
); |
||||
} else { |
||||
append_and_remove_dot_segments( |
||||
r_path, |
||||
target_buffer, |
||||
path_start_in_target, |
||||
); |
||||
} |
||||
} |
||||
} |
||||
path_end = target_buffer.len(); |
||||
|
||||
// T.query = R.query;
|
||||
target_buffer.push_str(r_query); |
||||
query_end = target_buffer.len(); |
||||
} |
||||
// T.fragment = R.fragment;
|
||||
target_buffer.push_str(r_fragment); |
||||
fragment_end = target_buffer.len(); |
||||
} |
||||
} |
||||
Ok(IriElementsPositions { |
||||
scheme_end, |
||||
authority_end, |
||||
path_end, |
||||
query_end, |
||||
fragment_end, |
||||
}) |
||||
} |
||||
|
||||
// RFC 3986 5.2.4 Remove Dot Segments
|
||||
fn append_and_remove_dot_segments( |
||||
mut input: &str, |
||||
output: &mut String, |
||||
path_start_in_output: usize, |
||||
) { |
||||
while !input.is_empty() { |
||||
if input.starts_with("../") { |
||||
input = &input[3..]; |
||||
} else if input.starts_with("./") || input.starts_with("/./") { |
||||
input = &input[2..]; |
||||
} else if input == "/." { |
||||
input = "/"; |
||||
} else if input.starts_with("/../") { |
||||
pop_last_segment(output, path_start_in_output); |
||||
input = &input[3..]; |
||||
} else if input == "/.." { |
||||
pop_last_segment(output, path_start_in_output); |
||||
input = "/"; |
||||
} else if input == "." || input == ".." { |
||||
input = ""; |
||||
} else { |
||||
if input.starts_with('/') { |
||||
output.push('/'); |
||||
input = &input[1..]; |
||||
} |
||||
if let Some(i) = input.find('/') { |
||||
output.push_str(&input[..i]); |
||||
input = &input[i..]; |
||||
} else { |
||||
output.push_str(input); |
||||
input = ""; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn pop_last_segment(buffer: &mut String, path_start_in_buffer: usize) { |
||||
if let Some((last_slash_position, _)) = buffer[path_start_in_buffer..] |
||||
.char_indices() |
||||
.rev() |
||||
.find(|(_, c)| *c == '/') |
||||
{ |
||||
buffer.truncate(last_slash_position + path_start_in_buffer) |
||||
} |
||||
} |
||||
|
||||
fn append_and_remove_dot_segments_with_extra_slash( |
||||
input: &str, |
||||
output: &mut String, |
||||
path_start_in_output: usize, |
||||
) { |
||||
if input.is_empty() { |
||||
output.push('/'); |
||||
} else if input.starts_with("./") { |
||||
append_and_remove_dot_segments(&input[1..], output, path_start_in_output) |
||||
} else if input == "." { |
||||
append_and_remove_dot_segments("/", output, path_start_in_output) |
||||
} else if input.starts_with("../") { |
||||
pop_last_segment(output, path_start_in_output); |
||||
append_and_remove_dot_segments(&input[2..], output, path_start_in_output) |
||||
} else if input == ".." { |
||||
pop_last_segment(output, path_start_in_output); |
||||
append_and_remove_dot_segments("/", output, path_start_in_output) |
||||
} else { |
||||
output.push('/'); |
||||
if let Some(i) = input.find('/') { |
||||
output.push_str(&input[..i]); |
||||
append_and_remove_dot_segments(&input[i..], output, path_start_in_output) |
||||
} else { |
||||
output.push_str(input); |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn parse_iri(value: &[u8], start: usize) -> Result<IriElementsPositions, usize> { |
||||
// IRI = scheme ":" ihier-part [ "?" iquery ] [ "#" ifragment ]
|
||||
let scheme_end = parse_scheme(value, start)?; |
||||
if scheme_end >= value.len() || value[scheme_end] != b':' { |
||||
return Err(scheme_end); |
||||
} |
||||
|
||||
let (authority_end, path_end) = parse_ihier_part(value, scheme_end + 1)?; |
||||
|
||||
let query_end = if path_end < value.len() && value[path_end] == b'?' { |
||||
parse_iquery(value, path_end + 1)? |
||||
} else { |
||||
path_end |
||||
}; |
||||
|
||||
let fragment_end = if query_end < value.len() && value[query_end] == b'#' { |
||||
parse_ifragment(value, query_end + 1)? |
||||
} else { |
||||
query_end |
||||
}; |
||||
|
||||
Ok(IriElementsPositions { |
||||
scheme_end: scheme_end + 1, |
||||
authority_end, |
||||
path_end, |
||||
query_end, |
||||
fragment_end, |
||||
}) |
||||
} |
||||
|
||||
fn parse_ihier_part(value: &[u8], start: usize) -> Result<(usize, usize), usize> { |
||||
// (authority_end, path_end)
|
||||
// ihier-part = "//" iauthority ipath-abempty / ipath-absolute / ipath-rootless / ipath-empty
|
||||
if value[start..].starts_with(b"//") { |
||||
let authority_end = parse_iauthority(value, start + 2)?; |
||||
Ok((authority_end, parse_ipath_abempty(value, authority_end)?)) |
||||
} else if value[start..].starts_with(b"/") { |
||||
Ok((start, parse_ipath_absolute(value, start)?)) |
||||
} else { |
||||
match parse_ipath_rootless(value, start) { |
||||
Ok(i) => Ok((start, i)), |
||||
Err(i) => { |
||||
if i == start { |
||||
Ok((start, i)) // ipath empty
|
||||
} else { |
||||
Err(i) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn parse_iri_reference(value: &[u8], start: usize) -> Result<IriElementsPositions, usize> { |
||||
// IRI-reference = IRI / irelative-ref
|
||||
match parse_iri(value, start) { |
||||
Ok(positions) => Ok(positions), |
||||
Err(_) => parse_irelative_ref(value, start), |
||||
} |
||||
} |
||||
|
||||
fn parse_irelative_ref(value: &[u8], start: usize) -> Result<IriElementsPositions, usize> { |
||||
// irelative-ref = irelative-part [ "?" iquery ] [ "#" ifragment ]
|
||||
let (authority_end, path_end) = parse_irelative_path(value, start)?; |
||||
|
||||
let query_end = if path_end < value.len() && value[path_end] == b'?' { |
||||
parse_iquery(value, path_end + 1)? |
||||
} else { |
||||
path_end |
||||
}; |
||||
let fragment_end = if query_end < value.len() && value[query_end] == b'#' { |
||||
parse_ifragment(&value, query_end + 1)? |
||||
} else { |
||||
query_end |
||||
}; |
||||
|
||||
Ok(IriElementsPositions { |
||||
scheme_end: start, |
||||
authority_end, |
||||
path_end, |
||||
query_end, |
||||
fragment_end, |
||||
}) |
||||
} |
||||
|
||||
fn parse_irelative_path(value: &[u8], start: usize) -> Result<(usize, usize), usize> { |
||||
// (authority_end, path_end)
|
||||
// irelative-part = "//" iauthority ipath-abempty / ipath-absolute / ipath-noscheme / ipath-empty
|
||||
if value[start..].starts_with(b"//") { |
||||
let authority_end = parse_iauthority(&value, start + 2)?; |
||||
Ok((authority_end, parse_ipath_abempty(value, authority_end)?)) |
||||
} else if value[start..].starts_with(b"/") { |
||||
Ok((start, parse_ipath_absolute(value, start)?)) |
||||
} else { |
||||
match parse_ipath_noscheme(value, start) { |
||||
Ok(i) => Ok((start, i)), |
||||
Err(i) => { |
||||
if i == start { |
||||
Ok((start, i)) // ipath empty
|
||||
} else { |
||||
Err(i) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn parse_scheme(value: &[u8], start: usize) -> IriState { |
||||
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
||||
if value.len() <= start || !is_alpha(value[start]) { |
||||
return Err(start); |
||||
} |
||||
for (i, c) in value[start..].iter().enumerate() { |
||||
match *c { |
||||
c if is_alpha(c) || is_digit(c) || c == b'+' || c == b'-' || c == b'.' => (), |
||||
_ => return Ok(start + i), |
||||
} |
||||
} |
||||
Err(value.len()) |
||||
} |
||||
|
||||
fn parse_iauthority(value: &[u8], start: usize) -> IriState { |
||||
// iauthority = [ iuserinfo "@" ] ihost [ ":" port ]
|
||||
//TODO: implement properly
|
||||
for (i, c) in value[start..].iter().enumerate() { |
||||
match *c { |
||||
b'/' | b'?' | b'#' => return Ok(start + i), |
||||
_ => (), |
||||
} |
||||
} |
||||
Ok(value.len()) |
||||
} |
||||
|
||||
fn parse_ipath_abempty(value: &[u8], start: usize) -> IriState { |
||||
// ipath-abempty = *( "/" isegment )
|
||||
let mut i = start; |
||||
while i < value.len() { |
||||
match value[i] { |
||||
b'/' => { |
||||
i = parse_isegment(value, i + 1)?; |
||||
} |
||||
_ => return Ok(i), |
||||
} |
||||
} |
||||
Ok(value.len()) |
||||
} |
||||
|
||||
fn parse_ipath_absolute(value: &[u8], start: usize) -> IriState { |
||||
// ipath-absolute = "/" [ isegment-nz *( "/" isegment ) ] = "/" [ isegment-nz ipath-abempty ]
|
||||
if !value[start..].starts_with(b"/") { |
||||
return Err(start); |
||||
} |
||||
|
||||
match parse_isegment_nz(value, start + 1) { |
||||
Ok(i) => parse_ipath_abempty(value, i), |
||||
Err(i) => { |
||||
if i == start + 1 { |
||||
Ok(i) // optional
|
||||
} else { |
||||
Err(i) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn parse_ipath_noscheme(value: &[u8], start: usize) -> IriState { |
||||
// ipath-noscheme = isegment-nz-nc *( "/" isegment ) = isegment-nz-nc ipath-abempty
|
||||
let i = parse_isegment_nz_nc(value, start)?; |
||||
parse_ipath_abempty(&value, i) |
||||
} |
||||
|
||||
fn parse_ipath_rootless(value: &[u8], start: usize) -> IriState { |
||||
// ipath-rootless = isegment-nz *( "/" isegment ) = isegment-nz ipath-abempty
|
||||
let i = parse_isegment_nz(value, start)?; |
||||
parse_ipath_abempty(value, i) |
||||
} |
||||
|
||||
fn parse_isegment(value: &[u8], start: usize) -> IriState { |
||||
// isegment = *ipchar
|
||||
//TODO: implement properly
|
||||
for (i, c) in value[start..].iter().enumerate() { |
||||
match *c { |
||||
b'/' | b'?' | b'#' => return Ok(start + i), |
||||
_ => (), |
||||
} |
||||
} |
||||
Ok(value.len()) |
||||
} |
||||
|
||||
fn parse_isegment_nz(value: &[u8], start: usize) -> IriState { |
||||
// isegment-nz = 1*ipchar
|
||||
let i = parse_isegment(value, start)?; |
||||
if i == start { |
||||
Err(0) |
||||
} else { |
||||
Ok(i) |
||||
} |
||||
} |
||||
|
||||
fn parse_isegment_nz_nc(value: &[u8], start: usize) -> IriState { |
||||
// isegment-nz-nc = 1*( iunreserved / pct-encoded / sub-delims / "@" )
|
||||
//TODO: implement properly
|
||||
for (i, c) in value[start..].iter().enumerate() { |
||||
match *c { |
||||
b'/' | b'?' | b'#' | b':' => return if i == start { Err(i) } else { Ok(i) }, |
||||
_ => (), |
||||
} |
||||
} |
||||
Ok(value.len()) |
||||
} |
||||
|
||||
fn parse_iquery(value: &[u8], start: usize) -> IriState { |
||||
// iquery = *( ipchar / iprivate / "/" / "?" )
|
||||
//TODO: implement properly
|
||||
for (i, c) in value[start..].iter().enumerate() { |
||||
if *c == b'#' { |
||||
return Ok(start + i); |
||||
} |
||||
} |
||||
Ok(value.len()) |
||||
} |
||||
|
||||
fn parse_ifragment(value: &[u8], _start: usize) -> IriState { |
||||
// ifragment = *( ipchar / "/" / "?" )
|
||||
//TODO: implement properly
|
||||
Ok(value.len()) |
||||
} |
||||
|
||||
fn is_alpha(b: u8) -> bool { |
||||
match b { |
||||
b'a'..=b'z' | b'A'..=b'Z' => true, |
||||
_ => false, |
||||
} |
||||
} |
||||
|
||||
fn is_digit(b: u8) -> bool { |
||||
match b { |
||||
b'0'..=b'9' => true, |
||||
_ => false, |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn test_parsing() { |
||||
let examples = [ |
||||
"file://foo", |
||||
"ftp://ftp.is.co.za/rfc/rfc1808.txt", |
||||
"http://www.ietf.org/rfc/rfc2396.txt", |
||||
"ldap://[2001:db8::7]/c=GB?objectClass?one", |
||||
"mailto:John.Doe@example.com", |
||||
"news:comp.infosystems.www.servers.unix", |
||||
"tel:+1-816-555-1212", |
||||
"telnet://192.0.2.16:80/", |
||||
"urn:oasis:names:specification:docbook:dtd:xml:4.1.2", |
||||
"http://example.com", |
||||
"http://example.com/", |
||||
"http://example.com/foo", |
||||
"http://example.com/foo/bar", |
||||
"http://example.com/foo/bar/", |
||||
"http://example.com/foo/bar?q=1&r=2", |
||||
"http://example.com/foo/bar/?q=1&r=2", |
||||
"http://example.com#toto", |
||||
"http://example.com/#toto", |
||||
"http://example.com/foo#toto", |
||||
"http://example.com/foo/bar#toto", |
||||
"http://example.com/foo/bar/#toto", |
||||
"http://example.com/foo/bar?q=1&r=2#toto", |
||||
"http://example.com/foo/bar/?q=1&r=2#toto", |
||||
]; |
||||
|
||||
for e in &examples { |
||||
assert!( |
||||
Iri::parse(e.to_string()).is_ok(), |
||||
"{} is not recognized as an IRI", |
||||
e |
||||
); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn test_resolve_relative_iri() { |
||||
let base = "http://a/b/c/d;p?q"; |
||||
|
||||
let examples = [ |
||||
("g:h", "g:h"), |
||||
("g", "http://a/b/c/g"), |
||||
("g/", "http://a/b/c/g/"), |
||||
("/g", "http://a/g"), |
||||
("//g", "http://g"), |
||||
("?y", "http://a/b/c/d;p?y"), |
||||
("g?y", "http://a/b/c/g?y"), |
||||
("#s", "http://a/b/c/d;p?q#s"), |
||||
("g#s", "http://a/b/c/g#s"), |
||||
("g?y#s", "http://a/b/c/g?y#s"), |
||||
(";x", "http://a/b/c/;x"), |
||||
("g;x", "http://a/b/c/g;x"), |
||||
("g;x?y#s", "http://a/b/c/g;x?y#s"), |
||||
("", "http://a/b/c/d;p?q"), |
||||
(".", "http://a/b/c/"), |
||||
("./", "http://a/b/c/"), |
||||
("./g", "http://a/b/c/g"), |
||||
("..", "http://a/b/"), |
||||
("../", "http://a/b/"), |
||||
("../g", "http://a/b/g"), |
||||
("../..", "http://a/"), |
||||
("../../", "http://a/"), |
||||
("../../g", "http://a/g"), |
||||
("../../../g", "http://a/g"), |
||||
("../../../../g", "http://a/g"), |
||||
("/./g", "http://a/g"), |
||||
("/../g", "http://a/g"), |
||||
("g.", "http://a/b/c/g."), |
||||
(".g", "http://a/b/c/.g"), |
||||
("g..", "http://a/b/c/g.."), |
||||
("..g", "http://a/b/c/..g"), |
||||
("./../g", "http://a/b/g"), |
||||
("./g/.", "http://a/b/c/g/"), |
||||
("g/./h", "http://a/b/c/g/h"), |
||||
("g/../h", "http://a/b/c/h"), |
||||
("g;x=1/./y", "http://a/b/c/g;x=1/y"), |
||||
("g;x=1/../y", "http://a/b/c/y"), |
||||
("g?y/./x", "http://a/b/c/g?y/./x"), |
||||
("g?y/../x", "http://a/b/c/g?y/../x"), |
||||
("g#s/./x", "http://a/b/c/g#s/./x"), |
||||
("g#s/../x", "http://a/b/c/g#s/../x"), |
||||
("http:g", "http:g"), |
||||
("./g:h", "http://a/b/c/g:h"), |
||||
]; |
||||
|
||||
let base = Iri::parse(base.to_owned()).unwrap(); |
||||
for (input, output) in examples.iter() { |
||||
let result = base.resolve(input); |
||||
assert!( |
||||
result.is_ok(), |
||||
"Resolving of {} failed with error: {}", |
||||
input, |
||||
result.unwrap_err() |
||||
); |
||||
let result = result.unwrap().into_string(); |
||||
assert_eq!( |
||||
result, *output, |
||||
"Resolving of {} is wrong. Found {} and expecting {}", |
||||
input, result, output |
||||
); |
||||
} |
||||
} |
Loading…
Reference in new issue