/*!

This crate provides an `Iri` and `IriRef` type.
## Examples
The [`Iri`] type is a wrapper around the `url::Url` type, and as can be constructed in the
same manner.
```rust
use rdftk_iri::Iri;
use std::str::FromStr;
let result = Iri::from_str(
"https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top",
).unwrap();
```
The [`IriExtra`] trait provides a number of additional methods useful to the IRI as a namespace
and namespaced-name identifier.
```rust
use rdftk_iri::{Iri, IriExtra as _};
use std::str::FromStr;
let namespace = Iri::from_str(
"https://example.org/ns/things#",
).unwrap();
assert!(namespace.looks_like_namespace());
let name = namespace.make_name("ThisThing").unwrap();
assert_eq!(
name.to_string(),
"https://example.org/ns/things#ThisThing".to_string(),
);
assert_eq!(
name.namespace(),
Some(namespace),
);
assert_eq!(
name.name(),
Some("ThisThing".to_string()),
);
```
*/
#![warn(
unknown_lints,
// ---------- Stylistic
absolute_paths_not_starting_with_crate,
elided_lifetimes_in_paths,
explicit_outlives_requirements,
macro_use_extern_crate,
nonstandard_style, /* group */
noop_method_call,
rust_2018_idioms,
single_use_lifetimes,
trivial_casts,
trivial_numeric_casts,
// ---------- Future
future_incompatible, /* group */
rust_2021_compatibility, /* group */
// ---------- Public
missing_debug_implementations,
// missing_docs,
unreachable_pub,
// ---------- Unsafe
unsafe_code,
unsafe_op_in_unsafe_fn,
// ---------- Unused
unused, /* group */
)]
#![deny(
// ---------- Public
exported_private_dependencies,
// ---------- Deprecated
anonymous_parameters,
bare_trait_objects,
ellipsis_inclusive_range_patterns,
// ---------- Unsafe
deref_nullptr,
drop_bounds,
dyn_drop,
)]
// ------------------------------------------------------------------------------------------------
// Public Types
// ------------------------------------------------------------------------------------------------
///
/// The common type for IRI values used throughout the RDFtk packages.
///
pub type Iri = url::Url;
///
/// The reference-counted type wrapping an `Iri`.
///
pub type IriRef = std::sync::Arc<Iri>;
///
/// Errors reported while parsing a string into an IRI.
///
pub type Error = url::ParseError;
///
/// Additional, mainly constructor functions for the [`Iri`] type.
///
pub trait IriExtra {
///
/// Returns a copy of the current IRI with the path component replaced by `path`.
///
fn with_new_path<S>(&self, path: S) -> Self
where
S: AsRef<str>;
///
/// Returns a copy of the current IRI with the fragment component replaced by `fragment`.
///
/// Example
///
/// ```
/// use rdftk_iri::{genid, Iri, IriExtra};
/// use std::str::FromStr;
///
/// let no_fragment = Iri::from_str("https://example.org/ns").unwrap();
/// let empty_fragment = Iri::from_str("https://example.org/ns#").unwrap();
/// let some_fragment = Iri::from_str("https://example.org/ns#name").unwrap();
///
/// assert_eq!(some_fragment, no_fragment.with_new_fragment("name"));
///
/// assert_eq!(some_fragment, empty_fragment.with_new_fragment("name"));
///
/// assert_eq!(some_fragment, some_fragment.with_new_fragment("name"));
/// ```
///
fn with_new_fragment<S>(&self, fragment: S) -> Self
where
S: AsRef<str>;
///
/// Returns a copy of the current IRI with the fragment component replaced by an empty string.
///
///
/// Returns a copy of the current IRI with the fragment component removed.
///
/// Example
///
/// ```
/// use rdftk_iri::{genid, Iri, IriExtra};
/// use std::str::FromStr;
///
/// let no_fragment = Iri::from_str("https://example.org/ns").unwrap();
/// let empty_fragment = Iri::from_str("https://example.org/ns#").unwrap();
/// let some_fragment = Iri::from_str("https://example.org/ns#name").unwrap();
///
/// assert_eq!(empty_fragment, no_fragment.with_empty_fragment());
///
/// assert_eq!(empty_fragment, empty_fragment.with_empty_fragment());
///
/// assert_eq!(empty_fragment, some_fragment.with_empty_fragment());
/// ```
///
fn with_empty_fragment(&self) -> Self;
///
/// Returns a copy of the current IRI with the fragment component removed.
///
/// Example
///
/// ```
/// use rdftk_iri::{genid, Iri, IriExtra};
/// use std::str::FromStr;
///
/// let no_fragment = Iri::from_str("https://example.org/ns").unwrap();
/// let empty_fragment = Iri::from_str("https://example.org/ns#").unwrap();
/// let some_fragment = Iri::from_str("https://example.org/ns#name").unwrap();
///
/// assert_eq!(no_fragment, no_fragment.with_no_fragment());
///
/// assert_eq!(no_fragment, empty_fragment.with_no_fragment());
///
/// assert_eq!(no_fragment, some_fragment.with_no_fragment());
/// ```
///
fn with_no_fragment(&self) -> Self;
///
/// Returns `true` if this IRI may be used as a valid namespace. A valid namespace follows the
/// format:
///
/// 1. Has an empty, but present, fragment identifier.
/// 1. Or, it has a path ending with the character `'/'`,
/// 1. and, it does not have a query part.
///
/// Example
///
/// ```
/// use rdftk_iri::{genid, Iri, IriExtra};
/// use std::str::FromStr;
///
/// let maybe = Iri::from_str("https://example.org/ns/").unwrap();
/// assert!(maybe.looks_like_namespace());
///
/// let maybe = Iri::from_str("https://example.org/ns#").unwrap();
/// assert!(maybe.looks_like_namespace());
///
/// let maybe = Iri::from_str("https://example.org/ns/Name").unwrap();
/// assert!(!maybe.looks_like_namespace());
///
/// let maybe = Iri::from_str("https://example.org?q=10").unwrap();
/// assert!(!maybe.looks_like_namespace());
/// ```
///
fn looks_like_namespace(&self) -> bool;
///
/// IF this IRI represents a namespaced-name, return a (namespace, name) pair, else `None`.
///
/// Example
///
/// ```
/// use rdftk_iri::{genid, Iri, IriExtra};
/// use std::str::FromStr;
///
/// let namespace = Iri::from_str("https://example.org/ns/Name").unwrap();
/// assert_eq!(
/// namespace.split(),
/// Some((
/// Iri::from_str("https://example.org/ns/").unwrap(),
/// "Name".to_string(),
/// )),
/// );
///
/// let namespace = Iri::from_str("https://example.org/ns#Name").unwrap();
/// assert_eq!(
/// namespace.split(),
/// Some((
/// Iri::from_str("https://example.org/ns#").unwrap(),
/// "Name".to_string(),
/// )),
/// );
///
/// let namespace = Iri::from_str("https://example.org").unwrap();
/// assert_eq!(
/// namespace.split(),
/// None,
/// );
/// ```
///
fn split(&self) -> Option<(Self, String)>
where
Self: Sized;
///
/// IF this IRI represents a namespaced-name, return the namespace part, else `None`.
///
/// Example
///
/// ```
/// use rdftk_iri::{genid, Iri, IriExtra};
/// use std::str::FromStr;
///
/// let ns_name = Iri::from_str("https://example.org/ns/Name").unwrap();
/// assert_eq!(
/// ns_name.namespace(),
/// Some(Iri::from_str("https://example.org/ns/").unwrap()),
/// );
///
/// let ns_name = Iri::from_str("https://example.org/ns#Name").unwrap();
/// assert_eq!(
/// ns_name.namespace(),
/// Some(Iri::from_str("https://example.org/ns#").unwrap()),
/// );
///
/// let ns_name = Iri::from_str("https://example.org").unwrap();
/// assert_eq!(
/// ns_name.namespace(),
/// None,
/// );
/// ```
///
fn namespace(&self) -> Option<Self>
where
Self: Sized,
{
self.split().map(|(u, _)| u)
}
///
/// IF this IRI represents a namespaced-name, return the name part, else `None`.
///
/// Example
///
/// ```
/// use rdftk_iri::{genid, Iri, IriExtra};
/// use std::str::FromStr;
///
/// let ns_name = Iri::from_str("https://example.org/ns/Name").unwrap();
/// assert_eq!(
/// ns_name.name(),
/// Some("Name".to_string()),
/// );
///
/// let ns_name = Iri::from_str("https://example.org/ns#Name").unwrap();
/// assert_eq!(
/// ns_name.name(),
/// Some("Name".to_string()),
/// );
///
/// let ns_name = Iri::from_str("https://example.org").unwrap();
/// assert_eq!(
/// ns_name.namespace(),
/// None,
/// );
/// ```
///
fn name(&self) -> Option<String>
where
Self: Sized,
{
self.split().map(|(_, n)| n)
}
///
/// Assuming this IRI is a namespace, add the provided name.
///
/// Example
///
/// ```
/// use rdftk_iri::{genid, Iri, IriExtra};
/// use std::str::FromStr;
///
/// let namespace = Iri::from_str("https://example.org/ns/").unwrap();
/// assert_eq!(
/// namespace.make_name("Name").map(|s|s.to_string()),
/// Some("https://example.org/ns/Name".to_string()),
/// );
///
/// let namespace = Iri::from_str("https://example.org/ns#").unwrap();
/// assert_eq!(
/// namespace.make_name("Name").map(|s|s.to_string()),
/// Some("https://example.org/ns#Name".to_string()),
/// );
///
/// let namespace = Iri::from_str("https://example.org/ns").unwrap();
/// assert_eq!(
/// namespace.make_name("Name").map(|s|s.to_string()),
/// None,
/// );
/// ```
///
fn make_name<S>(&self, name: S) -> Option<Self>
where
S: AsRef<str>,
Self: Sized;
}
///
/// Returns a new IRI with a well-known path of `"genid"` using the scheme and server
/// components from `base`.
///
/// Example
///
/// ```
/// use rdftk_iri::{genid, Iri, IriRef, IriExtra};
/// use std::str::FromStr;
///
/// let base: IriRef = Iri::from_str("https://example.org/path#fragment").unwrap().into();
///
/// assert!(
/// genid(&base).unwrap().to_string().starts_with(
/// "https://example.org/.well-known/genid/"
/// )
/// );
/// ```
///
pub fn genid(base: &IriRef) -> Result<IriRef, Error> {
let new_uuid = uuid::Uuid::new_v4();
let new_uuid = new_uuid
.simple()
.encode_lower(&mut uuid::Uuid::encode_buffer())
.to_string();
let path = format!("/.well-known/genid/{new_uuid}");
Ok(IriRef::from(base.join(&path)?))
}
// ------------------------------------------------------------------------------------------------
// Implementations
// ------------------------------------------------------------------------------------------------
impl IriExtra for Iri {
fn with_new_path<S>(&self, path: S) -> Self
where
S: AsRef<str>,
{
let mut new_self = self.clone();
new_self.set_path(path.as_ref());
new_self
}
fn with_new_fragment<S>(&self, fragment: S) -> Self
where
S: AsRef<str>,
{
let mut new_self = self.clone();
new_self.set_fragment(Some(fragment.as_ref()));
new_self
}
fn with_empty_fragment(&self) -> Self {
self.with_new_fragment("")
}
fn with_no_fragment(&self) -> Self {
let mut new_self = self.clone();
new_self.set_fragment(None);
new_self
}
fn looks_like_namespace(&self) -> bool {
self.fragment() == Some("") || (self.path().ends_with("/") && self.query().is_none())
}
fn split(&self) -> Option<(Self, String)>
where
Self: Sized,
{
if self.fragment().map(|s| !s.is_empty()).unwrap_or_default() {
let name = self.fragment().unwrap().to_string();
Some((self.with_empty_fragment(), name))
} else if !self.path().is_empty() && !self.path().ends_with("/") && self.query().is_none() {
let name = self.path_segments().unwrap().last().unwrap();
let path = self.path();
let path = &path[0..path.len() - name.len()];
Some((self.with_new_path(path), name.to_string()))
} else {
None
}
}
fn make_name<S>(&self, name: S) -> Option<Self>
where
S: AsRef<str>,
Self: Sized,
{
if self.fragment() == Some("") {
Some(self.with_new_fragment(name.as_ref()))
} else if self.path().ends_with("/") && self.query().is_none() {
Some(self.with_new_path(&format!("{}{}", self.path(), name.as_ref())))
} else {
None
}
}
}