rfc2396 1.1.0

A library for validating strings as RFC2396-compliant URIs
Documentation
// Enable all clippy lints and enforce, and opt out of individual lints
#![cfg_attr(feature = "cargo-clippy", warn(clippy::cargo, clippy::pedantic, clippy::nursery))]
#![cfg_attr(feature = "cargo-clippy", allow(clippy::module_name_repetitions, clippy::non_ascii_literal))]

//! To use this crate, simply call one of the `validate*` functions, depending on your use case and desired return type:
//!
//! - [`validate(S) -> bool`]
//! - [`validate_nom(S) -> nom::IResult<&str, &str>`]
//! - [`validate_opt(S) -> Option<&str>`]
//! - [`validate_res(S) -> anyhow::Result<&str>`]
//!
//! Where `S` is some type that implements [`Into<&str>`]; for example, [`&str`] itself!
//!
//! All functions except the [`_nom`] variant will additionally ensure that the entire input is consumed and
//! matches exactly what was parsed internally. If you require that this check is not done (e.g. as part of a larger
//! parsing exercise), then use the [`_nom`] variant.
//!
//! [`validate(S) -> bool`]: fn.validate.html
//! [`validate_nom(S) -> nom::IResult<&str, &str>`]: fn.validate_nom.html
//! [`validate_opt(S) -> Option<&str>`]: fn.validate_opt.html
//! [`validate_res(S) -> anyhow::Result<&str>`]: fn.validate_res.html
//! [`Into<&str>`]: https://doc.rust-lang.org/std/convert/trait.Into.html
//! [`&str`]: https://doc.rust-lang.org/std/primitive.str.html
//! [`_nom`]: fn.validate_nom.html

use anyhow::Context;

mod combinators;
mod parsers;

/// Validate the given URI against [RFC2396], returning a [`bool`] indicating its validity.
///
/// Entire input consumption is also checked as part of validation.
///
/// # Examples
///
/// ```rust
/// let is_valid = rfc2396::validate("https://example.com");
/// assert!(is_valid);
/// ```
///
/// [RFC2396]: https://tools.ietf.org/html/rfc2396
/// [`bool`]: https://doc.rust-lang.org/std/primitive.bool.html
pub fn validate<'a, S>(uri: S) -> bool
where
    S: Into<&'a str>,
{
    let input = uri.into();

    match crate::parsers::uri_reference(input) {
        Ok((_, matched)) => input == matched,
        Err(_) => false,
    }
}

/// Validate the given URI against [RFC2396], returning a [`nom::IResult<&str, &str>`] indicating its validity.
///
/// # Examples
///
/// ```rust
/// let validation = rfc2396::validate_nom("https://example.com");
/// assert_eq!(validation, nom::IResult::Ok(("", "https://example.com")));
/// ```
///
/// # Errors
///
/// If validation fails, the raw, default [`nom::IResult`] generated by [`nom`] will be returned.
///
/// [RFC2396]: https://tools.ietf.org/html/rfc2396
/// [`nom::IResult<&str, &str>`]: https://docs.rs/nom/6.0.1/nom/type.IResult.html
/// [`nom::IResult`]: https://docs.rs/nom/6.0.1/nom/type.IResult.html
/// [`nom`]: https://crates.io/crates/nom
pub fn validate_nom<'a, S>(uri: S) -> nom::IResult<&'a str, &'a str>
where
    S: Into<&'a str>,
{
    crate::parsers::uri_reference(uri.into())
}

/// Validate the given URI against [RFC2396], returning an [`Option<&str>`] indicating its validity.
///
/// Entire input consumption is also checked as part of validation. The embedded [`&str`] is the input.
///
/// # Examples
///
/// ```rust
/// let valid_uri = rfc2396::validate_opt("https://example.com");
/// assert_eq!(valid_uri, Some("https://example.com"));
/// ```
///
/// [RFC2396]: https://tools.ietf.org/html/rfc2396
/// [`Option<&str>`]: https://doc.rust-lang.org/std/option/enum.Option.html
/// [`&str`]: https://doc.rust-lang.org/std/primitive.str.html
pub fn validate_opt<'a, S>(uri: S) -> Option<&'a str>
where
    S: Into<&'a str>,
{
    let input = uri.into();

    match crate::parsers::uri_reference(input) {
        Ok((_, matched)) if matched == input => Some(input),
        _ => None,
    }
}

/// Validate the given URI against [RFC2396], returning an [`anyhow::Result<&str>`] indicating its validity.
///
/// Entire input consumption is also checked as part of validation. The embedded [`&str`] is the input.
///
/// # Examples
///
/// ```rust
/// # /// <https://stackoverflow.com/a/58119924>
/// # fn type_of<T>(_: &T) -> &str {
/// #     std::any::type_name::<T>()
/// # }
/// #
/// # fn main() -> anyhow::Result<()> {
/// let validation = rfc2396::validate_res("https://example.com");
/// assert_eq!(type_of(&validation), std::any::type_name::<anyhow::Result<&str>>());
/// assert_eq!(validation?, "https://example.com");
/// #
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// If validation fails, a default [`nom::IResult`] converted into an [`anyhow::Result`] with additional context added
/// by [`anyhow`] will be returned.
///
/// [RFC2396]: https://tools.ietf.org/html/rfc2396
/// [`anyhow::Result<&str>`]: https://docs.rs/anyhow/1.0.36/anyhow/type.Result.html
/// [`&str`]: https://doc.rust-lang.org/std/primitive.str.html
/// [`nom::IResult`]: https://docs.rs/nom/6.0.1/nom/type.IResult.html
/// [`anyhow::Result`]: https://docs.rs/anyhow/1.0.36/anyhow/type.Result.html
/// [`anyhow`]:https://crates.io/crates/anyhow
pub fn validate_res<'a, S>(uri: S) -> anyhow::Result<&'a str>
where
    S: Into<&'a str>,
{
    let input = uri.into();

    (match crate::parsers::uri_reference(input) {
        Ok((_, matched)) => {
            if matched == input {
                Ok(input)
            } else {
                Err(anyhow::anyhow!("URI matched by parser (`{}`) does not equal input (`{}`)", matched, input))
            }
        }
        Err(error) => Err(anyhow::anyhow!(error.map(|mut e| e.input = input))),
    })
    .context("failed to parse URI")
}