use crate::error::{Error as IriError, ErrorKind, Result as IriResult};
use crate::{Authority, Fragment, Normalize, Path, Port, Query, Scheme};
use regex::Regex;
use std::convert::TryFrom;
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct IRI {
scheme: Option<Scheme>,
authority: Option<Authority>,
path: Path,
query: Option<Query>,
fragment: Option<Fragment>,
}
pub type IRIRef = Arc<IRI>;
impl Default for IRI {
fn default() -> Self {
Self::new(&Path::default())
}
}
impl Display for IRI {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}{}",
match &self.scheme {
None => String::new(),
Some(scheme) => scheme.to_string(),
},
&self.scheme_specific_part()
)
}
}
impl FromStr for IRI {
type Err = IriError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_iri(s)
}
}
impl From<Path> for IRI {
fn from(path: Path) -> Self {
Self::from(&path)
}
}
impl From<&Path> for IRI {
fn from(path: &Path) -> Self {
Self::new(path)
}
}
#[cfg(feature = "path_iri")]
impl TryFrom<PathBuf> for IRI {
type Error = IriError;
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
Self::try_from(&path)
}
}
#[cfg(feature = "path_iri")]
impl TryFrom<&PathBuf> for IRI {
type Error = IriError;
fn try_from(path: &PathBuf) -> Result<Self, Self::Error> {
Self::new_file(path)
}
}
#[cfg(feature = "uuid_iri")]
impl TryFrom<uuid::Uuid> for IRI {
type Error = IriError;
fn try_from(path: uuid::Uuid) -> Result<Self, Self::Error> {
Self::try_from(&path)
}
}
#[cfg(feature = "uuid_iri")]
impl TryFrom<&uuid::Uuid> for IRI {
type Error = IriError;
fn try_from(path: &uuid::Uuid) -> Result<Self, Self::Error> {
Self::new_name("uuid", &path.to_hyphenated().to_string())
}
}
impl Normalize for IRI {
fn normalize(self) -> IriResult<Self> {
let mut normalized = Self {
scheme: match &self.scheme {
None => None,
Some(scheme) => Some(scheme.clone().normalize()?),
},
authority: match &self.authority {
None => None,
Some(authority) => {
let mut authority = authority.clone().normalize()?;
if self.has_scheme() && !authority.has_port() {
if let Some(port) = Port::default_for(&self.scheme().as_ref().unwrap()) {
authority.set_port(port);
}
}
Some(authority)
}
},
path: self.path.normalize()?,
query: match self.query {
None => None,
Some(query) => Some(query.normalize()?),
},
fragment: match self.fragment {
None => None,
Some(fragment) => Some(fragment.normalize()?),
},
};
if let Some(scheme) = normalized.scheme() {
if vec!["file", "ftp", "http", "https", "tftp"].contains(&scheme.value().as_str())
&& normalized.path.is_empty()
{
normalized.path = Path::root();
}
}
Ok(normalized)
}
}
impl IRI {
pub fn new(path: &Path) -> Self {
Self {
scheme: None,
authority: None,
path: path.clone(),
query: None,
fragment: None,
}
}
pub fn new_file(path: &PathBuf) -> IriResult<Self> {
Ok(Self {
scheme: Some(Scheme::file()),
authority: None,
path: Path::from_str(&path.to_string_lossy().to_string())?,
query: None,
fragment: None,
})
}
pub fn new_name(
namespace_identifier: &str,
namespace_specific_string: &str,
) -> IriResult<Self> {
Ok(Self {
scheme: Some(Scheme::urn()),
authority: None,
path: Path::from_str(&format!(
"{}:{}",
namespace_identifier, namespace_specific_string
))?,
query: None,
fragment: None,
})
}
pub fn with_new_path(&self, path: Path) -> Self {
Self {
path,
..self.clone()
}
}
pub fn without_path(&self) -> Self {
Self {
path: Path::default(),
..self.clone()
}
}
pub fn with_new_query(&self, query: Option<Query>) -> Self {
Self {
query,
..self.clone()
}
}
pub fn without_query(&self) -> Self {
Self {
query: None,
..self.clone()
}
}
pub fn with_new_fragment(&self, fragment: Option<Fragment>) -> Self {
Self {
fragment,
..self.clone()
}
}
pub fn without_fragment(&self) -> Self {
Self {
fragment: None,
..self.clone()
}
}
pub fn resolve(&self, relative: &IRI) -> IriResult<Self> {
if relative.is_absolute() || self.is_opaque() {
Ok(relative.clone())
} else if !relative.has_scheme()
&& !relative.has_authority()
&& relative.path().is_empty()
&& !relative.has_query()
&& relative.has_fragment()
{
Ok(self.with_new_fragment(relative.fragment().clone()))
} else {
unimplemented!()
}
}
pub fn relativize(&self, _other: &IRIRef) -> IriResult<Self> {
unimplemented!()
}
pub fn is_absolute(&self) -> bool {
self.has_scheme()
}
pub fn is_opaque(&self) -> bool {
let ssp = self.scheme_specific_part();
self.is_absolute() && !ssp.is_empty() && !ssp.starts_with("/")
}
pub fn has_scheme(&self) -> bool {
self.scheme.is_some()
}
pub fn scheme(&self) -> &Option<Scheme> {
&self.scheme
}
pub fn scheme_specific_part(&self) -> String {
format!(
"{}{}{}{}",
match &self.authority {
None => String::new(),
Some(authority) => authority.to_string(),
},
&self.path.to_string(),
match &self.query {
None => String::new(),
Some(query) => query.to_string(),
},
match &self.fragment {
None => String::new(),
Some(fragment) => fragment.to_string(),
},
)
}
pub fn has_authority(&self) -> bool {
self.authority.is_some()
}
pub fn authority(&self) -> &Option<Authority> {
&self.authority
}
pub fn has_path(&self) -> bool {
!self.path.is_empty()
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn has_query(&self) -> bool {
self.query.is_some()
}
pub fn query(&self) -> &Option<Query> {
&self.query
}
pub fn has_fragment(&self) -> bool {
self.fragment.is_some()
}
pub fn fragment(&self) -> &Option<Fragment> {
&self.fragment
}
pub fn set_scheme(&mut self, scheme: Option<Scheme>) {
self.scheme = scheme;
}
pub fn set_authority(&mut self, authority: Option<Authority>) {
self.authority = authority;
}
pub fn set_path(&mut self, path: Path) {
self.path = path;
}
pub fn set_query(&mut self, query: Option<Query>) {
self.query = query;
}
pub fn set_fragment(&mut self, fragment: Option<Fragment>) {
self.fragment = fragment;
}
}
const GRP_SCHEME: usize = 2;
const GRP_AUTHORITY: usize = 4;
const GRP_PATH: usize = 5;
const GRP_QUERY: usize = 7;
const GRP_FRAGMENT: usize = 9;
fn parse_iri(s: &str) -> IriResult<IRI> {
let regex = Regex::new(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?").unwrap();
match regex.captures(s) {
Some(captures) => Ok(IRI {
scheme: match captures.get(GRP_SCHEME) {
None => None,
Some(grp) => Some(Scheme::from_str(grp.as_str())?),
},
authority: match captures.get(GRP_AUTHORITY) {
None => None,
Some(grp) => Some(Authority::from_str(grp.as_str())?),
},
path: match captures.get(GRP_PATH) {
None => Path::default(),
Some(grp) => Path::from_str(grp.as_str())?,
},
query: match captures.get(GRP_QUERY) {
None => None,
Some(grp) => Some(Query::from_str(grp.as_str())?),
},
fragment: match captures.get(GRP_FRAGMENT) {
None => None,
Some(grp) => Some(Fragment::from_str(grp.as_str())?),
},
}),
None => Err(ErrorKind::Syntax(s.to_string()).into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Host;
#[test]
fn test_parse_iri_simple_url() {
let result = parse_iri(
"https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top",
);
assert!(result.is_ok());
println!("{:#?}", result);
let result = result.unwrap();
assert_eq!(result.scheme(), &Some(Scheme::https()));
let authority = result.authority().as_ref().unwrap();
assert_eq!(
authority.host(),
&Host::from_str("www.example.com").unwrap()
);
let user_info = authority.user_info().as_ref().unwrap();
assert_eq!(user_info.user_name(), &"john.doe".to_string());
assert_eq!(user_info.password(), &None);
assert_eq!(authority.port(), &Some(123.into()));
assert_eq!(result.path(), &Path::from_str("/forum/questions/").unwrap());
assert_eq!(result.fragment(), &Some(Fragment::from_str("top").unwrap()));
}
#[test]
fn test_parse_ldap_iri() {
let result = parse_iri("ldap://[2001:db8::7]/c=GB?objectClass?one");
assert!(result.is_ok());
let result = result.unwrap();
println!("{:#?}", result);
}
#[test]
fn test_parse_mailto_iri() {
let result = parse_iri("mailto:John.Doe@example.com");
assert!(result.is_ok());
let result = result.unwrap();
println!("{:#?}", result);
assert_eq!(result.scheme(), &Some(Scheme::mailto()));
assert_eq!(result.authority(), &None);
assert_eq!(
result.path(),
&Path::from_str("John.Doe@example.com").unwrap()
);
assert_eq!(result.fragment(), &None);
}
#[test]
fn test_parse_usenet_iri() {
let result = parse_iri("news:comp.infosystems.www.servers.unix");
assert!(result.is_ok());
let result = result.unwrap();
println!("{:#?}", result);
assert_eq!(result.scheme(), &Some(Scheme::news()));
assert_eq!(result.authority(), &None);
assert_eq!(
result.path(),
&Path::from_str("comp.infosystems.www.servers.unix").unwrap()
);
assert_eq!(result.fragment(), &None);
}
#[test]
fn test_parse_tel_iri() {
let result = parse_iri("tel:+1-816-555-1212");
assert!(result.is_ok());
let result = result.unwrap();
println!("{:#?}", result);
assert_eq!(result.scheme(), &Some(Scheme::tel()));
assert_eq!(result.authority(), &None);
assert_eq!(result.path(), &Path::from_str("+1-816-555-1212").unwrap());
assert_eq!(result.fragment(), &None);
}
#[test]
fn test_parse_telnet_iri() {
let result = parse_iri("telnet://192.0.2.16:80/");
assert!(result.is_ok());
let result = result.unwrap();
println!("{:#?}", result);
assert_eq!(result.scheme(), &Some(Scheme::telnet()));
let authority = result.authority().as_ref().unwrap();
assert_eq!(authority.host(), &Host::from_str("192.0.2.16").unwrap());
assert_eq!(authority.user_info(), &None);
assert_eq!(authority.port(), &Some(80.into()));
assert_eq!(result.path(), &Path::from_str("/").unwrap());
assert_eq!(result.fragment(), &None);
}
#[test]
fn test_parse_urn_iri() {
let result = parse_iri("urn:oasis:names:specification:docbook:dtd:xml:4.1.2");
assert!(result.is_ok());
let result = result.unwrap();
println!("{:#?}", result);
assert_eq!(result.scheme(), &Some(Scheme::urn()));
assert_eq!(result.authority(), &None);
assert_eq!(
result.path(),
&Path::from_str("oasis:names:specification:docbook:dtd:xml:4.1.2").unwrap()
);
assert_eq!(result.fragment(), &None);
}
#[test]
fn test_parse_iri_i18n_path() {
let result = parse_iri("https://en.wiktionary.org/wiki/Ῥόδος");
assert!(result.is_ok());
let result = result.unwrap();
println!("{:#?}", result);
assert_eq!(result.scheme(), &Some(Scheme::https()));
let authority = result.authority().as_ref().unwrap();
assert_eq!(
authority.host(),
&Host::from_str("en.wiktionary.org").unwrap()
);
assert_eq!(authority.user_info(), &None);
assert_eq!(authority.port(), &None);
assert_eq!(result.path(), &Path::from_str("/wiki/Ῥόδος").unwrap());
assert_eq!(result.fragment(), &None);
}
#[test]
fn test_parse_iri_i18n_host() {
let result = parse_iri("http://www.myfictionαlbank.com/");
assert!(result.is_ok());
let result = result.unwrap();
println!("{:#?}", result);
assert_eq!(result.scheme(), &Some(Scheme::http()));
let authority = result.authority().as_ref().unwrap();
assert_eq!(
authority.host(),
&Host::from_str("www.myfictionαlbank.com").unwrap()
);
assert_eq!(authority.user_info(), &None);
assert_eq!(authority.port(), &None);
assert_eq!(result.path(), &Path::from_str("/").unwrap());
assert_eq!(result.fragment(), &None);
}
}