azul-core 0.0.16

Common datatypes used for the Azul document object model, shared across all azul-* crates
Documentation
//! URL types for the C API.
//!
//! Provides a C-compatible, parsed-URL type. Key types: [`Url`],
//! [`UrlParseError`], [`ResultUrlUrlParseError`].
//!
//! The POD type and the cheap accessors live here in `azul-core` (so consumers
//! like `crate::video::VideoSource` can hold a typed `Url` without an
//! `azul-layout` dependency). `Url::parse` / `Url::join`, which rely on the
//! `url` crate, are gated behind the `url` feature; `azul_layout`'s `http`
//! feature enables it. Re-exported as `azul_layout::url`.

use alloc::string::String;
#[cfg(not(feature = "std"))]
use alloc::string::ToString;
use core::fmt;

use azul_css::{impl_result, impl_result_inner, AzString};

/// A parsed URL
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
#[repr(C)]
pub struct Url {
    /// The full URL string
    pub href: AzString,
    /// The scheme (e.g., "https")
    pub scheme: AzString,
    /// The host (e.g., "example.com")
    pub host: AzString,
    /// The port number, or 0 if not specified (sentinel value; see `effective_port()`)
    pub port: u16,
    /// The path (e.g., "/path/to/resource")
    pub path: AzString,
    /// The query string without '?' (e.g., "key=value")
    pub query: AzString,
    /// The fragment without '#' (e.g., "section")
    pub fragment: AzString,
}

/// Error when parsing a URL
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct UrlParseError {
    /// Error message
    pub message: AzString,
}

impl fmt::Display for UrlParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.message.as_str())
    }
}

#[cfg(feature = "std")]
impl std::error::Error for UrlParseError {}

// FFI-safe Result type for URL parsing
impl_result!(
    Url,
    UrlParseError,
    ResultUrlUrlParseError,
    copy = false,
    [Debug, Clone, PartialEq, Eq]
);

impl Url {
    /// Parse a URL from a string
    ///
    /// # Errors
    ///
    /// Returns a `UrlParseError` if `s` is not a valid absolute URL.
    #[cfg(feature = "url")]
    pub fn parse(s: &str) -> Result<Self, UrlParseError> {
        use ::url::Url as UrlParser;

        let parsed = UrlParser::parse(s).map_err(|e| UrlParseError {
            message: AzString::from(e.to_string()),
        })?;

        Ok(Self {
            href: AzString::from(parsed.as_str().to_string()),
            scheme: AzString::from(parsed.scheme().to_string()),
            host: AzString::from(parsed.host_str().unwrap_or("").to_string()),
            port: parsed.port().unwrap_or(0),
            path: AzString::from(parsed.path().to_string()),
            query: AzString::from(parsed.query().unwrap_or("").to_string()),
            fragment: AzString::from(parsed.fragment().unwrap_or("").to_string()),
        })
    }

    /// Create a URL from components
    #[must_use]
    pub fn from_parts(scheme: &str, host: &str, port: u16, path: &str) -> Self {
        let port_str = if port == 0
            || (scheme == "http" && port == 80)
            || (scheme == "https" && port == 443)
        {
            String::new()
        } else {
            alloc::format!(":{port}")
        };

        let href = alloc::format!("{scheme}://{host}{port_str}{path}");

        Self {
            href: AzString::from(href),
            scheme: AzString::from(scheme.to_string()),
            host: AzString::from(host.to_string()),
            port,
            path: AzString::from(path.to_string()),
            query: AzString::from(String::new()),
            fragment: AzString::from(String::new()),
        }
    }

    /// Get the full URL as a string slice
    #[must_use]
    pub fn as_str(&self) -> &str {
        self.href.as_str()
    }

    /// Check if this is an HTTPS URL
    #[must_use]
    pub fn is_https(&self) -> bool {
        self.scheme.as_str() == "https"
    }

    /// Check if this is an HTTP URL
    #[must_use]
    pub fn is_http(&self) -> bool {
        self.scheme.as_str() == "http"
    }

    /// Get the effective port (using default ports for http/https)
    #[must_use]
    pub fn effective_port(&self) -> u16 {
        if self.port != 0 {
            self.port
        } else if self.is_https() {
            443
        } else if self.is_http() {
            80
        } else {
            0
        }
    }

    /// Join a relative path to this URL
    ///
    /// # Errors
    ///
    /// Returns a `UrlParseError` if this URL's `href` is not parseable as a
    /// base, or if `path` cannot be resolved against it.
    #[cfg(feature = "url")]
    pub fn join(&self, path: &str) -> Result<Self, UrlParseError> {
        use ::url::Url as UrlParser;

        let base = UrlParser::parse(self.href.as_str()).map_err(|e| UrlParseError {
            message: AzString::from(e.to_string()),
        })?;

        let joined = base.join(path).map_err(|e| UrlParseError {
            message: AzString::from(e.to_string()),
        })?;

        Self::parse(joined.as_str())
    }

    /// Stub: `url` feature disabled (the `url` crate is gated behind it).
    #[cfg(not(feature = "url"))]
    /// # Errors
    ///
    /// Returns an error: the `url` feature is disabled, so URL parsing is unsupported.
    pub const fn parse(_s: &str) -> Result<Self, UrlParseError> {
        Err(UrlParseError {
            message: AzString::from_const_str("url feature not enabled"),
        })
    }

    /// Stub: `url` feature disabled (the `url` crate is gated behind it).
    #[cfg(not(feature = "url"))]
    /// # Errors
    ///
    /// Returns an error: the `url` feature is disabled, so URL joining is unsupported.
    pub const fn join(&self, _path: &str) -> Result<Self, UrlParseError> {
        Err(UrlParseError {
            message: AzString::from_const_str("url feature not enabled"),
        })
    }
}

impl fmt::Display for Url {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.href.as_str())
    }
}

#[cfg(test)]
#[path = "url_test.rs"]
mod url_test;